From 8b979cb4b39173db87383dcdd64d41e8efef2cbd Mon Sep 17 00:00:00 2001 From: Aron Griffis Date: Thu, 6 Feb 2014 20:34:41 -0500 Subject: [PATCH 01/58] Reduce duplicate code in CssAbsolutizingTestCase --- compressor/tests/test_filters.py | 93 ++++++++++++++++---------------- 1 file changed, 45 insertions(+), 48 deletions(-) diff --git a/compressor/tests/test_filters.py b/compressor/tests/test_filters.py index b656a65..f390bb1 100644 --- a/compressor/tests/test_filters.py +++ b/compressor/tests/test_filters.py @@ -1,4 +1,5 @@ from __future__ import with_statement, unicode_literals +from collections import defaultdict import io import os import sys @@ -20,6 +21,10 @@ from compressor.filters.template import TemplateFilter from compressor.tests.test_base import test_dir +def blankdict(*args, **kwargs): + return defaultdict(lambda: '', *args, **kwargs) + + @unittest.skipIf(find_command(settings.COMPRESS_CSSTIDY_BINARY) is None, 'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY) class CssTidyTestCase(TestCase): @@ -99,8 +104,8 @@ class CssMinTestCase(TestCase): class CssAbsolutizingTestCase(TestCase): hashing_method = 'mtime' hashing_func = staticmethod(get_hashed_mtime) - content = ("p { background: url('../../img/python.png') }" - "p { filter: Alpha(src='../../img/python.png') }") + template = ("p { background: url('%(url)simg/python.png%(query)s%(hash)s%(frag)s') }" + "p { filter: Alpha(src='%(url)simg/python.png%(query)s%(hash)s%(frag)s') }") def setUp(self): self.old_enabled = settings.COMPRESS_ENABLED @@ -123,37 +128,34 @@ class CssAbsolutizingTestCase(TestCase): def test_css_absolute_filter(self): filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') - params = { + content = self.template % blankdict(url='../../') + params = blankdict({ 'url': settings.COMPRESS_URL, - 'hash': self.hashing_func(imagefilename), - } - output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" - "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params - filter = CssAbsoluteFilter(self.content) + 'hash': '?' + self.hashing_func(imagefilename), + }) + output = self.template % params + filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) settings.COMPRESS_URL = params['url'] = 'http://static.example.com/' - filter = CssAbsoluteFilter(self.content) - filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') - output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" - "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params + output = self.template % params + filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) def test_css_absolute_filter_url_fragment(self): filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') - params = { + content = self.template % blankdict(url='../../', frag='#foo') + params = blankdict({ 'url': settings.COMPRESS_URL, - 'hash': self.hashing_func(imagefilename), - } - content = "p { background: url('../../img/python.png#foo') }" - - output = "p { background: url('%(url)simg/python.png?%(hash)s#foo') }" % params + 'hash': '?' + self.hashing_func(imagefilename), + 'frag': '#foo', + }) + output = self.template % params filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) settings.COMPRESS_URL = params['url'] = 'http://media.example.com/' + output = self.template % params filter = CssAbsoluteFilter(content) - filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') - output = "p { background: url('%(url)simg/python.png?%(hash)s#foo') }" % params self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) def test_css_absolute_filter_only_url_fragment(self): @@ -163,60 +165,55 @@ class CssAbsolutizingTestCase(TestCase): self.assertEqual(content, filter.input(filename=filename, basename='css/url/test.css')) settings.COMPRESS_URL = 'http://media.example.com/' filter = CssAbsoluteFilter(content) - filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') self.assertEqual(content, filter.input(filename=filename, basename='css/url/test.css')) def test_css_absolute_filter_querystring(self): filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') - params = { + content = self.template % blankdict(url='../../', query='?foo') + params = blankdict({ 'url': settings.COMPRESS_URL, - 'hash': self.hashing_func(imagefilename), - } - content = "p { background: url('../../img/python.png?foo') }" - - output = "p { background: url('%(url)simg/python.png?foo&%(hash)s') }" % params + 'query': '?foo', + 'hash': '&' + self.hashing_func(imagefilename), + }) + output = self.template % params filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) settings.COMPRESS_URL = params['url'] = 'http://media.example.com/' + output = self.template % params filter = CssAbsoluteFilter(content) - filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') - output = "p { background: url('%(url)simg/python.png?foo&%(hash)s') }" % params self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) def test_css_absolute_filter_https(self): filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') - params = { + content = self.template % blankdict(url='../../') + params = blankdict({ 'url': settings.COMPRESS_URL, - 'hash': self.hashing_func(imagefilename), - } - output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" - "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params - filter = CssAbsoluteFilter(self.content) + 'hash': '?' + self.hashing_func(imagefilename), + }) + output = self.template % params + filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) settings.COMPRESS_URL = params['url'] = 'https://static.example.com/' - filter = CssAbsoluteFilter(self.content) - filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') - output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" - "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params + output = self.template % params + filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) def test_css_absolute_filter_relative_path(self): filename = os.path.join(settings.TEST_DIR, 'whatever', '..', 'static', 'whatever/../css/url/test.css') imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') - params = { + content = self.template % blankdict(url='../../') + params = blankdict({ 'url': settings.COMPRESS_URL, - 'hash': self.hashing_func(imagefilename), - } - output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" - "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params - filter = CssAbsoluteFilter(self.content) + 'hash': '?' + self.hashing_func(imagefilename), + }) + output = self.template % params + filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) settings.COMPRESS_URL = params['url'] = 'https://static.example.com/' - filter = CssAbsoluteFilter(self.content) - output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" - "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params + output = self.template % params + filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) def test_css_hunks(self): From db731cf01685001cf36188c4a70c44d3eb4dcc94 Mon Sep 17 00:00:00 2001 From: Aron Griffis Date: Thu, 6 Feb 2014 21:21:10 -0500 Subject: [PATCH 02/58] Fix #467: Remove unnecessary checks (filename in COMPRESS_ROOT, or existence of file) in CssAbsoluteFilter --- compressor/filters/css_default.py | 10 +--------- compressor/tests/test_filters.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/compressor/filters/css_default.py b/compressor/filters/css_default.py index 1727beb..44e21e7 100644 --- a/compressor/filters/css_default.py +++ b/compressor/filters/css_default.py @@ -5,7 +5,6 @@ import posixpath from compressor.cache import get_hashed_mtime, get_hashed_content from compressor.conf import settings from compressor.filters import FilterBase, FilterError -from compressor.utils import staticfiles URL_PATTERN = re.compile(r'url\(([^\)]+)\)') SRC_PATTERN = re.compile(r'src=([\'"])(.+?)\1') @@ -22,10 +21,7 @@ class CssAbsoluteFilter(FilterBase): self.has_scheme = False def input(self, filename=None, basename=None, **kwargs): - if filename is not None: - filename = os.path.normcase(os.path.abspath(filename)) - if (not (filename and filename.startswith(self.root)) and - not self.find(basename)): + if not filename: return self.content self.path = basename.replace(os.sep, '/') self.path = self.path.lstrip('/') @@ -40,10 +36,6 @@ class CssAbsoluteFilter(FilterBase): return SRC_PATTERN.sub(self.src_converter, URL_PATTERN.sub(self.url_converter, self.content)) - def find(self, basename): - if settings.DEBUG and basename and staticfiles.finders: - return staticfiles.finders.find(basename) - def guess_filename(self, url): local_path = url if self.has_scheme: diff --git a/compressor/tests/test_filters.py b/compressor/tests/test_filters.py index f390bb1..1ab5694 100644 --- a/compressor/tests/test_filters.py +++ b/compressor/tests/test_filters.py @@ -216,6 +216,21 @@ class CssAbsolutizingTestCase(TestCase): filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + def test_css_absolute_filter_filename_outside_compress_root(self): + filename = '/foo/bar/baz/test.css' + content = self.template % blankdict(url='../qux/') + params = blankdict({ + 'url': settings.COMPRESS_URL + 'bar/qux/', + }) + output = self.template % params + filter = CssAbsoluteFilter(content) + self.assertEqual(output, filter.input(filename=filename, basename='bar/baz/test.css')) + settings.COMPRESS_URL = 'https://static.example.com/' + params['url'] = settings.COMPRESS_URL + 'bar/qux/' + output = self.template % params + filter = CssAbsoluteFilter(content) + self.assertEqual(output, filter.input(filename=filename, basename='bar/baz/test.css')) + def test_css_hunks(self): hash_dict = { 'hash1': self.hashing_func(os.path.join(settings.COMPRESS_ROOT, 'img/python.png')), From 2651014eccfffa696b70869b19c9d0fea8290a91 Mon Sep 17 00:00:00 2001 From: Enrique Paredes Date: Fri, 14 Feb 2014 19:29:57 +0100 Subject: [PATCH 03/58] Fix: compressor.filter.base Nullify self.type for Specializations to filter_type --- compressor/filters/base.py | 2 +- compressor/tests/precompiler.py | 8 +++---- compressor/tests/test_filters.py | 36 +++++++++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/compressor/filters/base.py b/compressor/filters/base.py index a49a529..5842db5 100644 --- a/compressor/filters/base.py +++ b/compressor/filters/base.py @@ -26,7 +26,7 @@ class FilterBase(object): """ def __init__(self, content, filter_type=None, filename=None, verbose=0, charset=None): - self.type = filter_type + self.type = filter_type or getattr(self, 'type', None) self.content = content self.verbose = verbose or settings.COMPRESS_VERBOSE self.logger = logger diff --git a/compressor/tests/precompiler.py b/compressor/tests/precompiler.py index 059a322..5b3e9af 100644 --- a/compressor/tests/precompiler.py +++ b/compressor/tests/precompiler.py @@ -7,11 +7,11 @@ import sys def main(): p = optparse.OptionParser() p.add_option('-f', '--file', action="store", - type="string", dest="filename", - help="File to read from, defaults to stdin", default=None) + type="string", dest="filename", + help="File to read from, defaults to stdin", default=None) p.add_option('-o', '--output', action="store", - type="string", dest="outfile", - help="File to write to, defaults to stdout", default=None) + type="string", dest="outfile", + help="File to write to, defaults to stdout", default=None) options, arguments = p.parse_args() diff --git a/compressor/tests/test_filters.py b/compressor/tests/test_filters.py index b656a65..8bce62c 100644 --- a/compressor/tests/test_filters.py +++ b/compressor/tests/test_filters.py @@ -17,6 +17,10 @@ from compressor.filters.base import CompilerFilter from compressor.filters.cssmin import CSSMinFilter from compressor.filters.css_default import CssAbsoluteFilter from compressor.filters.template import TemplateFilter +from compressor.filters.closure import ClosureCompilerFilter +from compressor.filters.csstidy import CSSTidyFilter +from compressor.filters.yuglify import YUglifyCSSFilter, YUglifyJSFilter +from compressor.filters.yui import YUICSSFilter, YUIJSFilter from compressor.tests.test_base import test_dir @@ -30,7 +34,6 @@ class CssTidyTestCase(TestCase): color: black; } """) - from compressor.filters.csstidy import CSSTidyFilter ret = CSSTidyFilter(content).input() self.assertIsInstance(ret, six.text_type) self.assertEqual( @@ -301,3 +304,34 @@ class TemplateTestCase(TestCase): #footer {font-weight: bold;} """ self.assertEqual(input, TemplateFilter(content).input()) + + +class SpecializedFiltersTest(TestCase): + """ + Test to check the Specializations of filters. + """ + def test_closure_filter(self): + filter = ClosureCompilerFilter('') + self.assertEqual(filter.options, (('binary', six.text_type('java -jar compiler.jar')), ('args', six.text_type('')))) + + def test_csstidy_filter(self): + filter = CSSTidyFilter('') + self.assertEqual(filter.options, (('binary', six.text_type('csstidy')), ('args', six.text_type('--template=highest')))) + + def test_yuglify_filters(self): + filter = YUglifyCSSFilter('') + self.assertEqual(filter.command, '{binary} {args} --type=css') + self.assertEqual(filter.options, (('binary', six.text_type('yuglify')), ('args', six.text_type('--terminal')))) + + filter = YUglifyJSFilter('') + self.assertEqual(filter.command, '{binary} {args} --type=js') + self.assertEqual(filter.options, (('binary', six.text_type('yuglify')), ('args', six.text_type('--terminal')))) + + def test_yui_filters(self): + filter = YUICSSFilter('') + self.assertEqual(filter.command, '{binary} {args} --type=css') + self.assertEqual(filter.options, (('binary', six.text_type('java -jar yuicompressor.jar')), ('args', six.text_type('')))) + + filter = YUIJSFilter('', verbose=1) + self.assertEqual(filter.command, '{binary} {args} --type=js --verbose') + self.assertEqual(filter.options, (('binary', six.text_type('java -jar yuicompressor.jar')), ('args', six.text_type('')), ('verbose', 1))) From d8ebfb50c0665895d2635faeaa8c21510ad815b7 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 21 May 2014 12:07:59 +0200 Subject: [PATCH 04/58] Updated badges to be SVG and added Python3 port badge. --- README.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 93afc64..5739df3 100644 --- a/README.rst +++ b/README.rst @@ -1,19 +1,22 @@ Django Compressor ================= -.. image:: https://coveralls.io/repos/django-compressor/django-compressor/badge.png?branch=develop +.. image:: https://coveralls.io/repos/django-compressor/django-compressor/badge.svg?branch=develop :target: https://coveralls.io/r/django-compressor/django-compressor?branch=develop -.. image:: https://pypip.in/v/django_compressor/badge.png +.. image:: https://pypip.in/v/django_compressor/badge.svg :target: https://pypi.python.org/pypi/django_compressor -.. image:: https://pypip.in/d/django_compressor/badge.png +.. image:: https://pypip.in/d/django_compressor/badge.svg :target: https://pypi.python.org/pypi/django_compressor -.. image:: https://secure.travis-ci.org/django-compressor/django-compressor.png?branch=develop +.. image:: https://secure.travis-ci.org/django-compressor/django-compressor.svg?branch=develop :alt: Build Status :target: http://travis-ci.org/django-compressor/django-compressor +.. image:: https://caniusepython3.com/project/django_compressor.svg + :target: https://caniusepython3.com/project/django_compressor + Django Compressor combines and compresses linked and inline Javascript or CSS in a Django template into cacheable static files by using the ``compress`` template tag. From 7f1e9634bf8a9c2d85a1305da6a75f7dd4b171cc Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 21 May 2014 12:08:43 +0200 Subject: [PATCH 05/58] Fixed one badge that doesn't provide SVG. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 5739df3..86ed3d7 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ Django Compressor ================= -.. image:: https://coveralls.io/repos/django-compressor/django-compressor/badge.svg?branch=develop +.. image:: https://coveralls.io/repos/django-compressor/django-compressor/badge.png?branch=develop :target: https://coveralls.io/r/django-compressor/django-compressor?branch=develop .. image:: https://pypip.in/v/django_compressor/badge.svg From c8495264b503a595fbc89e6c8a83a402eff445c6 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Sun, 25 May 2014 14:09:33 +0200 Subject: [PATCH 06/58] Use get_template() when dealing with django templates --- compressor/offline/django.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/compressor/offline/django.py b/compressor/offline/django.py index 6541471..3986562 100644 --- a/compressor/offline/django.py +++ b/compressor/offline/django.py @@ -1,13 +1,12 @@ from __future__ import absolute_import -import io from copy import copy from django import template from django.conf import settings -from django.template import Template from django.template import Context from django.template.base import Node, VariableNode, TextNode, NodeList from django.template.defaulttags import IfNode +from django.template.loader import get_template from django.template.loader_tags import ExtendsNode, BlockNode, BlockContext @@ -93,13 +92,12 @@ class DjangoParser(object): self.charset = charset def parse(self, template_name): - with io.open(template_name, mode='rb') as file: - try: - return Template(file.read().decode(self.charset)) - except template.TemplateSyntaxError as e: - raise TemplateSyntaxError(str(e)) - except template.TemplateDoesNotExist as e: - raise TemplateDoesNotExist(str(e)) + try: + return get_template(template_name) + except template.TemplateSyntaxError as e: + raise TemplateSyntaxError(str(e)) + except template.TemplateDoesNotExist as e: + raise TemplateDoesNotExist(str(e)) def process_template(self, template, context): return True From 53e9c7ff1cd9fa8f78d3b89886d1bf308cb1f549 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Sun, 25 May 2014 15:32:16 +0200 Subject: [PATCH 07/58] Add support for COMPRESS_CSS_HASHING_METHOD = None --- compressor/filters/css_default.py | 2 ++ compressor/tests/test_filters.py | 32 +++++++++++++++++++++++-------- docs/settings.txt | 9 +++++---- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/compressor/filters/css_default.py b/compressor/filters/css_default.py index 1727beb..2e28cdd 100644 --- a/compressor/filters/css_default.py +++ b/compressor/filters/css_default.py @@ -70,6 +70,8 @@ class CssAbsoluteFilter(FilterBase): suffix = get_hashed_mtime(filename) elif settings.COMPRESS_CSS_HASHING_METHOD in ("hash", "content"): suffix = get_hashed_content(filename) + elif settings.COMPRESS_CSS_HASHING_METHOD is None: + suffix = None else: raise FilterError('COMPRESS_CSS_HASHING_METHOD is configured ' 'with an unknown method (%s).' % diff --git a/compressor/tests/test_filters.py b/compressor/tests/test_filters.py index 8bce62c..1362dfc 100644 --- a/compressor/tests/test_filters.py +++ b/compressor/tests/test_filters.py @@ -123,6 +123,24 @@ class CssAbsolutizingTestCase(TestCase): settings.COMPRESS_URL = self.old_url settings.COMPRESS_CSS_HASHING_METHOD = self.old_hashing_method + def test_css_no_hash(self): + settings.COMPRESS_CSS_HASHING_METHOD = None + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + params = { + 'url': settings.COMPRESS_URL, + } + output = ("p { background: url('%(url)simg/python.png') }" + "p { filter: Alpha(src='%(url)simg/python.png') }") % params + filter = CssAbsoluteFilter(self.content) + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + + settings.COMPRESS_URL = params['url'] = 'http://static.example.com/' + filter = CssAbsoluteFilter(self.content) + filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') + output = ("p { background: url('%(url)simg/python.png') }" + "p { filter: Alpha(src='%(url)simg/python.png') }") % params + self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + def test_css_absolute_filter(self): filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png') @@ -134,6 +152,7 @@ class CssAbsolutizingTestCase(TestCase): "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params filter = CssAbsoluteFilter(self.content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = params['url'] = 'http://static.example.com/' filter = CssAbsoluteFilter(self.content) filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') @@ -153,6 +172,7 @@ class CssAbsolutizingTestCase(TestCase): output = "p { background: url('%(url)simg/python.png?%(hash)s#foo') }" % params filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = params['url'] = 'http://media.example.com/' filter = CssAbsoluteFilter(content) filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') @@ -164,6 +184,7 @@ class CssAbsolutizingTestCase(TestCase): content = "p { background: url('#foo') }" filter = CssAbsoluteFilter(content) self.assertEqual(content, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = 'http://media.example.com/' filter = CssAbsoluteFilter(content) filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') @@ -181,6 +202,7 @@ class CssAbsolutizingTestCase(TestCase): output = "p { background: url('%(url)simg/python.png?foo&%(hash)s') }" % params filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = params['url'] = 'http://media.example.com/' filter = CssAbsoluteFilter(content) filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') @@ -198,6 +220,7 @@ class CssAbsolutizingTestCase(TestCase): "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params filter = CssAbsoluteFilter(self.content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = params['url'] = 'https://static.example.com/' filter = CssAbsoluteFilter(self.content) filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') @@ -216,6 +239,7 @@ class CssAbsolutizingTestCase(TestCase): "p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params filter = CssAbsoluteFilter(self.content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) + settings.COMPRESS_URL = params['url'] = 'https://static.example.com/' filter = CssAbsoluteFilter(self.content) output = ("p { background: url('%(url)simg/python.png?%(hash)s') }" @@ -256,14 +280,6 @@ class CssAbsolutizingTestCaseWithHash(CssAbsolutizingTestCase): hashing_method = 'content' hashing_func = staticmethod(get_hashed_content) - def setUp(self): - super(CssAbsolutizingTestCaseWithHash, self).setUp() - self.css = """ - - - """ - self.css_node = CssCompressor(self.css) - class CssDataUriTestCase(TestCase): def setUp(self): diff --git a/docs/settings.txt b/docs/settings.txt index 0d3fd72..4d2f470 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -81,10 +81,11 @@ Backend settings .. attribute:: COMPRESS_CSS_HASHING_METHOD - The method to use when calculating the hash to append to - processed URLs. Either ``'mtime'`` (default) or ``'content'``. - Use the latter in case you're using multiple server to serve your - static files. + The method to use when calculating the suffix to append to URLs in + your processed CSS files. Either ``None``, ``'mtime'`` (default) or + ``'content'``. Use the ``None`` if you want to completely disable that + feature, and the ``'content'`` in case you're using multiple servers + to serve your content. - ``compressor.filters.csstidy.CSSTidyFilter`` From d5a5fec5677efd0ecd1603a68fa4e535490bd1aa Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Sun, 25 May 2014 15:49:34 +0200 Subject: [PATCH 08/58] Fix cache backend for 1.7 compatibility (locmem:// has been deprecated for a while) --- compressor/test_settings.py | 2 +- compressor/tests/test_base.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compressor/test_settings.py b/compressor/test_settings.py index a5abf92..5531bcc 100644 --- a/compressor/test_settings.py +++ b/compressor/test_settings.py @@ -3,7 +3,7 @@ import django TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'tests') -COMPRESS_CACHE_BACKEND = 'locmem://' +COMPRESS_CACHE_BACKEND = 'django.core.cache.backends.locmem.LocMemCache' DATABASES = { 'default': { diff --git a/compressor/tests/test_base.py b/compressor/tests/test_base.py index 46b1d91..c6efe5c 100644 --- a/compressor/tests/test_base.py +++ b/compressor/tests/test_base.py @@ -267,4 +267,4 @@ class CacheBackendTestCase(CompressorTestCase): def test_correct_backend(self): from compressor.cache import cache - self.assertEqual(cache.__class__, locmem.CacheClass) + self.assertEqual(cache.__class__, locmem.LocMemCache) From 04f76dad57dcb60819275205502fd97c3c75f6e6 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Sun, 25 May 2014 15:19:15 +0200 Subject: [PATCH 09/58] Remove compatibility with old 'staticfiles' app --- compressor/test_settings.py | 7 +++++++ compressor/utils/staticfiles.py | 16 +++------------- docs/quickstart.txt | 6 ++---- docs/remote-storages.txt | 12 +++++------- 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/compressor/test_settings.py b/compressor/test_settings.py index 5531bcc..af33ec6 100644 --- a/compressor/test_settings.py +++ b/compressor/test_settings.py @@ -13,11 +13,18 @@ DATABASES = { } INSTALLED_APPS = [ + 'django.contrib.staticfiles', 'compressor', 'coffin', 'jingo', ] +STATICFILES_FINDERS = [ + 'django.contrib.staticfiles.finders.FileSystemFinder', + 'django.contrib.staticfiles.finders.AppDirectoriesFinder', + 'compressor.finders.CompressorFinder', +] + STATIC_URL = '/static/' diff --git a/compressor/utils/staticfiles.py b/compressor/utils/staticfiles.py index 28026f2..2d9ed00 100644 --- a/compressor/utils/staticfiles.py +++ b/compressor/utils/staticfiles.py @@ -4,20 +4,10 @@ from django.core.exceptions import ImproperlyConfigured from compressor.conf import settings -INSTALLED = ("staticfiles" in settings.INSTALLED_APPS or - "django.contrib.staticfiles" in settings.INSTALLED_APPS) +if "django.contrib.staticfiles" in settings.INSTALLED_APPS: + from django.contrib.staticfiles import finders # noqa -if INSTALLED: - if "django.contrib.staticfiles" in settings.INSTALLED_APPS: - from django.contrib.staticfiles import finders - else: - try: - from staticfiles import finders # noqa - except ImportError: - # Old (pre 1.0) and incompatible version of staticfiles - INSTALLED = False - - if (INSTALLED and "compressor.finders.CompressorFinder" + if ("compressor.finders.CompressorFinder" not in settings.STATICFILES_FINDERS): raise ImproperlyConfigured( "When using Django Compressor together with staticfiles, " diff --git a/docs/quickstart.txt b/docs/quickstart.txt index 4acfab2..612d10d 100644 --- a/docs/quickstart.txt +++ b/docs/quickstart.txt @@ -18,10 +18,8 @@ Installation * See the list of :ref:`settings` to modify Django Compressor's default behaviour and make adjustments for your website. -* In case you use Django's staticfiles_ contrib app (or its standalone - counterpart django-staticfiles_) you have to add Django Compressor's file - finder to the ``STATICFILES_FINDERS`` setting, for example with - ``django.contrib.staticfiles``: +* In case you use Django's staticfiles_ contrib app you have to add Django + Compressor's file finder to the ``STATICFILES_FINDERS`` setting, like this: .. code-block:: python diff --git a/docs/remote-storages.txt b/docs/remote-storages.txt index 91e7c2e..8af6934 100644 --- a/docs/remote-storages.txt +++ b/docs/remote-storages.txt @@ -39,12 +39,11 @@ The storage backend to save the compressed files needs to be changed, too:: Using staticfiles ^^^^^^^^^^^^^^^^^ -If you are using Django's staticfiles_ contrib app or the standalone -app django-staticfiles_, you'll need to use a temporary filesystem cache -for Django Compressor to know which files to compress. Since staticfiles -provides a management command to collect static files from various -locations which uses a storage backend, this is where both apps can be -integrated. +If you are using Django's staticfiles_ contrib app, you'll need to use a +temporary filesystem cache for Django Compressor to know which files to +compress. Since staticfiles provides a management command to collect static +files from various locations which uses a storage backend, this is where both +apps can be integrated. #. Make sure the :attr:`~django.conf.settings.COMPRESS_ROOT` and STATIC_ROOT_ settings are equal since both apps need to look at the same directories @@ -84,7 +83,6 @@ integrated. .. _Amazon S3: https://s3.amazonaws.com/ .. _boto: http://boto.cloudhackers.com/ .. _django-storages: http://code.welldev.org/django-storages/ -.. _django-staticfiles: http://github.com/jezdez/django-staticfiles/ .. _staticfiles: http://docs.djangoproject.com/en/dev/howto/static-files/ .. _STATIC_ROOT: http://docs.djangoproject.com/en/dev/ref/settings/#static-root .. _STATIC_URL: http://docs.djangoproject.com/en/dev/ref/settings/#static-url From 5c1bd405504cba56d0df2b39568c7e9ae57b0db9 Mon Sep 17 00:00:00 2001 From: GermanoGuerrini Date: Wed, 4 Dec 2013 15:02:46 +0100 Subject: [PATCH 10/58] Support for async/defer Now javascripts are compressed preserving async or defer. --- compressor/js.py | 32 ++++++++++++++++++-- compressor/templates/compressor/js_file.html | 2 +- compressor/tests/static/js/three.js | 1 + compressor/tests/static/js/two.js | 1 + compressor/tests/test_base.py | 28 +++++++++++++++++ compressor/tests/test_jinja2ext.py | 3 +- 6 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 compressor/tests/static/js/three.js create mode 100644 compressor/tests/static/js/two.js diff --git a/compressor/js.py b/compressor/js.py index b087804..387669c 100644 --- a/compressor/js.py +++ b/compressor/js.py @@ -12,14 +12,40 @@ class JsCompressor(Compressor): def split_contents(self): if self.split_content: return self.split_content + self.nodes = [] for elem in self.parser.js_elems(): attribs = self.parser.elem_attribs(elem) if 'src' in attribs: basename = self.get_basename(attribs['src']) filename = self.get_filename(basename) content = (SOURCE_FILE, filename, basename, elem) - self.split_content.append(content) else: - content = self.parser.elem_content(elem) - self.split_content.append((SOURCE_HUNK, content, None, elem)) + content = (SOURCE_HUNK, self.parser.elem_content(elem), None, elem) + self.split_content.append(content) + extra_attr = '' + if 'async' in attribs: + extra_attr = ' async' + elif 'defer' in attribs: + extra_attr = ' defer' + # Append to the previous node if it had the same attribute + append_to_previous = self.nodes and self.nodes[-1][0] == extra_attr + if append_to_previous and settings.COMPRESS_ENABLED: + self.nodes[-1][1].split_content.append(content) + else: + node = JsCompressor(content=self.parser.elem_str(elem), + context=self.context) + node.split_content.append(content) + self.nodes.append((extra_attr, node)) return self.split_content + + def output(self, *args, **kwargs): + if (settings.COMPRESS_ENABLED or settings.COMPRESS_PRECOMPILERS or + kwargs.get('forced', False)): + self.split_contents() + if hasattr(self, 'nodes'): + ret = [] + for extra_attr, subnode in self.nodes: + subnode.extra_context.update({'extra_attr': extra_attr}) + ret.append(subnode.output(*args, **kwargs)) + return '\n'.join(ret) + return super(JsCompressor, self).output(*args, **kwargs) diff --git a/compressor/templates/compressor/js_file.html b/compressor/templates/compressor/js_file.html index 09d6a9b..e3f5d25 100644 --- a/compressor/templates/compressor/js_file.html +++ b/compressor/templates/compressor/js_file.html @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/compressor/tests/static/js/three.js b/compressor/tests/static/js/three.js new file mode 100644 index 0000000..e01a825 --- /dev/null +++ b/compressor/tests/static/js/three.js @@ -0,0 +1 @@ +hermanos = {} \ No newline at end of file diff --git a/compressor/tests/static/js/two.js b/compressor/tests/static/js/two.js new file mode 100644 index 0000000..595f5b5 --- /dev/null +++ b/compressor/tests/static/js/two.js @@ -0,0 +1 @@ +pollos = {} \ No newline at end of file diff --git a/compressor/tests/test_base.py b/compressor/tests/test_base.py index a68db96..66cbbe7 100644 --- a/compressor/tests/test_base.py +++ b/compressor/tests/test_base.py @@ -252,3 +252,31 @@ class CacheBackendTestCase(CompressorTestCase): def test_correct_backend(self): from compressor.cache import cache self.assertEqual(cache.__class__, locmem.CacheClass) + + +class JsAsyncDeferTestCase(SimpleTestCase): + def setUp(self): + self.js = """\ + + + + + + + """ + + def test_js_output(self): + def extract_attr(tag): + if tag.has_attr('async'): + return 'async' + if tag.has_attr('defer'): + return 'defer' + js_node = JsCompressor(self.js) + output = [None, 'async', 'defer', None, 'async', None] + if six.PY3: + scripts = make_soup(js_node.output()).find_all('script') + attrs = [extract_attr(i) for i in scripts] + else: + scripts = make_soup(js_node.output()).findAll('script') + attrs = [s.get('async') or s.get('defer') for s in scripts] + self.assertEqual(output, attrs) diff --git a/compressor/tests/test_jinja2ext.py b/compressor/tests/test_jinja2ext.py index c77580f..d2ab97d 100644 --- a/compressor/tests/test_jinja2ext.py +++ b/compressor/tests/test_jinja2ext.py @@ -56,8 +56,7 @@ class TestJinja2CompressorExtension(TestCase): self.assertEqual(tag_body, template.render()) def test_empty_tag(self): - template = self.env.from_string("""{% compress js %}{% block js %} - {% endblock %}{% endcompress %}""") + template = self.env.from_string("""{% compress js %}{% block js %}{% endblock %}{% endcompress %}""") context = {'STATIC_URL': settings.COMPRESS_URL} self.assertEqual('', template.render(context)) From a327860deba935dfd847702e5927ab1dab633419 Mon Sep 17 00:00:00 2001 From: GermanoGuerrini Date: Wed, 4 Dec 2013 15:02:46 +0100 Subject: [PATCH 11/58] Support for async/defer Now javascripts are compressed preserving async or defer. --- compressor/tests/test_base.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/compressor/tests/test_base.py b/compressor/tests/test_base.py index 66cbbe7..8917da8 100644 --- a/compressor/tests/test_base.py +++ b/compressor/tests/test_base.py @@ -63,6 +63,22 @@ class CompressorTestCase(SimpleTestCase): """ self.js_node = JsCompressor(self.js) + def assertEqualCollapsed(self, a, b): + """ + assertEqual with internal newlines collapsed to single, and + trailing whitespace removed. + """ + collapse = lambda x: re.sub(r'\n+', '\n', x).rstrip() + self.assertEqual(collapse(a), collapse(b)) + + def assertEqualSplits(self, a, b): + """ + assertEqual for splits, particularly ignoring the presence of + a trailing newline on the content. + """ + mangle = lambda split: [(x[0], x[1], x[2], x[3].rstrip()) for x in split] + self.assertEqual(mangle(a), mangle(b)) + def test_css_split(self): out = [ ( @@ -85,7 +101,7 @@ class CompressorTestCase(SimpleTestCase): ] split = self.css_node.split_contents() split = [(x[0], x[1], x[2], self.css_node.parser.elem_str(x[3])) for x in split] - self.assertEqual(out, split) + self.assertEqualSplits(split, out) def test_css_hunks(self): out = ['body { background:#990; }', 'p { border:5px solid green;}', 'body { color:#fff; }'] @@ -104,7 +120,7 @@ class CompressorTestCase(SimpleTestCase): def test_css_return_if_off(self): settings.COMPRESS_ENABLED = False - self.assertEqual(self.css, self.css_node.output()) + self.assertEqualCollapsed(self.css, self.css_node.output()) def test_cachekey(self): is_cachekey = re.compile(r'\w{12}') @@ -132,7 +148,7 @@ class CompressorTestCase(SimpleTestCase): ] split = self.js_node.split_contents() split = [(x[0], x[1], x[2], self.js_node.parser.elem_str(x[3])) for x in split] - self.assertEqual(out, split) + self.assertEqualSplits(split, out) def test_js_hunks(self): out = ['obj = {};', 'obj.value = "value";'] @@ -154,7 +170,7 @@ class CompressorTestCase(SimpleTestCase): @override_settings(COMPRESS_PRECOMPILERS=(), COMPRESS_ENABLED=False) def test_js_return_if_off(self): - self.assertEqual(self.js, self.js_node.output()) + self.assertEqualCollapsed(self.js, self.js_node.output()) def test_js_return_if_on(self): output = '' @@ -251,7 +267,7 @@ class CacheBackendTestCase(CompressorTestCase): def test_correct_backend(self): from compressor.cache import cache - self.assertEqual(cache.__class__, locmem.CacheClass) + self.assertEqual(cache.__class__, locmem.LocMemCache) class JsAsyncDeferTestCase(SimpleTestCase): From 405af243169a97c8a7e6b1206111ee0dc0ab1ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Wed, 4 Jun 2014 15:39:42 -0400 Subject: [PATCH 12/58] Fix link markup --- docs/django-sekizai.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/django-sekizai.txt b/docs/django-sekizai.txt index 6fd80c9..ab70b17 100644 --- a/docs/django-sekizai.txt +++ b/docs/django-sekizai.txt @@ -3,12 +3,12 @@ django-sekizai Support ====================== -Django Compressor comes with support for _django-sekizai via an extension. -_django-sekizai provides the ability to include template code, from within +Django Compressor comes with support for django-sekizai_ via an extension. +django-sekizai provides the ability to include template code, from within any block, to a parent block. It is primarily used to include js/css from included templates to the master template. -It requires _django-sekizai to installed. Refer to the _django-sekizai _docs +It requires django-sekizai to be installed. Refer to the `django-sekizai docs`_ for how to use ``render_block`` Usage @@ -21,4 +21,4 @@ Usage .. _django-sekizai: https://github.com/ojii/django-sekizai -.. _docs: http://django-sekizai.readthedocs.org/en/latest/ +.. _django-sekizai docs: http://django-sekizai.readthedocs.org/en/latest/ From f551dabdb4cbf30a22d5c6fc29f7db9c389fdf90 Mon Sep 17 00:00:00 2001 From: Markus Amalthea Magnuson Date: Wed, 16 Jul 2014 14:03:22 +0200 Subject: [PATCH 13/58] Update usage.txt --- docs/usage.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage.txt b/docs/usage.txt index 3e18a8f..5bf665e 100644 --- a/docs/usage.txt +++ b/docs/usage.txt @@ -48,7 +48,7 @@ Which would be rendered something like: .. note:: - Remember that django-compressor will try to :ref:`group ouputs by media `. + Remember that django-compressor will try to :ref:`group outputs by media `. Linked files **must** be accessible via :attr:`~django.conf.settings.COMPRESS_URL`. From 4e13e9a791204a328d4b380afcc3a2f6d7ff8195 Mon Sep 17 00:00:00 2001 From: Aron Griffis Date: Wed, 16 Jul 2014 08:46:24 -0400 Subject: [PATCH 14/58] Update test_css_no_hash pursuant to changes in CssAbsolutizingTestCase from 626bc52007378bedb39a40f4a0334f0e20ba49f6. --- compressor/tests/test_filters.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/compressor/tests/test_filters.py b/compressor/tests/test_filters.py index d2033e7..c023b69 100644 --- a/compressor/tests/test_filters.py +++ b/compressor/tests/test_filters.py @@ -131,19 +131,17 @@ class CssAbsolutizingTestCase(TestCase): def test_css_no_hash(self): settings.COMPRESS_CSS_HASHING_METHOD = None filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') - params = { + content = self.template % blankdict(url='../../') + params = blankdict({ 'url': settings.COMPRESS_URL, - } - output = ("p { background: url('%(url)simg/python.png') }" - "p { filter: Alpha(src='%(url)simg/python.png') }") % params - filter = CssAbsoluteFilter(self.content) + }) + output = self.template % params + filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) settings.COMPRESS_URL = params['url'] = 'http://static.example.com/' - filter = CssAbsoluteFilter(self.content) - filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css') - output = ("p { background: url('%(url)simg/python.png') }" - "p { filter: Alpha(src='%(url)simg/python.png') }") % params + output = self.template % params + filter = CssAbsoluteFilter(content) self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css')) def test_css_absolute_filter(self): From e189da7fc21d5bdd4f46f8cdcc8535834ab78c1d Mon Sep 17 00:00:00 2001 From: Lukasz Balcerzak Date: Fri, 18 Jul 2014 15:42:46 +0200 Subject: [PATCH 15/58] Types at readme (-engine should be --engine) --- docs/jinja2.txt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/jinja2.txt b/docs/jinja2.txt index 134b0b8..b585047 100644 --- a/docs/jinja2.txt +++ b/docs/jinja2.txt @@ -46,9 +46,9 @@ This can be a lamda or function that returns a Jinja2 environment. Usage ----- -Run the following compress command along with an ``-engine`` parameter. The +Run the following compress command along with an ``--engine`` parameter. The parameter can be either jinja2 or django (default). For example, -"./manage.py compress -engine jinja2". +"./manage.py compress --engine jinja2". Using both Django and Jinja2 templates -------------------------------------- @@ -62,7 +62,7 @@ A typical usage could be : - "./manage.py compress" for processing Django templates first, skipping Jinja2 templates. -- "./manage.py compress -engine jinja2" for processing Jinja2 templates, +- "./manage.py compress --engine jinja2" for processing Jinja2 templates, skipping Django templates. However, it is still recommended that you do not mix Django and Jinja2 @@ -172,4 +172,3 @@ Jinja2 alone (with custom loader) are tested and work on Python 2.6, 2.7 and .. _Jinja2: http://jinja.pocoo.org/docs/ .. _Coffin: http://pypi.python.org/pypi/Coffin .. _Jingo: https://jingo.readthedocs.org/en/latest/ - From 369065c6a7070b9395d4bbfd6cb61da3ed59b62f Mon Sep 17 00:00:00 2001 From: Aron Griffis Date: Fri, 18 Jul 2014 10:50:27 -0400 Subject: [PATCH 16/58] Apply shell quoting to infile/outfile. #536 --- compressor/filters/base.py | 11 +++++++++++ .../tests/static/css/filename with spaces.css | 1 + compressor/tests/test_filters.py | 15 +++++++++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 compressor/tests/static/css/filename with spaces.css diff --git a/compressor/filters/base.py b/compressor/filters/base.py index 2174613..fcc56ab 100644 --- a/compressor/filters/base.py +++ b/compressor/filters/base.py @@ -3,6 +3,11 @@ import io import logging import subprocess +try: + from shlex import quote as shell_quote # Python 3 +except ImportError: + from pipes import quote as shell_quote # Python 2 + from django.core.exceptions import ImproperlyConfigured from django.core.files.temp import NamedTemporaryFile from django.utils.importlib import import_module @@ -147,6 +152,12 @@ class CompilerFilter(FilterBase): self.outfile = NamedTemporaryFile(mode='r+', suffix=ext) options["outfile"] = self.outfile.name + # Quote infile and outfile for spaces etc. + if "infile" in options: + options["infile"] = shell_quote(options["infile"]) + if "outfile" in options: + options["outfile"] = shell_quote(options["outfile"]) + try: command = self.command.format(**options) proc = subprocess.Popen( diff --git a/compressor/tests/static/css/filename with spaces.css b/compressor/tests/static/css/filename with spaces.css new file mode 100644 index 0000000..239f51c --- /dev/null +++ b/compressor/tests/static/css/filename with spaces.css @@ -0,0 +1 @@ +body { background:#424242; } \ No newline at end of file diff --git a/compressor/tests/test_filters.py b/compressor/tests/test_filters.py index c023b69..084b50c 100644 --- a/compressor/tests/test_filters.py +++ b/compressor/tests/test_filters.py @@ -47,10 +47,13 @@ class CssTidyTestCase(TestCase): class PrecompilerTestCase(TestCase): def setUp(self): - self.filename = os.path.join(test_dir, 'static/css/one.css') + self.test_precompiler = os.path.join(test_dir, 'precompiler.py') + self.setup_infile() + + def setup_infile(self, filename='static/css/one.css'): + self.filename = os.path.join(test_dir, filename) with io.open(self.filename, encoding=settings.FILE_CHARSET) as file: self.content = file.read() - self.test_precompiler = os.path.join(test_dir, 'precompiler.py') def test_precompiler_infile_outfile(self): command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler) @@ -59,6 +62,14 @@ class PrecompilerTestCase(TestCase): charset=settings.FILE_CHARSET, command=command) self.assertEqual("body { color:#990; }", compiler.input()) + def test_precompiler_infile_with_spaces(self): + self.setup_infile('static/css/filename with spaces.css') + command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler) + compiler = CompilerFilter( + content=self.content, filename=self.filename, + charset=settings.FILE_CHARSET, command=command) + self.assertEqual("body { color:#424242; }", compiler.input()) + def test_precompiler_infile_stdout(self): command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler) compiler = CompilerFilter( From 92eaece0a1f62026519ab7d89dbbd7d96aa5ee25 Mon Sep 17 00:00:00 2001 From: Vladimir Bolshakov Date: Fri, 22 Aug 2014 19:11:21 +0400 Subject: [PATCH 17/58] Support for clean-css (https://github.com/GoalSmashers/clean-css/) --- compressor/conf.py | 2 ++ compressor/filters/cleancss.py | 10 ++++++++++ compressor/tests/test_filters.py | 5 +++++ docs/settings.txt | 15 +++++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 compressor/filters/cleancss.py diff --git a/compressor/conf.py b/compressor/conf.py index e9763d9..87f9d69 100644 --- a/compressor/conf.py +++ b/compressor/conf.py @@ -45,6 +45,8 @@ class CompressorConf(AppConf): YUGLIFY_BINARY = 'yuglify' YUGLIFY_CSS_ARGUMENTS = '--terminal' YUGLIFY_JS_ARGUMENTS = '--terminal' + CLEAN_CSS_BINARY = 'cleancss' + CLEAN_CSS_ARGUMENTS = '' DATA_URI_MAX_SIZE = 1024 # the cache backend to use diff --git a/compressor/filters/cleancss.py b/compressor/filters/cleancss.py new file mode 100644 index 0000000..daa381a --- /dev/null +++ b/compressor/filters/cleancss.py @@ -0,0 +1,10 @@ +from compressor.conf import settings +from compressor.filters import CompilerFilter + + +class CleanCSSFilter(CompilerFilter): + command = "{binary} {args} -o {outfile} {infile}" + options = ( + ("binary", settings.COMPRESS_CLEAN_CSS_BINARY), + ("args", settings.COMPRESS_CLEAN_CSS_ARGUMENTS), + ) diff --git a/compressor/tests/test_filters.py b/compressor/tests/test_filters.py index 084b50c..784d89a 100644 --- a/compressor/tests/test_filters.py +++ b/compressor/tests/test_filters.py @@ -22,6 +22,7 @@ from compressor.filters.closure import ClosureCompilerFilter from compressor.filters.csstidy import CSSTidyFilter from compressor.filters.yuglify import YUglifyCSSFilter, YUglifyJSFilter from compressor.filters.yui import YUICSSFilter, YUIJSFilter +from compressor.filters.cleancss import CleanCSSFilter from compressor.tests.test_base import test_dir @@ -372,3 +373,7 @@ class SpecializedFiltersTest(TestCase): filter = YUIJSFilter('', verbose=1) self.assertEqual(filter.command, '{binary} {args} --type=js --verbose') self.assertEqual(filter.options, (('binary', six.text_type('java -jar yuicompressor.jar')), ('args', six.text_type('')), ('verbose', 1))) + + def test_clean_css_filter(self): + filter = CleanCSSFilter('') + self.assertEqual(filter.options, (('binary', six.text_type('cleancss')), ('args', six.text_type('')))) diff --git a/docs/settings.txt b/docs/settings.txt index 4d2f470..84950a4 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -137,9 +137,24 @@ Backend settings A filter that uses Zachary Voase's Python port of the YUI CSS compression algorithm cssmin_. + - ``compressor.filters.cleancss.CleanCSSFilter`` + + A filter that passes the CSS content to the `clean-css`_ tool. + + .. attribute:: CLEAN_CSS_BINARY + + The clean-css binary filesystem path. + + .. attribute:: CLEAN_CSS_ARGUMENTS + + The arguments passed to clean-css. + + .. _CSSTidy: http://csstidy.sourceforge.net/ .. _`data: URIs`: http://en.wikipedia.org/wiki/Data_URI_scheme .. _cssmin: http://pypi.python.org/pypi/cssmin/ + .. _`clean-css`: https://github.com/GoalSmashers/clean-css/ + - ``compressor.filters.template.TemplateFilter`` From ac70fbfb7f8349e5c1150f40bf1d21ba635d0cb8 Mon Sep 17 00:00:00 2001 From: Chirag Jadwani Date: Fri, 29 Aug 2014 13:15:35 +0530 Subject: [PATCH 18/58] Fix AttributeError in offline compression --- compressor/offline/django.py | 2 ++ .../test_compressor_offline.html | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/compressor/offline/django.py b/compressor/offline/django.py index 3986562..b326093 100644 --- a/compressor/offline/django.py +++ b/compressor/offline/django.py @@ -54,6 +54,8 @@ def remove_block_nodes(nodelist, block_stack, block_context): if not block_stack: continue node = block_context.get_block(block_stack[-1].name) + if not node: + continue if isinstance(node, BlockNode): expanded_block = expand_blocknode(node, block_stack, block_context) new_nodelist.extend(expanded_block) diff --git a/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html b/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html index 01382ec..10097c1 100644 --- a/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html +++ b/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html @@ -5,4 +5,9 @@ + {% block orphan %} + {{ block.super }} + An 'orphan' block that refers to a non-existent super block. + Contents of this block are ignored. + {% endblock %} {% endspaceless %}{% endblock %} From a4dcc8cbb12d11a3aa1f0279cd0fe3a6d1e5e517 Mon Sep 17 00:00:00 2001 From: Francesco Zaia Date: Mon, 1 Sep 2014 12:17:38 +0100 Subject: [PATCH 19/58] Update quickstart.txt SlimIt main domain has expired --- docs/quickstart.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.txt b/docs/quickstart.txt index 4acfab2..bf57c76 100644 --- a/docs/quickstart.txt +++ b/docs/quickstart.txt @@ -95,6 +95,6 @@ Optional .. _lxml: http://codespeak.net/lxml/ .. _libxml2: http://xmlsoft.org/ .. _html5lib: http://code.google.com/p/html5lib/ -.. _`Slim It`: http://slimit.org/ +.. _`Slim It`: https://github.com/rspivak/slimit .. _django-appconf: http://pypi.python.org/pypi/django-appconf/ .. _versiontools: http://pypi.python.org/pypi/versiontools/ From dc77320c197c15bb47f0aae8c2072a96e65a1fbd Mon Sep 17 00:00:00 2001 From: Francesco Zaia Date: Mon, 1 Sep 2014 12:34:02 +0100 Subject: [PATCH 20/58] Update settings.txt --- docs/settings.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/settings.txt b/docs/settings.txt index 4d2f470..b31386d 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -221,7 +221,7 @@ Backend settings .. _`Google Closure compiler`: http://code.google.com/closure/compiler/ .. _`YUI compressor`: http://developer.yahoo.com/yui/compressor/ .. _`yUglify compressor`: https://github.com/yui/yuglify - .. _`Slim It`: http://slimit.org/ + .. _`Slim It`: https://github.com/rspivak/slimit .. attribute:: COMPRESS_PRECOMPILERS From 1a96480d9e3472af78d0dc7bcaf28f3a1be07b70 Mon Sep 17 00:00:00 2001 From: Corey Farwell Date: Tue, 2 Sep 2014 22:04:37 -0400 Subject: [PATCH 21/58] Run automated tests on Django 1.7 --- .travis.yml | 4 ++++ tox.ini | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/.travis.yml b/.travis.yml index 81020b7..307b0c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,10 @@ install: script: - tox env: + - TOXENV=py34-1.7.X + - TOXENV=py33-1.7.X + - TOXENV=py32-1.7.X + - TOXENV=py27-1.7.X - TOXENV=py33-1.6.X - TOXENV=py32-1.6.X - TOXENV=py27-1.6.X diff --git a/tox.ini b/tox.ini index 1aa5e81..32eeab2 100644 --- a/tox.ini +++ b/tox.ini @@ -54,6 +54,30 @@ commands = django-admin.py --version make test +[testenv:py34-1.7.X] +basepython = python3.4 +deps = + Django>=1.7,<1.8 + {[deps]three} + +[testenv:py33-1.7.X] +basepython = python3.3 +deps = + Django>=1.7,<1.8 + {[deps]three} + +[testenv:py32-1.7.X] +basepython = python3.2 +deps = + Django>=1.7,<1.8 + {[deps]three_two} + +[testenv:py27-1.7.X] +basepython = python2.7 +deps = + Django>=1.7,<1.8 + {[deps]two} + [testenv:py33-1.6.X] basepython = python3.3 deps = From 6d5f6e04f297555be2242bc625a08c05eaec69ec Mon Sep 17 00:00:00 2001 From: gordon Date: Wed, 17 Sep 2014 13:52:47 -0400 Subject: [PATCH 22/58] shlex.quote/pipes.quote is not compatible with Windows --- compressor/filters/base.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/compressor/filters/base.py b/compressor/filters/base.py index fcc56ab..f46c35c 100644 --- a/compressor/filters/base.py +++ b/compressor/filters/base.py @@ -3,10 +3,18 @@ import io import logging import subprocess -try: - from shlex import quote as shell_quote # Python 3 -except ImportError: - from pipes import quote as shell_quote # Python 2 +from platform import system + +if system() != "Windows": + try: + from shlex import quote as shell_quote # Python 3 + except ImportError: + from pipes import quote as shell_quote # Python 2 +else: + from subprocess import list2cmdline + def shell_quote(s): + # shlex.quote/pipes.quote is not compatible with Windows + return list2cmdline([s]) from django.core.exceptions import ImproperlyConfigured from django.core.files.temp import NamedTemporaryFile From 8170f2cd81419eb2090fd641517f0b85ea58db02 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Fri, 19 Sep 2014 11:44:01 +0200 Subject: [PATCH 23/58] Mention that we dropped Django 1.3.x support in changelog. --- docs/changelog.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 3828197..26a4a35 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -1,12 +1,12 @@ Changelog ========= -v1.4 ----- +v1.4 (06/20/2014) +----------------- - Added Python 3 compatibility. -- Added compatibility with Django 1.6.x. +- Added compatibility with Django 1.6.x and dropped support for Django 1.3.X. - Fixed compatibility with html5lib 1.0. @@ -46,7 +46,7 @@ v1.3 (03/18/2013) - Dropped support for Python 2.5. Removed ``any`` and ``walk`` compatibility functions in ``compressor.utils``. - - Removed compatibility with Django 1.2 for default values of some settings: + - Removed compatibility with some old django setttings: - :attr:`~COMPRESS_ROOT` no longer uses ``MEDIA_ROOT`` if ``STATIC_ROOT`` is not defined. It expects ``STATIC_ROOT`` to be defined instead. From 042b45f6fdf204a1801fafb259ffe25ba0771be7 Mon Sep 17 00:00:00 2001 From: Jere Malinen Date: Fri, 10 Oct 2014 12:03:16 +0300 Subject: [PATCH 24/58] Fix reading UTF-8 files which have BOM https://github.com/django-compressor/django-compressor/issues/567 --- compressor/base.py | 3 +++ compressor/tests/static/css/utf-8_with-BOM.css | 1 + compressor/tests/test_base.py | 8 ++++++++ 3 files changed, 12 insertions(+) create mode 100644 compressor/tests/static/css/utf-8_with-BOM.css diff --git a/compressor/base.py b/compressor/base.py index de9c9ce..ae3a2c4 100644 --- a/compressor/base.py +++ b/compressor/base.py @@ -140,6 +140,9 @@ class Compressor(object): """ Reads file contents using given `charset` and returns it as text. """ + if charset == 'utf-8': + # Removes BOM + charset = 'utf-8-sig' with codecs.open(filename, 'r', charset) as fd: try: return fd.read() diff --git a/compressor/tests/static/css/utf-8_with-BOM.css b/compressor/tests/static/css/utf-8_with-BOM.css new file mode 100644 index 0000000..06a6209 --- /dev/null +++ b/compressor/tests/static/css/utf-8_with-BOM.css @@ -0,0 +1 @@ +.compress-test {color: red;} \ No newline at end of file diff --git a/compressor/tests/test_base.py b/compressor/tests/test_base.py index c6efe5c..d4de9d1 100644 --- a/compressor/tests/test_base.py +++ b/compressor/tests/test_base.py @@ -112,6 +112,14 @@ class CompressorTestCase(SimpleTestCase): hunks = '\n'.join([h for h in self.css_node.hunks()]) self.assertEqual(out, hunks) + def test_css_output_with_bom_input(self): + out = 'body { background:#990; }\n.compress-test {color: red;}' + css = (""" + """) + css_node_with_bom = CssCompressor(css) + hunks = '\n'.join([h for h in css_node_with_bom.hunks()]) + self.assertEqual(out, hunks) + def test_css_mtimes(self): is_date = re.compile(r'^\d{10}[\.\d]+$') for date in self.css_node.mtimes: From 39047bf85bcf2d64731db02453a6d8937bef5294 Mon Sep 17 00:00:00 2001 From: sdfsdhgjkbmnmxc Date: Sun, 14 Dec 2014 15:38:14 +0300 Subject: [PATCH 25/58] Django 1.8 warnings fix --- compressor/cache.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/compressor/cache.py b/compressor/cache.py index 4847939..a48bfef 100644 --- a/compressor/cache.py +++ b/compressor/cache.py @@ -4,11 +4,21 @@ import os import socket import time -from django.core.cache import get_cache +try: + from django.core.cache import caches + def get_cache(name): + return caches[name] +except ImportError: + from django.core.cache import get_cache + from django.core.files.base import ContentFile from django.utils.encoding import force_text, smart_bytes from django.utils.functional import SimpleLazyObject -from django.utils.importlib import import_module + +try: + from importlib import import_module +except: + from django.utils.importlib import import_module from compressor.conf import settings from compressor.storage import default_storage From d27a8e909f79ae8d731c9149d01beebc832ec178 Mon Sep 17 00:00:00 2001 From: sdfsdhgjkbmnmxc Date: Sun, 14 Dec 2014 15:45:43 +0300 Subject: [PATCH 26/58] Update base.py --- compressor/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compressor/base.py b/compressor/base.py index ae3a2c4..528b602 100644 --- a/compressor/base.py +++ b/compressor/base.py @@ -5,7 +5,10 @@ import codecs from django.core.files.base import ContentFile from django.template import Context from django.template.loader import render_to_string -from django.utils.importlib import import_module +try: + from importlib import import_module +except: + from django.utils.importlib import import_module from django.utils.safestring import mark_safe try: From 39ac22ffbfdb0a2fdd890c89c0fd2ff2ff2476b4 Mon Sep 17 00:00:00 2001 From: sdfsdhgjkbmnmxc Date: Sun, 14 Dec 2014 15:51:55 +0300 Subject: [PATCH 27/58] django.utils.importlib will be removed in Django 1.9 --- compressor/filters/base.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/compressor/filters/base.py b/compressor/filters/base.py index fcc56ab..a25e769 100644 --- a/compressor/filters/base.py +++ b/compressor/filters/base.py @@ -10,7 +10,12 @@ except ImportError: from django.core.exceptions import ImproperlyConfigured from django.core.files.temp import NamedTemporaryFile -from django.utils.importlib import import_module + +try: + from importlib import import_module +except ImportError: + from django.utils.importlib import import_module + from django.utils.encoding import smart_text from django.utils import six From 03915ffc40de1da9915d6a14bd4ab3ba80533f8c Mon Sep 17 00:00:00 2001 From: sdfsdhgjkbmnmxc Date: Sun, 14 Dec 2014 15:57:12 +0300 Subject: [PATCH 28/58] Update __init__.py --- compressor/parser/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compressor/parser/__init__.py b/compressor/parser/__init__.py index a3fe78f..e935658 100644 --- a/compressor/parser/__init__.py +++ b/compressor/parser/__init__.py @@ -1,6 +1,9 @@ from django.utils import six from django.utils.functional import LazyObject -from django.utils.importlib import import_module +try: + from importlib import import_module +except ImportError: + from django.utils.importlib import import_module # support legacy parser module usage from compressor.parser.base import ParserBase # noqa From a8f23cdf3c47c0058deaefb5b0cd30080ca43cda Mon Sep 17 00:00:00 2001 From: John Franey Date: Tue, 16 Dec 2014 13:43:37 -0500 Subject: [PATCH 29/58] Update settings.txt Fixed broken link to CoffeeScript homepage --- docs/settings.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/settings.txt b/docs/settings.txt index 303e0fc..268f645 100644 --- a/docs/settings.txt +++ b/docs/settings.txt @@ -321,7 +321,7 @@ Backend settings .. _less: http://lesscss.org/ - .. _CoffeeScript: http://jashkenas.github.com/coffee-script/ + .. _CoffeeScript: http://coffeescript.org/ .. attribute:: COMPRESS_STORAGE From 36bcc887d45a96108cc166a9fcdbbf0fc0c9cce2 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 30 Dec 2014 13:27:13 +0100 Subject: [PATCH 30/58] Fixed typo in last merge. --- compressor/js.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compressor/js.py b/compressor/js.py index b1234a7..f60cc7e 100644 --- a/compressor/js.py +++ b/compressor/js.py @@ -44,7 +44,7 @@ class JsCompressor(Compressor): if (settings.COMPRESS_ENABLED or settings.COMPRESS_PRECOMPILERS or kwargs.get('forced', False)): self.split_contents() - if hasattr(self, 'nodes'): + if hasattr(self, 'extra_nodes'): ret = [] for extra, subnode in self.extra_nodes: subnode.extra_context.update({'extra': extra}) From dec32b1e7938d4a935dae99e770657363f9758b4 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 30 Dec 2014 13:45:04 +0100 Subject: [PATCH 31/58] Simplified tox config with generative config. --- .travis.yml | 24 ++++---- compressor/test_settings.py | 2 + tox.ini | 119 ++++++------------------------------ 3 files changed, 33 insertions(+), 112 deletions(-) diff --git a/.travis.yml b/.travis.yml index 307b0c8..410eaef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,20 +7,20 @@ install: script: - tox env: - - TOXENV=py34-1.7.X - - TOXENV=py33-1.7.X - - TOXENV=py32-1.7.X - - TOXENV=py27-1.7.X - - TOXENV=py33-1.6.X - - TOXENV=py32-1.6.X - - TOXENV=py27-1.6.X - - TOXENV=py26-1.6.X - - TOXENV=py33-1.5.X - - TOXENV=py32-1.5.X - - TOXENV=py27-1.5.X + - TOXENV=py25-1.4.X + - TOXENV=py25-1.5.X + - TOXENV=py26-1.4.X - TOXENV=py26-1.5.X - TOXENV=py27-1.4.X - - TOXENV=py26-1.4.X + - TOXENV=py27-1.5.X + - TOXENV=py26-1.6.X + - TOXENV=py27-1.6.X + - TOXENV=py32-1.6.X + - TOXENV=py33-1.6.X + - TOXENV=py27-1.7.X + - TOXENV=py32-1.7.X + - TOXENV=py33-1.7.X + - TOXENV=py34-1.7.X notifications: irc: "irc.freenode.org#django-compressor" after_success: coveralls diff --git a/compressor/test_settings.py b/compressor/test_settings.py index 5531bcc..f5fa64d 100644 --- a/compressor/test_settings.py +++ b/compressor/test_settings.py @@ -38,3 +38,5 @@ SECRET_KEY = "iufoj=mibkpdz*%bob952x(%49rqgv8gg45k36kjcg76&-y5=!" PASSWORD_HASHERS = ( 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher', ) + +MIDDLEWARE_CLASSES = [] diff --git a/tox.ini b/tox.ini index 32eeab2..9c33224 100644 --- a/tox.ini +++ b/tox.ini @@ -33,113 +33,32 @@ three_two = [tox] envlist = - py33-1.6.X, - py32-1.6.X, - py27-1.6.X, - py26-1.6.X, - py33-1.5.X, - py32-1.5.X, - py27-1.5.X, - py26-1.5.X, - py27-1.4.X, - py26-1.4.X + {py25,py26,py27}-{1.4.X,1.5.X}, + {py26,py27,py32,py33}-{1.6.X}, + {py27,py32,py33,py34}-{1.7.X} [testenv] +basepython = + py26: python2.6 + py27: python2.7 + py32: python3.2 + py33: python3.3 + py34: python3.4 +usedevelop = true setenv = CPPFLAGS=-O0 -usedevelop = true whitelist_externals = /usr/bin/make downloadcache = {toxworkdir}/_download/ commands = django-admin.py --version make test - -[testenv:py34-1.7.X] -basepython = python3.4 deps = - Django>=1.7,<1.8 - {[deps]three} - -[testenv:py33-1.7.X] -basepython = python3.3 -deps = - Django>=1.7,<1.8 - {[deps]three} - -[testenv:py32-1.7.X] -basepython = python3.2 -deps = - Django>=1.7,<1.8 - {[deps]three_two} - -[testenv:py27-1.7.X] -basepython = python2.7 -deps = - Django>=1.7,<1.8 - {[deps]two} - -[testenv:py33-1.6.X] -basepython = python3.3 -deps = - Django>=1.6,<1.7 - {[deps]three} - -[testenv:py32-1.6.X] -basepython = python3.2 -deps = - Django>=1.6,<1.7 - {[deps]three_two} - -[testenv:py27-1.6.X] -basepython = python2.7 -deps = - Django>=1.6,<1.7 - {[deps]two} - -[testenv:py26-1.6.X] -basepython = python2.6 -deps = - Django>=1.6,<1.7 - {[deps]two} - -[testenv:py33-1.5.X] -basepython = python3.3 -deps = - Django>=1.5,<1.6 - django-discover-runner - {[deps]three} - -[testenv:py32-1.5.X] -basepython = python3.2 -deps = - Django>=1.5,<1.6 - django-discover-runner - {[deps]three_two} - -[testenv:py27-1.5.X] -basepython = python2.7 -deps = - Django>=1.5,<1.6 - django-discover-runner - {[deps]two} - -[testenv:py26-1.5.X] -basepython = python2.6 -deps = - Django>=1.5,<1.6 - django-discover-runner - {[deps]two} - -[testenv:py27-1.4.X] -basepython = python2.7 -deps = - Django>=1.4,<1.5 - django-discover-runner - {[deps]two} - -[testenv:py26-1.4.X] -basepython = python2.6 -deps = - Django>=1.4,<1.5 - django-discover-runner - {[deps]two} + 1.4.X: Django>=1.4,<1.5, django-discover-runner + 1.5.X: Django>=1.5,<1.6, django-discover-runner + 1.6.X: Django>=1.6,<1.7 + 1.7.X: Django>=1.7,<1.8 + py26: {[deps]two} + py27: {[deps]two} + py32: {[deps]three_two} + py33: {[deps]three} + py34: {[deps]three} From 063204d783806e056cd87390f72f75bc62c2a47c Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 30 Dec 2014 13:52:22 +0100 Subject: [PATCH 32/58] Fixed tox.ini. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 9c33224..b62ee86 100644 --- a/tox.ini +++ b/tox.ini @@ -39,6 +39,7 @@ envlist = [testenv] basepython = + py25: python2.5 py26: python2.6 py27: python2.7 py32: python3.2 From 8fc897fac51fda8cf26580bfc97c9186bbd9242d Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 30 Dec 2014 13:56:35 +0100 Subject: [PATCH 33/58] Ah, removed Python 2.5 because it's not supported on Travis anymore. --- .travis.yml | 2 -- tox.ini | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 410eaef..c3cecbd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,8 +7,6 @@ install: script: - tox env: - - TOXENV=py25-1.4.X - - TOXENV=py25-1.5.X - TOXENV=py26-1.4.X - TOXENV=py26-1.5.X - TOXENV=py27-1.4.X diff --git a/tox.ini b/tox.ini index b62ee86..29f130c 100644 --- a/tox.ini +++ b/tox.ini @@ -33,13 +33,12 @@ three_two = [tox] envlist = - {py25,py26,py27}-{1.4.X,1.5.X}, + {py26,py27}-{1.4.X,1.5.X}, {py26,py27,py32,py33}-{1.6.X}, {py27,py32,py33,py34}-{1.7.X} [testenv] basepython = - py25: python2.5 py26: python2.6 py27: python2.7 py32: python3.2 From 4c3ae4cdfc3bd5026da45c3233d59a4ad608aed3 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Tue, 30 Dec 2014 13:58:30 +0100 Subject: [PATCH 34/58] Fixed tox syntax. --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 29f130c..b7c1a14 100644 --- a/tox.ini +++ b/tox.ini @@ -53,8 +53,8 @@ commands = django-admin.py --version make test deps = - 1.4.X: Django>=1.4,<1.5, django-discover-runner - 1.5.X: Django>=1.5,<1.6, django-discover-runner + 1.4.X: Django>=1.4,<1.5 + 1.5.X: Django>=1.5,<1.6 1.6.X: Django>=1.6,<1.7 1.7.X: Django>=1.7,<1.8 py26: {[deps]two} @@ -62,3 +62,4 @@ deps = py32: {[deps]three_two} py33: {[deps]three} py34: {[deps]three} + django-discover-runner \ No newline at end of file From cc6c83ae5d527cec8efc51b72b0ada96b85710cc Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 9 Mar 2015 10:57:16 +0100 Subject: [PATCH 35/58] Fix whitespace error --- compressor/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compressor/cache.py b/compressor/cache.py index a48bfef..94a64fd 100644 --- a/compressor/cache.py +++ b/compressor/cache.py @@ -10,7 +10,7 @@ try: return caches[name] except ImportError: from django.core.cache import get_cache - + from django.core.files.base import ContentFile from django.utils.encoding import force_text, smart_bytes from django.utils.functional import SimpleLazyObject From 530c6ebebf14c1cbe6aaaf0716ea94dd785580d9 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Mon, 9 Mar 2015 10:57:34 +0100 Subject: [PATCH 36/58] Update cache config for 1.7 --- compressor/test_settings.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/compressor/test_settings.py b/compressor/test_settings.py index f5fa64d..1d3b476 100644 --- a/compressor/test_settings.py +++ b/compressor/test_settings.py @@ -3,7 +3,13 @@ import django TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'tests') -COMPRESS_CACHE_BACKEND = 'django.core.cache.backends.locmem.LocMemCache' + +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', + 'LOCATION': 'unique-snowflake' + } +} DATABASES = { 'default': { From 7706baea15be12f36b471040ca0165f11f143f1d Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 10 Mar 2015 20:01:24 +0100 Subject: [PATCH 37/58] Remove `jingo` for 1.8. Tests don't even run without this: File "django-compressor/.tox/py27-1.8.X/lib/python2.7/site-packages/jingo/__init__.py", line 12, in from django.template.context import get_standard_processors ImportError: cannot import name get_standard_processors --- compressor/test_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/compressor/test_settings.py b/compressor/test_settings.py index 1d3b476..6a72e7f 100644 --- a/compressor/test_settings.py +++ b/compressor/test_settings.py @@ -21,8 +21,9 @@ DATABASES = { INSTALLED_APPS = [ 'compressor', 'coffin', - 'jingo', ] +if django.VERSION < (1, 8): + INSTALLED_APPS.append('jingo') STATIC_URL = '/static/' From 72b33ddbcdc41eb5af42b86329d81d96894a9d10 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 10 Mar 2015 20:04:07 +0100 Subject: [PATCH 38/58] Make `compress` command (at least) run through on 1.8 FAILED (failures=15, errors=15, skipped=1) AssertionError: 2 != 12 OfflineGenerationError: ... key "XYZ" missing ... --- compressor/management/commands/compress.py | 41 +++++++++++++--------- compressor/offline/django.py | 27 +++++++++----- 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/compressor/management/commands/compress.py b/compressor/management/commands/compress.py index 6be215e..0bc436a 100644 --- a/compressor/management/commands/compress.py +++ b/compressor/management/commands/compress.py @@ -5,6 +5,7 @@ import sys from fnmatch import fnmatch from optparse import make_option +import django from django.core.management.base import NoArgsCommand, CommandError import django.template from django.template import Context @@ -53,24 +54,30 @@ class Command(NoArgsCommand): requires_model_validation = False def get_loaders(self): - from django.template.loader import template_source_loaders - if template_source_loaders is None: - try: - from django.template.loader import ( - find_template as finder_func) - except ImportError: - from django.template.loader import ( - find_template_source as finder_func) # noqa - try: - # Force django to calculate template_source_loaders from - # TEMPLATE_LOADERS settings, by asking to find a dummy template - source, name = finder_func('test') - except django.template.TemplateDoesNotExist: - pass - # Reload template_source_loaders now that it has been calculated ; - # it should contain the list of valid, instanciated template loaders - # to use. + if django.VERSION < (1, 8): from django.template.loader import template_source_loaders + if template_source_loaders is None: + try: + from django.template.loader import ( + find_template as finder_func) + except ImportError: + from django.template.loader import ( + find_template_source as finder_func) # noqa + try: + # Force django to calculate template_source_loaders from + # TEMPLATE_LOADERS settings, by asking to find a dummy template + source, name = finder_func('test') + except django.template.TemplateDoesNotExist: + pass + # Reload template_source_loaders now that it has been calculated ; + # it should contain the list of valid, instanciated template loaders + # to use. + from django.template.loader import template_source_loaders + else: + from django.template import engines + template_source_loaders = [] + for e in engines.all(): + template_source_loaders.extend(e.engine.get_template_loaders(e.engine.loaders)) loaders = [] # If template loader is CachedTemplateLoader, return the loaders # that it wraps around. So if we have diff --git a/compressor/offline/django.py b/compressor/offline/django.py index b326093..107c6e4 100644 --- a/compressor/offline/django.py +++ b/compressor/offline/django.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from copy import copy +import django from django import template from django.conf import settings from django.template import Context @@ -14,7 +15,7 @@ from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist from compressor.templatetags.compress import CompressorNode -def handle_extendsnode(extendsnode, block_context=None): +def handle_extendsnode(extendsnode, block_context=None, original=None): """Create a copy of Node tree of a derived template replacing all blocks tags with the nodes of appropriate blocks. Also handles {{ block.super }} tags. @@ -26,6 +27,9 @@ def handle_extendsnode(extendsnode, block_context=None): block_context.add_blocks(blocks) context = Context(settings.COMPRESS_OFFLINE_CONTEXT) + if original is not None: + context.template = original + compiled_parent = extendsnode.get_parent(context) parent_nodelist = compiled_parent.nodelist # If the parent template has an ExtendsNode it is not the root. @@ -33,7 +37,7 @@ def handle_extendsnode(extendsnode, block_context=None): # The ExtendsNode has to be the first non-text node. if not isinstance(node, TextNode): if isinstance(node, ExtendsNode): - return handle_extendsnode(node, block_context) + return handle_extendsnode(node, block_context, original) break # Add blocks of the root template to block context. blocks = dict((n.name, n) for n in @@ -95,7 +99,10 @@ class DjangoParser(object): def parse(self, template_name): try: - return get_template(template_name) + if django.VERSION < (1, 8): + return get_template(template_name) + else: + return get_template(template_name).template except template.TemplateSyntaxError as e: raise TemplateSyntaxError(str(e)) except template.TemplateDoesNotExist as e: @@ -111,15 +118,17 @@ class DjangoParser(object): pass def render_nodelist(self, template, context, node): + if django.VERSION >= (1, 8): + context.template = template return node.nodelist.render(context) def render_node(self, template, context, node): return node.render(context, forced=True) - def get_nodelist(self, node): + def get_nodelist(self, node, original=None): if isinstance(node, ExtendsNode): try: - return handle_extendsnode(node) + return handle_extendsnode(node, block_context=None, original=original) except template.TemplateSyntaxError as e: raise TemplateSyntaxError(str(e)) except template.TemplateDoesNotExist as e: @@ -134,10 +143,12 @@ class DjangoParser(object): nodelist = getattr(node, 'nodelist', []) return nodelist - def walk_nodes(self, node): - for node in self.get_nodelist(node): + def walk_nodes(self, node, original=None): + if django.VERSION >= (1, 8) and original is None: + original = node + for node in self.get_nodelist(node, original): if isinstance(node, CompressorNode) and node.is_offline_compression_enabled(forced=True): yield node else: - for node in self.walk_nodes(node): + for node in self.walk_nodes(node, original): yield node From 51b6c3556e616851b008fa9de3e1bb7cb575e1c7 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 11 Mar 2015 08:20:50 +0100 Subject: [PATCH 39/58] Skip various tests on 1.8 Removes the errors: FAILED (failures=15, skipped=4) --- compressor/tests/test_offline.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/compressor/tests/test_offline.py b/compressor/tests/test_offline.py index 327b901..c283bf9 100644 --- a/compressor/tests/test_offline.py +++ b/compressor/tests/test_offline.py @@ -3,6 +3,7 @@ import io import os import sys +import django from django.core.management.base import CommandError from django.template import Template, Context from django.test import TestCase @@ -29,6 +30,8 @@ else: # compressor_nodes.setdefault(template, []).extend(nodes) # causes the error "unhashable type: 'Template'" _TEST_JINJA2 = not(sys.version_info[0] == 3 and sys.version_info[1] == 2) +if django.VERSION >= (1, 8): + _TEST_JINJA2 = False class OfflineTestCaseMixin(object): @@ -454,6 +457,8 @@ class OfflineGenerationComplexTestCase(OfflineTestCaseMixin, TestCase): # It seems there is no evidence nor indicated support for Python 3+. @unittest.skipIf(sys.version_info >= (3, 2), "Coffin does not support 3.2+") +@unittest.skipIf(django.VERSION >= (1, 8), + "Import error on 1.8") class OfflineGenerationCoffinTestCase(OfflineTestCaseMixin, TestCase): templates_dir = "test_coffin" expected_hash = "32c8281e3346" @@ -478,6 +483,8 @@ class OfflineGenerationCoffinTestCase(OfflineTestCaseMixin, TestCase): # is also evident in its tox.ini file. @unittest.skipIf(sys.version_info >= (3, 2) and sys.version_info < (3, 3), "Jingo does not support 3.2") +@unittest.skipIf(django.VERSION >= (1, 8), + "Import error on 1.8") class OfflineGenerationJingoTestCase(OfflineTestCaseMixin, TestCase): templates_dir = "test_jingo" expected_hash = "61ec584468eb" From 7c887546dec4990882b33c73d1c0cba2e406c9e9 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 11 Mar 2015 14:14:16 +0100 Subject: [PATCH 40/58] Remove coveralls for now It's a bit annoying for the moment. --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c3cecbd..8795346 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ before_install: - sudo apt-get update - sudo apt-get install csstidy libxml2-dev libxslt-dev install: - - pip install tox coveralls + - pip install tox script: - tox env: @@ -21,4 +21,3 @@ env: - TOXENV=py34-1.7.X notifications: irc: "irc.freenode.org#django-compressor" -after_success: coveralls From 4a301bef5b9071b4455a0026b31c7242c89c80d1 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 11 Mar 2015 15:51:41 +0100 Subject: [PATCH 41/58] Reset EngineHandler() between tests. Better: FAILED (failures=1, errors=4, skipped=4) --- compressor/tests/test_offline.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/compressor/tests/test_offline.py b/compressor/tests/test_offline.py index c283bf9..159fdf5 100644 --- a/compressor/tests/test_offline.py +++ b/compressor/tests/test_offline.py @@ -5,7 +5,7 @@ import sys import django from django.core.management.base import CommandError -from django.template import Template, Context +from django.template import Template, Context, EngineHandler from django.test import TestCase from django.utils import six, unittest @@ -47,6 +47,8 @@ class OfflineTestCaseMixin(object): engines = ("django",) def setUp(self): + if django.VERSION >= (1, 8): + django.template.engines = EngineHandler() self._old_compress = settings.COMPRESS_ENABLED self._old_compress_offline = settings.COMPRESS_OFFLINE self._old_template_dirs = settings.TEMPLATE_DIRS From ca855dfe13f36430fa938d08186c6de9da2bfec9 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 11 Mar 2015 16:26:58 +0100 Subject: [PATCH 42/58] Fix import for Django <1.8 --- compressor/tests/test_offline.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/compressor/tests/test_offline.py b/compressor/tests/test_offline.py index 159fdf5..c3e4708 100644 --- a/compressor/tests/test_offline.py +++ b/compressor/tests/test_offline.py @@ -5,7 +5,11 @@ import sys import django from django.core.management.base import CommandError -from django.template import Template, Context, EngineHandler +from django.template import Template, Context +try: + from django.template import EngineHandler +except ImportError: + pass from django.test import TestCase from django.utils import six, unittest From 01ace9b8a4326fa365e6caeccaa39922e2ca36be Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 12 Mar 2015 09:16:07 +0100 Subject: [PATCH 43/58] Use `override_setting`... ... rather than doing it by hand --- compressor/tests/test_offline.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/compressor/tests/test_offline.py b/compressor/tests/test_offline.py index c3e4708..d600bcd 100644 --- a/compressor/tests/test_offline.py +++ b/compressor/tests/test_offline.py @@ -6,10 +6,6 @@ import sys import django from django.core.management.base import CommandError from django.template import Template, Context -try: - from django.template import EngineHandler -except ImportError: - pass from django.test import TestCase from django.utils import six, unittest @@ -51,8 +47,6 @@ class OfflineTestCaseMixin(object): engines = ("django",) def setUp(self): - if django.VERSION >= (1, 8): - django.template.engines = EngineHandler() self._old_compress = settings.COMPRESS_ENABLED self._old_compress_offline = settings.COMPRESS_OFFLINE self._old_template_dirs = settings.TEMPLATE_DIRS @@ -67,7 +61,12 @@ class OfflineTestCaseMixin(object): # template to be skipped over. django_template_dir = os.path.join(settings.TEST_DIR, 'test_templates', self.templates_dir) jinja2_template_dir = os.path.join(settings.TEST_DIR, 'test_templates_jinja2', self.templates_dir) - settings.TEMPLATE_DIRS = (django_template_dir, jinja2_template_dir) + + if django.VERSION >= (1, 8): + self.override_template_dirs = self.settings(TEMPLATE_DIRS=(django_template_dir, jinja2_template_dir)) + self.override_template_dirs.__enter__() + else: + settings.TEMPLATE_DIRS = (django_template_dir, jinja2_template_dir) # Enable offline compress settings.COMPRESS_ENABLED = True @@ -91,10 +90,14 @@ class OfflineTestCaseMixin(object): self.template_jinja2 = jinja2_env.from_string(file.read()) def tearDown(self): + if django.VERSION >= (1, 8): + self.override_template_dirs.__exit__(None, None, None) + else: + settings.TEMPLATE_DIRS = self._old_template_dirs + settings.COMPRESS_JINJA2_GET_ENVIRONMENT = self._old_jinja2_get_environment settings.COMPRESS_ENABLED = self._old_compress settings.COMPRESS_OFFLINE = self._old_compress_offline - settings.TEMPLATE_DIRS = self._old_template_dirs manifest_path = os.path.join('CACHE', 'manifest.json') if default_storage.exists(manifest_path): default_storage.delete(manifest_path) From 873de689292153c77b48bbc8a59a5462801b4e64 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 12 Mar 2015 10:02:12 +0100 Subject: [PATCH 44/58] Add 1.8b2 to tox --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index b7c1a14..280d4cb 100644 --- a/tox.ini +++ b/tox.ini @@ -35,8 +35,8 @@ three_two = envlist = {py26,py27}-{1.4.X,1.5.X}, {py26,py27,py32,py33}-{1.6.X}, - {py27,py32,py33,py34}-{1.7.X} - + {py27,py32,py33,py34}-{1.7.X}, + {py27,py32,py33,py34}-{1.8.X} [testenv] basepython = py26: python2.6 @@ -57,6 +57,7 @@ deps = 1.5.X: Django>=1.5,<1.6 1.6.X: Django>=1.6,<1.7 1.7.X: Django>=1.7,<1.8 + 1.8.X: Django==1.8b2 py26: {[deps]two} py27: {[deps]two} py32: {[deps]three_two} From 061d5f5580be459937b87c5b66989150a7aba6f6 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 12 Mar 2015 10:03:07 +0100 Subject: [PATCH 45/58] Add the 1.8 envs to travis --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 8795346..b350b18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,5 +19,9 @@ env: - TOXENV=py32-1.7.X - TOXENV=py33-1.7.X - TOXENV=py34-1.7.X + - TOXENV=py27-1.8.X + - TOXENV=py32-1.8.X + - TOXENV=py33-1.8.X + - TOXENV=py34-1.8.X notifications: irc: "irc.freenode.org#django-compressor" From d3cd6d33546a9ceb3b4a84e8e0f7c44be6c70c0b Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 12 Mar 2015 10:51:19 +0100 Subject: [PATCH 46/58] Re-enable Jinja2 testing on 1.8 No reason why this is excluded. --- compressor/tests/test_offline.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/compressor/tests/test_offline.py b/compressor/tests/test_offline.py index d600bcd..ee386b8 100644 --- a/compressor/tests/test_offline.py +++ b/compressor/tests/test_offline.py @@ -30,8 +30,6 @@ else: # compressor_nodes.setdefault(template, []).extend(nodes) # causes the error "unhashable type: 'Template'" _TEST_JINJA2 = not(sys.version_info[0] == 3 and sys.version_info[1] == 2) -if django.VERSION >= (1, 8): - _TEST_JINJA2 = False class OfflineTestCaseMixin(object): From baa7a0e395a0e2a7d9703fa4ddccb0d310115558 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 18 Mar 2015 10:49:27 +0100 Subject: [PATCH 47/58] Pull all settings overrides into `TestCase.settings` --- compressor/tests/test_offline.py | 36 +++++++++++--------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/compressor/tests/test_offline.py b/compressor/tests/test_offline.py index ee386b8..7ce72ef 100644 --- a/compressor/tests/test_offline.py +++ b/compressor/tests/test_offline.py @@ -45,10 +45,6 @@ class OfflineTestCaseMixin(object): engines = ("django",) def setUp(self): - self._old_compress = settings.COMPRESS_ENABLED - self._old_compress_offline = settings.COMPRESS_OFFLINE - self._old_template_dirs = settings.TEMPLATE_DIRS - self._old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT self.log = StringIO() # Reset template dirs, because it enables us to force compress to @@ -60,15 +56,17 @@ class OfflineTestCaseMixin(object): django_template_dir = os.path.join(settings.TEST_DIR, 'test_templates', self.templates_dir) jinja2_template_dir = os.path.join(settings.TEST_DIR, 'test_templates_jinja2', self.templates_dir) - if django.VERSION >= (1, 8): - self.override_template_dirs = self.settings(TEMPLATE_DIRS=(django_template_dir, jinja2_template_dir)) - self.override_template_dirs.__enter__() - else: - settings.TEMPLATE_DIRS = (django_template_dir, jinja2_template_dir) + override_settings = { + 'TEMPLATE_DIRS': (django_template_dir, jinja2_template_dir,), + 'COMPRESS_ENABLED': True, + 'COMPRESS_OFFLINE': True + } - # Enable offline compress - settings.COMPRESS_ENABLED = True - settings.COMPRESS_OFFLINE = True + if "jinja2" in self.engines: + override_settings["COMPRESS_JINJA2_GET_ENVIRONMENT"] = lambda: self._get_jinja2_env() + + self.override_template_dirs = self.settings(**override_settings) + self.override_template_dirs.__enter__() if "django" in self.engines: self.template_path = os.path.join(django_template_dir, self.template_name) @@ -76,26 +74,16 @@ class OfflineTestCaseMixin(object): with io.open(self.template_path, encoding=settings.FILE_CHARSET) as file: self.template = Template(file.read()) - self._old_jinja2_get_environment = settings.COMPRESS_JINJA2_GET_ENVIRONMENT - if "jinja2" in self.engines: - # Setup Jinja2 settings. - settings.COMPRESS_JINJA2_GET_ENVIRONMENT = lambda: self._get_jinja2_env() - jinja2_env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT() + jinja2_env = override_settings["COMPRESS_JINJA2_GET_ENVIRONMENT"]() self.template_path_jinja2 = os.path.join(jinja2_template_dir, self.template_name) with io.open(self.template_path_jinja2, encoding=settings.FILE_CHARSET) as file: self.template_jinja2 = jinja2_env.from_string(file.read()) def tearDown(self): - if django.VERSION >= (1, 8): - self.override_template_dirs.__exit__(None, None, None) - else: - settings.TEMPLATE_DIRS = self._old_template_dirs + self.override_template_dirs.__exit__(None, None, None) - settings.COMPRESS_JINJA2_GET_ENVIRONMENT = self._old_jinja2_get_environment - settings.COMPRESS_ENABLED = self._old_compress - settings.COMPRESS_OFFLINE = self._old_compress_offline manifest_path = os.path.join('CACHE', 'manifest.json') if default_storage.exists(manifest_path): default_storage.delete(manifest_path) From 9a1738f05e79cced20f1a6a847cdd5b392d031d7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 15 Oct 2014 20:03:14 +0200 Subject: [PATCH 48/58] Handle TypeError from import_module Fixes #569 --- compressor/base.py | 2 +- compressor/cache.py | 2 +- compressor/filters/base.py | 2 +- compressor/management/commands/compress.py | 2 +- compressor/parser/__init__.py | 2 +- compressor/tests/test_base.py | 31 ++++++++++++++++++++-- 6 files changed, 34 insertions(+), 7 deletions(-) diff --git a/compressor/base.py b/compressor/base.py index 528b602..54b16f3 100644 --- a/compressor/base.py +++ b/compressor/base.py @@ -253,7 +253,7 @@ class Compressor(object): mod_name, cls_name = get_mod_func(filter_or_command) try: mod = import_module(mod_name) - except ImportError: + except (ImportError, TypeError): filter = CompilerFilter( content, filter_type=self.type, filename=filename, charset=charset, command=filter_or_command) diff --git a/compressor/cache.py b/compressor/cache.py index 94a64fd..f80bf54 100644 --- a/compressor/cache.py +++ b/compressor/cache.py @@ -49,7 +49,7 @@ def get_cachekey(*args, **kwargs): mod_name, func_name = get_mod_func( settings.COMPRESS_CACHE_KEY_FUNCTION) _cachekey_func = getattr(import_module(mod_name), func_name) - except (AttributeError, ImportError) as e: + except (AttributeError, ImportError, TypeError) as e: raise ImportError("Couldn't import cache key function %s: %s" % (settings.COMPRESS_CACHE_KEY_FUNCTION, e)) return _cachekey_func(*args, **kwargs) diff --git a/compressor/filters/base.py b/compressor/filters/base.py index c101347..ee14b82 100644 --- a/compressor/filters/base.py +++ b/compressor/filters/base.py @@ -83,7 +83,7 @@ class CallbackOutputFilter(FilterBase): try: mod_name, func_name = get_mod_func(self.callback) func = getattr(import_module(mod_name), func_name) - except ImportError: + except (ImportError, TypeError): if self.dependencies: if len(self.dependencies) == 1: warning = "dependency (%s) is" % self.dependencies[0] diff --git a/compressor/management/commands/compress.py b/compressor/management/commands/compress.py index 6be215e..f60994f 100644 --- a/compressor/management/commands/compress.py +++ b/compressor/management/commands/compress.py @@ -130,7 +130,7 @@ class Command(NoArgsCommand): if get_template_sources is None: get_template_sources = loader.get_template_sources paths.update(list(get_template_sources(''))) - except (ImportError, AttributeError): + except (ImportError, AttributeError, TypeError): # Yeah, this didn't work out so well, let's move on pass if not paths: diff --git a/compressor/parser/__init__.py b/compressor/parser/__init__.py index e935658..19beb01 100644 --- a/compressor/parser/__init__.py +++ b/compressor/parser/__init__.py @@ -33,5 +33,5 @@ class AutoSelectParser(LazyObject): import_module(dependency) self._wrapped = parser(content) break - except ImportError: + except (ImportError, TypeError): continue diff --git a/compressor/tests/test_base.py b/compressor/tests/test_base.py index 67dab7c..e8255db 100644 --- a/compressor/tests/test_base.py +++ b/compressor/tests/test_base.py @@ -12,11 +12,13 @@ from django.core.cache.backends import locmem from django.test import SimpleTestCase from django.test.utils import override_settings -from compressor.base import SOURCE_HUNK, SOURCE_FILE +from compressor import cache as cachemod +from compressor.base import SOURCE_FILE, SOURCE_HUNK +from compressor.cache import get_cachekey from compressor.conf import settings from compressor.css import CssCompressor +from compressor.exceptions import FilterDoesNotExist, FilterError from compressor.js import JsCompressor -from compressor.exceptions import FilterDoesNotExist def make_soup(markup): @@ -216,6 +218,14 @@ class CompressorTestCase(SimpleTestCase): css_node = CssCompressor(css) self.assertRaises(FilterDoesNotExist, css_node.output, 'inline') + @override_settings(COMPRESS_PRECOMPILERS=( + ('text/foobar', './foo -I ./bar/baz'), + ), COMPRESS_ENABLED=True) + def test_command_with_dot_precompiler(self): + css = '' + css_node = CssCompressor(css) + self.assertRaises(FilterError, css_node.output, 'inline') + class CssMediaTestCase(SimpleTestCase): def setUp(self): @@ -304,3 +314,20 @@ class JsAsyncDeferTestCase(SimpleTestCase): scripts = make_soup(js_node.output()).findAll('script') attrs = [s.get('async') or s.get('defer') for s in scripts] self.assertEqual(output, attrs) + + +class CacheTestCase(SimpleTestCase): + + def setUp(self): + cachemod._cachekey_func = None + + def test_get_cachekey_basic(self): + self.assertEqual(get_cachekey("foo"), "django_compressor.foo") + + @override_settings(COMPRESS_CACHE_KEY_FUNCTION='.leading.dot') + def test_get_cachekey_leading_dot(self): + self.assertRaises(ImportError, lambda: get_cachekey("foo")) + + @override_settings(COMPRESS_CACHE_KEY_FUNCTION='invalid.module') + def test_get_cachekey_invalid_mod(self): + self.assertRaises(ImportError, lambda: get_cachekey("foo")) From a46acf037ab917b54bbec4127e3a244bd926429e Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 19 Mar 2015 08:52:23 +0100 Subject: [PATCH 49/58] Rename test settings attribute (since we're no longer just setting TEMPLATE_DIRS) --- compressor/tests/test_offline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compressor/tests/test_offline.py b/compressor/tests/test_offline.py index 7ce72ef..59a3d62 100644 --- a/compressor/tests/test_offline.py +++ b/compressor/tests/test_offline.py @@ -65,8 +65,8 @@ class OfflineTestCaseMixin(object): if "jinja2" in self.engines: override_settings["COMPRESS_JINJA2_GET_ENVIRONMENT"] = lambda: self._get_jinja2_env() - self.override_template_dirs = self.settings(**override_settings) - self.override_template_dirs.__enter__() + self.override_settings = self.settings(**override_settings) + self.override_settings.__enter__() if "django" in self.engines: self.template_path = os.path.join(django_template_dir, self.template_name) @@ -82,7 +82,7 @@ class OfflineTestCaseMixin(object): self.template_jinja2 = jinja2_env.from_string(file.read()) def tearDown(self): - self.override_template_dirs.__exit__(None, None, None) + self.override_settings.__exit__(None, None, None) manifest_path = os.path.join('CACHE', 'manifest.json') if default_storage.exists(manifest_path): From 125f2b51f2af3adea6f7794dc915c7b58b50c6b8 Mon Sep 17 00:00:00 2001 From: Markus Amalthea Magnuson Date: Thu, 19 Mar 2015 14:05:55 +0100 Subject: [PATCH 50/58] Fix typo "lamda" -> "lambda" --- docs/jinja2.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/jinja2.txt b/docs/jinja2.txt index b585047..b0e3a96 100644 --- a/docs/jinja2.txt +++ b/docs/jinja2.txt @@ -42,7 +42,7 @@ Jinja2 Offline Compression Support ================================== You'd need to configure ``COMPRESS_JINJA2_GET_ENVIRONMENT`` so that Compressor can retrieve the Jinja2 environment for rendering. -This can be a lamda or function that returns a Jinja2 environment. +This can be a lambda or function that returns a Jinja2 environment. Usage ----- From 4f834459858a6345930b0cae51608842b186d81b Mon Sep 17 00:00:00 2001 From: Markus Amalthea Magnuson Date: Thu, 19 Mar 2015 14:07:37 +0100 Subject: [PATCH 51/58] Style ./manage.py examples like inline code. --- docs/jinja2.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/jinja2.txt b/docs/jinja2.txt index b0e3a96..f492279 100644 --- a/docs/jinja2.txt +++ b/docs/jinja2.txt @@ -48,7 +48,7 @@ Usage ----- Run the following compress command along with an ``--engine`` parameter. The parameter can be either jinja2 or django (default). For example, -"./manage.py compress --engine jinja2". +``./manage.py compress --engine jinja2``. Using both Django and Jinja2 templates -------------------------------------- @@ -60,9 +60,9 @@ template safely. (Vice versa for Django parser). A typical usage could be : -- "./manage.py compress" for processing Django templates first, skipping +- ``./manage.py compress`` for processing Django templates first, skipping Jinja2 templates. -- "./manage.py compress --engine jinja2" for processing Jinja2 templates, +- ``./manage.py compress --engine jinja2`` for processing Jinja2 templates, skipping Django templates. However, it is still recommended that you do not mix Django and Jinja2 From f934c6aeb11a4bc5d7ab6c2c93dd0ca099153124 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 25 Mar 2015 11:51:32 +0100 Subject: [PATCH 52/58] Change Notes: Add >1.4 changes --- docs/changelog.txt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/changelog.txt b/docs/changelog.txt index 26a4a35..627e99f 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -1,6 +1,16 @@ Changelog ========= +v1.5 (03/27/2015) +----------------- + +- Added compatibility with Django 1.7.x and 1.8.x + +- Fixed various other issues. See full `Changes for 1.5`_ on GitHub + +.. _Changes for 1.5: https://github.com/django-compressor/django-compressor/compare/1.4...develop + + v1.4 (06/20/2014) ----------------- From 248544d052333e87cbf540f918be86b8d8369c6a Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Wed, 25 Mar 2015 14:33:51 +0100 Subject: [PATCH 53/58] Full list of merged PRs --- docs/changelog.txt | 72 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index 627e99f..da53d5a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -4,11 +4,77 @@ Changelog v1.5 (03/27/2015) ----------------- -- Added compatibility with Django 1.7.x and 1.8.x +`Full Changelog `_ -- Fixed various other issues. See full `Changes for 1.5`_ on GitHub +- Fix typo and restyle manage command examples `\#604 `_ (`alimony `_) -.. _Changes for 1.5: https://github.com/django-compressor/django-compressor/compare/1.4...develop +- Fix compress command for 1.8 `\#601 `_ (`carltongibson `_) + +- Fix build `\#599 `_ (`carltongibson `_) + +- Update settings.txt `\#584 `_ (`johnfraney `_) + +- Fix Django 1.8 warnings `\#583 `_ (`sdfsdhgjkbmnmxc `_) + +- Handle TypeError from import\_module `\#571 `_ (`blueyed `_) + +- Fix reading UTF-8 files which have BOM `\#568 `_ (`JereMalinen `_) + +- shlex.quote/pipes.quote is not compatible with Windows `\#561 `_ (`thenewguy `_) + +- Run automated tests on Django 1.7 `\#556 `_ (`frewsxcv `_) + +- Patch 1 `\#555 `_ (`francescozaia `_) + +- Fix AttributeError in offline compression `\#554 `_ (`cbjadwani `_) + +- Support for clean-css `\#551 `_ (`vovanbo `_) + +- Types at readme \(-engine should be --engine\) `\#540 `_ (`lukaszb `_) + +- Fix typo `\#539 `_ (`alimony `_) + +- Fix link markup `\#530 `_ (`merwok `_) + +- Add support for COMPRESS\_CSS\_HASHING\_METHOD = None `\#526 `_ (`diox `_) + +- Remove compatibility with old 'staticfiles' app `\#525 `_ (`diox `_) + +- Use get\_template\(\) when dealing with django templates `\#524 `_ (`diox `_) + +- Fix FilterBase clears self.type for subclasses `\#479 `_ (`iknite `_) + +- Remove unnecessary filename and existence checks in CssAbsoluteFilter `\#471 `_ (`agriffis `_) + +- Fix minor typographical error in docs/index `\#594 `_ (`varunsharma `_) + +- Fix deprecation warning `\#548 `_ (`davidfischer-ch `_) + +- Add "enabled" override to compress template tag `\#547 `_ (`davidfischer-ch `_) + +- CSSO support `\#538 `_ (`vovanbo `_) + +- quote filename in command `\#536 `_ (`MikeAmy `_) + +- quote filename e.g. in case it contains spaces `\#535 `_ (`MikeAmy `_) + +- Put charset back into kwargs `\#523 `_ (`zmetcalf `_) + +- Fix bug with css compress for static files `\#504 `_ (`rogaha `_) + +- Support for async/defer tags `\#482 `_ (`hkpeprah `_) + +- resolve \[issue 171\] : offline compressor to compress 'compress tags' if those are not in child template but in parent template. `\#436 `_ (`sjiitr `_) + +- Don't leave `compressed` in the render context `\#354 `_ (`gavinwahl `_) + +- Adding support to ignore some file paths during offline compression `\#335 `_ (`ayadav `_) + +- Use django.template.loader.get\_template\(\) in compress management command `\#282 `_ (`mattpatey `_) + +- Compress selected templates `\#248 `_ (`jaap3 `_) + +- re-implementing default documented behaviour for uncompressable files, fixes \#241 `\#246 `_ (`ssaboum `_) v1.4 (06/20/2014) From b91ea517c66b2d136164f5e687bc18a2f98a0b91 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Fri, 27 Mar 2015 15:36:02 +0100 Subject: [PATCH 54/58] Tidy up as per @diox's comments --- docs/changelog.txt | 70 ++++++++++------------------------------------ 1 file changed, 14 insertions(+), 56 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index da53d5a..d127636 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -6,75 +6,33 @@ v1.5 (03/27/2015) `Full Changelog `_ -- Fix typo and restyle manage command examples `\#604 `_ (`alimony `_) +- Fix compress command and run automated tests for Django 1.8 (`carltongibson `_) -- Fix compress command for 1.8 `\#601 `_ (`carltongibson `_) +- Fix Django 1.8 warnings (`sdfsdhgjkbmnmxc `_) -- Fix build `\#599 `_ (`carltongibson `_) +- Handle TypeError from import_module (`blueyed `_) -- Update settings.txt `\#584 `_ (`johnfraney `_) +- Fix reading UTF-8 files which have BOM (`JereMalinen `_) -- Fix Django 1.8 warnings `\#583 `_ (`sdfsdhgjkbmnmxc `_) +- Fix incompatibility with Windows (shell_quote is not supported) (`thenewguy `_) -- Handle TypeError from import\_module `\#571 `_ (`blueyed `_) +- Run automated tests on Django 1.7 (`frewsxcv `_) -- Fix reading UTF-8 files which have BOM `\#568 `_ (`JereMalinen `_) +- Ignore non-existent {{ block.super }} in offline compression instead of raising AttributeError (`cbjadwani `_) -- shlex.quote/pipes.quote is not compatible with Windows `\#561 `_ (`thenewguy `_) +- Support for clean-css (`vovanbo `_) -- Run automated tests on Django 1.7 `\#556 `_ (`frewsxcv `_) +- Fix link markup (`@merwok `_) -- Patch 1 `\#555 `_ (`francescozaia `_) +- Add support for COMPRESS_CSS_HASHING_METHOD = None (`diox `_) -- Fix AttributeError in offline compression `\#554 `_ (`cbjadwani `_) +- Remove compatibility with old 'staticfiles' app (`diox `_) -- Support for clean-css `\#551 `_ (`vovanbo `_) +- In compress command, use get_template() instead of opening template files manually, fixing compatibility issues with custom template loaders (`diox `_) -- Types at readme \(-engine should be --engine\) `\#540 `_ (`lukaszb `_) +- Fix FilterBase so that does not override self.type for subclasses if filter_type is not specified at init (`iknite `_) -- Fix typo `\#539 `_ (`alimony `_) - -- Fix link markup `\#530 `_ (`merwok `_) - -- Add support for COMPRESS\_CSS\_HASHING\_METHOD = None `\#526 `_ (`diox `_) - -- Remove compatibility with old 'staticfiles' app `\#525 `_ (`diox `_) - -- Use get\_template\(\) when dealing with django templates `\#524 `_ (`diox `_) - -- Fix FilterBase clears self.type for subclasses `\#479 `_ (`iknite `_) - -- Remove unnecessary filename and existence checks in CssAbsoluteFilter `\#471 `_ (`agriffis `_) - -- Fix minor typographical error in docs/index `\#594 `_ (`varunsharma `_) - -- Fix deprecation warning `\#548 `_ (`davidfischer-ch `_) - -- Add "enabled" override to compress template tag `\#547 `_ (`davidfischer-ch `_) - -- CSSO support `\#538 `_ (`vovanbo `_) - -- quote filename in command `\#536 `_ (`MikeAmy `_) - -- quote filename e.g. in case it contains spaces `\#535 `_ (`MikeAmy `_) - -- Put charset back into kwargs `\#523 `_ (`zmetcalf `_) - -- Fix bug with css compress for static files `\#504 `_ (`rogaha `_) - -- Support for async/defer tags `\#482 `_ (`hkpeprah `_) - -- resolve \[issue 171\] : offline compressor to compress 'compress tags' if those are not in child template but in parent template. `\#436 `_ (`sjiitr `_) - -- Don't leave `compressed` in the render context `\#354 `_ (`gavinwahl `_) - -- Adding support to ignore some file paths during offline compression `\#335 `_ (`ayadav `_) - -- Use django.template.loader.get\_template\(\) in compress management command `\#282 `_ (`mattpatey `_) - -- Compress selected templates `\#248 `_ (`jaap3 `_) - -- re-implementing default documented behaviour for uncompressable files, fixes \#241 `\#246 `_ (`ssaboum `_) +- Remove unnecessary filename and existence checks in CssAbsoluteFilter (`agriffis `_) v1.4 (06/20/2014) From 79bdf0ad254c7d7cb704e68516a8ba795c39a0b3 Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Thu, 2 Apr 2015 15:31:58 +0200 Subject: [PATCH 55/58] Bump tox.ini to test with Django 1.8 final --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 280d4cb..78d6039 100644 --- a/tox.ini +++ b/tox.ini @@ -57,10 +57,10 @@ deps = 1.5.X: Django>=1.5,<1.6 1.6.X: Django>=1.6,<1.7 1.7.X: Django>=1.7,<1.8 - 1.8.X: Django==1.8b2 + 1.8.X: Django>=1.8,<1.9 py26: {[deps]two} py27: {[deps]two} py32: {[deps]three_two} py33: {[deps]three} py34: {[deps]three} - django-discover-runner \ No newline at end of file + django-discover-runner From 92e7a85855cb9d6faf9a12decaa2e9fef3d7a874 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 16 Apr 2015 10:31:30 +0200 Subject: [PATCH 56/58] Removed links to contributot profiles. --- docs/changelog.txt | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/changelog.txt b/docs/changelog.txt index d127636..07864df 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -6,33 +6,33 @@ v1.5 (03/27/2015) `Full Changelog `_ -- Fix compress command and run automated tests for Django 1.8 (`carltongibson `_) +- Fix compress command and run automated tests for Django 1.8 -- Fix Django 1.8 warnings (`sdfsdhgjkbmnmxc `_) +- Fix Django 1.8 warnings -- Handle TypeError from import_module (`blueyed `_) +- Handle TypeError from import_module -- Fix reading UTF-8 files which have BOM (`JereMalinen `_) +- Fix reading UTF-8 files which have BOM -- Fix incompatibility with Windows (shell_quote is not supported) (`thenewguy `_) +- Fix incompatibility with Windows (shell_quote is not supported) -- Run automated tests on Django 1.7 (`frewsxcv `_) +- Run automated tests on Django 1.7 -- Ignore non-existent {{ block.super }} in offline compression instead of raising AttributeError (`cbjadwani `_) +- Ignore non-existent {{ block.super }} in offline compression instead of raising AttributeError -- Support for clean-css (`vovanbo `_) +- Support for clean-css -- Fix link markup (`@merwok `_) +- Fix link markup -- Add support for COMPRESS_CSS_HASHING_METHOD = None (`diox `_) +- Add support for COMPRESS_CSS_HASHING_METHOD = None -- Remove compatibility with old 'staticfiles' app (`diox `_) +- Remove compatibility with old 'staticfiles' app -- In compress command, use get_template() instead of opening template files manually, fixing compatibility issues with custom template loaders (`diox `_) +- In compress command, use get_template() instead of opening template files manually, fixing compatibility issues with custom template loaders -- Fix FilterBase so that does not override self.type for subclasses if filter_type is not specified at init (`iknite `_) +- Fix FilterBase so that does not override self.type for subclasses if filter_type is not specified at init -- Remove unnecessary filename and existence checks in CssAbsoluteFilter (`agriffis `_) +- Remove unnecessary filename and existence checks in CssAbsoluteFilter v1.4 (06/20/2014) From d4ed174936e38520a01af0d65d7ff4ae0bf61fe5 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Thu, 16 Apr 2015 10:35:30 +0200 Subject: [PATCH 57/58] Small changes to contributing notes. --- docs/contributing.txt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/contributing.txt b/docs/contributing.txt index 225a1ae..8f0cd50 100644 --- a/docs/contributing.txt +++ b/docs/contributing.txt @@ -9,11 +9,12 @@ tidy, everybody has to follow a few rules (nothing major, I promise :) ) Community --------- -People interested in developing for the Django Compressor should head -over to #django-compressor on the `freenode`_ IRC network for help and to -discuss the development. +People interested in developing for the Django Compressor should: + +1. Head over to #django-compressor on the `freenode`_ IRC network for help and to +discuss the development. +2. Open an issue on GitHub explaining your ideas. -You may also be interested in following `@jezdez`_ on Twitter. In a nutshell ------------- @@ -143,7 +144,7 @@ Documentation should be: - Accessible. You should assume the reader to be moderately familiar with Python and Django, but not anything else. Link to documentation of libraries you use, for example, even if they are "obvious" to you. A brief - description of what it does is also welcome. + description of what it does is also welcome. Pulling of documentation is pretty fast and painless. Usually somebody goes over your text and merges it, since there are no "breaks" and that github From 49c3165a6af0d9b068f6f2b93e186fd831b0d50e Mon Sep 17 00:00:00 2001 From: Mathieu Pillard Date: Mon, 20 Apr 2015 16:13:10 +0200 Subject: [PATCH 58/58] Add @carltongibson to authors --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index de59146..7b9a571 100644 --- a/AUTHORS +++ b/AUTHORS @@ -29,6 +29,7 @@ Bojan Mihelac Boris Shemigon Brad Whittington Bruno Renié +Carlton Gibson Cassus Adam Banko Chris Adams Chris Streeter