+python-django-compressor (2.0-1~u14.04+mos1) mos9.0; urgency=medium
+
+ * Update to 2.0
+ * Update runtime requirements according to global requirements of Mitaka
+ * Sources are from
+ http://http.debian.net/debian/pool/main/p/python-django-compressor/python-django-compressor_2.0-1.dsc
+ * Related-Bug: #1545692
+
+ -- Daniil Trishkin <dtrishkin@mirantis.com> Mon, 14 Mar 2016 16:13:42 +0300
+
+python-django-compressor (2.0-1) unstable; urgency=medium
+
+ * New upstream release.
+ * Removed compressor/filters/jsmin/rjsmin.py from d/copyright.
+ * Removed compressor/utils/stringformat.py from d/copyright.
+
+ -- Thomas Goirand <zigo@debian.org> Mon, 11 Jan 2016 02:57:22 +0000
+
+python-django-compressor (1.6+2015.12.22.git.94755c5aa6-1) unstable; urgency=medium
+
+ * New upstream release based on commit 94755c5aa6 (Closes: #807355).
+ * Added new (build-)dependencies:
+ - python-django-overextends
+ - python-django-sekizai
+ - python-rjsmin
+ - python-rcssmin
+ - python-csscompressor
+ * Upped the min required version for python-coffin.
+
+ -- Thomas Goirand <zigo@debian.org> Tue, 15 Dec 2015 16:13:22 +0000
+
+python-django-compressor (1.5-3) unstable; urgency=medium
+
+ * Transition to python-django-compressor instead of python-compressor to
+ respect upstream egg-info (Closes: #799260).
+
+ -- Thomas Goirand <zigo@debian.org> Fri, 16 Oct 2015 09:10:17 +0000
+
python-django-compressor (1.5-2~u14.04+mos1) mos8.0; urgency=medium
* Source: http://http.debian.net/debian/pool/main/p/python-django-compressor/python-django-compressor_1.5-2.dsc
python-all,
python-setuptools,
python3-all,
- python3-setuptools
+ python3-setuptools,
Build-Depends-Indep: csstidy,
python-appconf,
python-bs4,
- python-coffin,
+ python-coffin (>= 2.0.1),
python-coverage,
+ python-csscompressor,
python-django,
python-django-discover-runner,
+ python-django-overextends,
+ python-django-sekizai (>= 0.9.0),
python-html5lib,
python-jingo,
python-jinja2,
python-lxml,
python-mock,
python-nose,
+ python-rjsmin,
+ python-rcssmin,
python-unittest2,
python3-appconf,
python3-bs4,
- python3-coffin,
+ python3-coffin (>= 2.0.1),
python3-coverage,
+ python3-csscompressor,
python3-django,
python3-django-discover-runner,
+ python3-django-overextends,
+ python3-django-sekizai (>= 0.9.0),
python3-html5lib,
python3-jingo,
python3-jinja2,
python3-lxml,
python3-mock,
python3-nose,
- python3-unittest2
+ python3-rjsmin,
+ python3-rcssmin,
+ python3-unittest2,
Standards-Version: 3.9.6
Vcs-Browser: http://anonscm.debian.org/gitweb/?p=openstack/python-django-compressor.git;a=summary
Vcs-Git: git://anonscm.debian.org/openstack/python-django-compressor.git
Homepage: http://pypi.python.org/pypi/django_compressor/
-Package: python-compressor
+Package: python-django-compressor
Architecture: all
-Depends: python-appconf,
- python-django,
+Depends: python-csscompressor,
+ python-django (>= 1.8),
+ python-django-appconf,
+ python-django-overextends,
+ python-rjsmin,
+ python-rcssmin,
${misc:Depends},
- ${python:Depends}
-Provides: ${python:Provides}
+ ${python:Depends},
+Breaks: python-compressor (<= 1.5-2),
+Replaces: python-compressor (<= 1.5-2),
+Provides: python-compressor,
+ ${python:Provides},
Description: Compresses linked, inline JS or CSS into single cached files - Python 2.7
Django Compressor combines and compresses linked and inline Javascript or CSS
in a Django templates into cacheable static files by using the compress
.
This package contains the Python 2.7 module.
-Package: python3-compressor
+Package: python3-django-compressor
Architecture: all
-Depends: python3-appconf,
- python3-django,
+Depends: python3-csscompressor,
+ python3-django (>= 1.8),
+ python3-django-appconf,
+ python3-django-overextends,
+ python3-rjsmin,
+ python3-rcssmin,
${misc:Depends},
- ${python3:Depends}
-Provides: ${python:Provides}
+ ${python3:Depends},
+Breaks: python3-compressor (<= 1.5-2),
+Replaces: python3-compressor (<= 1.5-2),
+Provides: python3-compressor,
+ ${python3:Provides},
Description: Compresses linked, inline JS or CSS into single cached files - Python 3.x
Django Compressor combines and compresses linked and inline Javascript or CSS
in a Django templates into cacheable static files by using the compress
template tag.
.
This package contains the Python 3.x module.
+
+Package: python-compressor
+Section: oldlibs
+Priority: extra
+Architecture: all
+Depends: python-django-compressor (>= 2.0),
+ ${misc:Depends},
+Provides: ${python:Provides},
+Description: Compresses linked, inline JS or CSS - Python 2.7 transition package
+ Django Compressor combines and compresses linked and inline Javascript or CSS
+ in a Django templates into cacheable static files by using the compress
+ template tag.
+ .
+ This is a transitional package to upgrade to python-django-compressor, and it
+ is safe to remove.
+
+Package: python3-compressor
+Section: oldlibs
+Priority: extra
+Architecture: all
+Depends: python3-django-compressor (>= 2.0),
+ ${misc:Depends},
+Description: Compresses linked, inline JS or CSS - Python 3.x transition package
+ Django Compressor combines and compresses linked and inline Javascript or CSS
+ in a Django templates into cacheable static files by using the compress
+ template tag.
+ .
+ This is a transitional package to upgrade to python3-django-compressor, and it
+ is safe to remove.
Copyright: (c) 2012, Thomas Goirand <zigo@debian.org>
License: MIT
-Files: compressor/filters/jsmin/rjsmin.py
-Copyright: 2006-2011, André Malo
-License: Apache-2.0
-
Files: compressor/filters/cssmin/*
Copyright: (c) 2010 Zachary Voase
License: MIT
Copyright: 2009-2011, Ask Solem and contributors
License: BSD-2-clauses
-Files: compressor/utils/stringformat.py
-Copyright: 2010, Florent Xicluna
-License: BSD-3-clauses
-
Files: *
Copyright: 2009-2011 django_compressor authors (see AUTHORS file)
2008 Andreas Pelme <andreas@pelme.se>
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-License: BSD-3-clauses
- Redistribution and use in source and binary forms of the software as well as
- documentation, with or without modification, are permitted provided that the
- following conditions are met:
- .
- * Redistributions of source code must retain the above copyright notice, this
- list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
- * The names of the contributors may not be used to endorse or promote products
- derived from this software without specific prior written permission.
- .
- THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
- PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
- CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
- EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
- PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
- BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-
License: BSD-2-clauses
Redistribution and use in source and binary forms of the software as well as
documentation, with or without modification, are permitted provided that the
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
-
-License: Apache-2.0
- Licensed under the Apache License, Version 2.0 (the "License"); you may not
- use this file except in compliance with the License. You may obtain a copy of
- the License at
- .
- http://www.apache.org/licenses/LICENSE-2.0
- .
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- License for the specific language governing permissions and limitations under
- the License.
- .
- On Debian based systems, the full text of the Apache-2.0 license is available
- in this file: /usr/share/common-licenses/Apache-2.0
+++ /dev/null
-Description: Removes failed test
- This unit test is failing, so removing it to build the package.
-Author: Thomas Goirand <zigo@debian.org>
-Forwarded: no
-Last-Update: 2015-08-04
-
---- python-django-compressor-1.5.orig/compressor/tests/test_base.py
-+++ python-django-compressor-1.5/compressor/tests/test_base.py
-@@ -288,34 +288,6 @@ class CacheBackendTestCase(CompressorTes
- self.assertEqual(cache.__class__, locmem.LocMemCache)
-
-
--class JsAsyncDeferTestCase(SimpleTestCase):
-- def setUp(self):
-- self.js = """\
-- <script src="/static/js/one.js" type="text/javascript"></script>
-- <script src="/static/js/two.js" type="text/javascript" async></script>
-- <script src="/static/js/three.js" type="text/javascript" defer></script>
-- <script type="text/javascript">obj.value = "value";</script>
-- <script src="/static/js/one.js" type="text/javascript" async></script>
-- <script src="/static/js/two.js" type="text/javascript" async></script>
-- <script src="/static/js/three.js" type="text/javascript"></script>"""
--
-- 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)
--
--
- class CacheTestCase(SimpleTestCase):
-
- def setUp(self):
+++ /dev/null
-remove-failed-test.patch
PYTHONS:=$(shell pyversions -vr)
PYTHON3S:=$(shell py3versions -vr)
-UPSTREAM_GIT = git://github.com/jezdez/django_compressor.git
+UPSTREAM_GIT = git://github.com/django-compressor/django-compressor.git
include /usr/share/openstack-pkg-tools/pkgos.make
override_dh_install:
set -e ; for pyvers in $(PYTHONS); do \
python$$pyvers setup.py install --install-layout=deb \
- --root $(CURDIR)/debian/python-compressor; \
+ --root $(CURDIR)/debian/python-django-compressor; \
done
set -e ; for pyvers in $(PYTHON3S); do \
python$$pyvers setup.py install --install-layout=deb \
- --root $(CURDIR)/debian/python3-compressor; \
+ --root $(CURDIR)/debian/python3-django-compressor; \
done
rm -f $(CURDIR)/debian/usr/lib/python*/dist-packages/compressor/tests/static/CACHE/css/*
rm -f $(CURDIR)/debian/usr/lib/python*/dist-packages/compressor/tests/static/CACHE/js/*
language: python
-before_install:
- - sudo apt-get update
- - sudo apt-get install csstidy libxml2-dev libxslt-dev
+sudo: false
install:
- pip install tox
script:
- tox
env:
- - TOXENV=py26-1.4.X
- - TOXENV=py26-1.5.X
- - TOXENV=py27-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
- TOXENV=py27-1.8.X
- TOXENV=py32-1.8.X
- TOXENV=py33-1.8.X
- TOXENV=py34-1.8.X
+ - TOXENV=py27-1.9.X
+ - TOXENV=py34-1.9.X
+# https://github.com/travis-ci/travis-ci/issues/4794
+matrix:
+ include:
+ - python: 3.5
+ env:
+ - TOXENV=py35-1.8.X
+ - python: 3.5
+ env:
+ - TOXENV=py35-1.9.X
notifications:
irc: "irc.freenode.org#django-compressor"
+after_success:
+ - pip install codecov
+ - codecov
Mehmet S. Catalbas
Michael van de Waeter
Mike Yumatov
+Nick Pope
Nicolas Charlot
Niran Babalola
Paul McMillan
django_compressor
-----------------
-Copyright (c) 2009-2014 Django Compressor authors (see AUTHORS file)
+Copyright (c) 2009-2015 Django Compressor authors (see AUTHORS file)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
pip install -r requirements/tests.txt
pip install Django
-test:
+flake8:
flake8 compressor --ignore=E501,E128,E701,E261,E301,E126,E127,E131
+
+runtests:
coverage run --branch --source=compressor `which django-admin.py` test --settings=compressor.test_settings compressor
- coverage report --omit=compressor/test*,compressor/filters/jsmin/rjsmin*,compressor/filters/cssmin/cssmin*,compressor/utils/stringformat*
-.PHONY: test
+coveragereport:
+ coverage report --omit=compressor/test*
+
+test: flake8 runtests coveragereport
+
+.PHONY: test runtests flake8 coveragereport
Django Compressor
=================
-.. 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:: http://codecov.io/github/django-compressor/django-compressor/coverage.svg?branch=develop
+ :target: http://codecov.io/github/django-compressor/django-compressor?branch=develop
.. image:: https://pypip.in/v/django_compressor/badge.svg
:target: https://pypi.python.org/pypi/django_compressor
html5lib_ based parser, as well as an abstract base class that makes it easy to
write a custom parser.
-Django Compressor also comes with built-in support for `CSS Tidy`_,
+Django Compressor also comes with built-in support for
`YUI CSS and JS`_ compressor, `yUglify CSS and JS`_ compressor, the Google's
`Closure Compiler`_, a Python port of Douglas Crockford's JSmin_, a Python port
-of the YUI CSS Compressor cssmin_ and a filter to convert (some) images into
+of the YUI CSS Compressor csscompressor_ and a filter to convert (some) images into
`data URIs`_.
If your setup requires a different compressor or other post-processing
.. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/
.. _lxml: http://lxml.de/
-.. _html5lib: http://code.google.com/p/html5lib/
-.. _CSS Tidy: http://csstidy.sourceforge.net/
+.. _html5lib: https://github.com/html5lib/html5lib-python
.. _YUI CSS and JS: http://developer.yahoo.com/yui/compressor/
.. _yUglify CSS and JS: https://github.com/yui/yuglify
.. _Closure Compiler: http://code.google.com/closure/compiler/
.. _JSMin: http://www.crockford.com/javascript/jsmin.html
-.. _cssmin: https://github.com/zacharyvoase/cssmin
+.. _csscompressor: https://github.com/sprymix/csscompressor
.. _data URIs: http://en.wikipedia.org/wiki/Data_URI_scheme
.. _django-compressor.readthedocs.org: http://django-compressor.readthedocs.org/en/latest/
.. _github.com/django-compressor/django-compressor: https://github.com/django-compressor/django-compressor
-
# following PEP 386
-__version__ = "1.5"
+__version__ = "2.0"
from __future__ import with_statement, unicode_literals
import os
import codecs
+from importlib import import_module
from django.core.files.base import ContentFile
-from django.template import Context
-from django.template.loader import render_to_string
-try:
- from importlib import import_module
-except:
- from django.utils.importlib import import_module
from django.utils.safestring import mark_safe
-
-try:
- from urllib.request import url2pathname
-except ImportError:
- from urllib import url2pathname
+from django.utils.six.moves.urllib.request import url2pathname
+from django.template.loader import render_to_string
from compressor.cache import get_hexdigest, get_mtime
from compressor.conf import settings
from compressor.exceptions import (CompressorError, UncompressableFileError,
FilterDoesNotExist)
-from compressor.filters import CompilerFilter
+from compressor.filters import CachedCompilerFilter
+from compressor.filters.css_default import CssAbsoluteFilter
from compressor.storage import compressor_file_storage
from compressor.signals import post_compress
from compressor.utils import get_class, get_mod_func, staticfiles
Base compressor object to be subclassed for content type
depending implementations details.
"""
- type = None
- def __init__(self, content=None, output_prefix=None, context=None, *args, **kwargs):
+ def __init__(self, content=None, output_prefix=None,
+ context=None, filters=None, *args, **kwargs):
self.content = content or "" # rendered contents of {% compress %} tag
self.output_prefix = output_prefix or "compressed"
self.output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
self.charset = settings.DEFAULT_CHARSET
self.split_content = []
self.context = context or {}
+ self.type = output_prefix or ""
+ self.filters = filters or []
self.extra_context = {}
- self.all_mimetypes = dict(settings.COMPRESS_PRECOMPILERS)
+ self.precompiler_mimetypes = dict(settings.COMPRESS_PRECOMPILERS)
self.finders = staticfiles.finders
self._storage = None
get_filename('css/one.css') -> '/full/path/to/static/css/one.css'
"""
filename = None
- # first try finding the file in the root
- try:
- # call path first so remote storages don't make it to exists,
- # which would cause network I/O
- filename = self.storage.path(basename)
- if not self.storage.exists(basename):
- filename = None
- except NotImplementedError:
- # remote storages don't implement path, access the file locally
- if compressor_file_storage.exists(basename):
- filename = compressor_file_storage.path(basename)
- # secondly try to find it with staticfiles (in debug mode)
+ # First try finding the file using the storage class.
+ # This is skipped in DEBUG mode as files might be outdated in
+ # compressor's final destination (COMPRESS_ROOT) during development
+ if not settings.DEBUG:
+ try:
+ # call path first so remote storages don't make it to exists,
+ # which would cause network I/O
+ filename = self.storage.path(basename)
+ if not self.storage.exists(basename):
+ filename = None
+ except NotImplementedError:
+ # remote storages don't implement path, access the file locally
+ if compressor_file_storage.exists(basename):
+ filename = compressor_file_storage.path(basename)
+ # secondly try to find it with staticfiles
if not filename and self.finders:
filename = self.finders.find(url2pathname(basename))
if filename:
options = dict(options, filename=value)
value = self.get_filecontent(value, charset)
- if self.all_mimetypes:
+ if self.precompiler_mimetypes:
precompiled, value = self.precompile(value, **options)
if enabled:
- yield self.filter(value, **options)
+ yield self.filter(value, self.cached_filters, **options)
+ elif precompiled:
+ # since precompiling moves files around, it breaks url()
+ # statements in css files. therefore we run the absolute filter
+ # on precompiled css files even if compression is disabled.
+ if CssAbsoluteFilter in self.cached_filters:
+ value = self.filter(value, [CssAbsoluteFilter], **options)
+ yield self.handle_output(kind, value, forced=True,
+ basename=basename)
else:
- if precompiled:
- yield self.handle_output(kind, value, forced=True,
- basename=basename)
- else:
- yield self.parser.elem_str(elem)
+ yield self.parser.elem_str(elem)
def filter_output(self, content):
"""
Passes the concatenated content to the 'output' methods
of the compressor filters.
"""
- return self.filter(content, method=METHOD_OUTPUT)
+ return self.filter(content, self.cached_filters, method=METHOD_OUTPUT)
def filter_input(self, forced=False):
"""
return False, content
attrs = self.parser.elem_attribs(elem)
mimetype = attrs.get("type", None)
- if mimetype:
- filter_or_command = self.all_mimetypes.get(mimetype)
- if filter_or_command is None:
- if mimetype not in ("text/css", "text/javascript"):
- raise CompressorError("Couldn't find any precompiler in "
- "COMPRESS_PRECOMPILERS setting for "
- "mimetype '%s'." % mimetype)
- else:
- mod_name, cls_name = get_mod_func(filter_or_command)
- try:
- mod = import_module(mod_name)
- except (ImportError, TypeError):
- filter = CompilerFilter(
- content, filter_type=self.type, filename=filename,
- charset=charset, command=filter_or_command)
- return True, filter.input(**kwargs)
- try:
- precompiler_class = getattr(mod, cls_name)
- except AttributeError:
- raise FilterDoesNotExist('Could not find "%s".' %
- filter_or_command)
- else:
- filter = precompiler_class(
- content, attrs, filter_type=self.type, charset=charset,
- filename=filename)
- return True, filter.input(**kwargs)
-
- return False, content
-
- def filter(self, content, method, **kwargs):
- for filter_cls in self.cached_filters:
+ if mimetype is None:
+ return False, content
+
+ filter_or_command = self.precompiler_mimetypes.get(mimetype)
+ if filter_or_command is None:
+ if mimetype in ("text/css", "text/javascript"):
+ return False, content
+ raise CompressorError("Couldn't find any precompiler in "
+ "COMPRESS_PRECOMPILERS setting for "
+ "mimetype '%s'." % mimetype)
+
+ mod_name, cls_name = get_mod_func(filter_or_command)
+ try:
+ mod = import_module(mod_name)
+ except (ImportError, TypeError):
+ filter = CachedCompilerFilter(
+ content=content, filter_type=self.type, filename=filename,
+ charset=charset, command=filter_or_command, mimetype=mimetype)
+ return True, filter.input(**kwargs)
+ try:
+ precompiler_class = getattr(mod, cls_name)
+ except AttributeError:
+ raise FilterDoesNotExist('Could not find "%s".' % filter_or_command)
+ filter = precompiler_class(
+ content, attrs=attrs, filter_type=self.type, charset=charset,
+ filename=filename)
+ return True, filter.input(**kwargs)
+
+ def filter(self, content, filters, method, **kwargs):
+ for filter_cls in filters:
filter_func = getattr(
filter_cls(content, filter_type=self.type), method)
try:
self.context['compressed'].update(context or {})
self.context['compressed'].update(self.extra_context)
- final_context = Context(self.context)
+ if hasattr(self.context, 'flatten'):
+ # Django 1.8 complains about Context being passed to its
+ # Template.render function.
+ final_context = self.context.flatten()
+ else:
+ final_context = self.context
+
post_compress.send(sender=self.__class__, type=self.type,
mode=mode, context=final_context)
template_name = self.get_template_name(mode)
- return render_to_string(template_name, context_instance=final_context)
+ return render_to_string(template_name, context=final_context)
import os
import socket
import time
+from importlib import import_module
-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.cache import caches
from django.core.files.base import ContentFile
from django.utils.encoding import force_text, smart_bytes
from django.utils.functional import SimpleLazyObject
-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 compressor.utils import get_mod_func
return get_hexdigest(file.read(), length)
+def get_precompiler_cachekey(command, contents):
+ return hashlib.sha1(smart_bytes('precompiler.%s.%s' % (command, contents))).hexdigest()
+
+
def cache_get(key):
packed_val = cache.get(key)
if packed_val is None:
return cache.set(key, packed_val, real_timeout)
-cache = SimpleLazyObject(lambda: get_cache(settings.COMPRESS_CACHE_BACKEND))
+cache = SimpleLazyObject(lambda: caches[settings.COMPRESS_CACHE_BACKEND])
import os
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
+from django.template.utils import InvalidTemplateEngineError
from appconf import AppConf
# ('text/stylus', 'stylus < {infile} > {outfile}'),
# ('text/x-scss', 'sass --scss {infile} {outfile}'),
)
+ CACHEABLE_PRECOMPILERS = ()
CLOSURE_COMPILER_BINARY = 'java -jar compiler.jar'
CLOSURE_COMPILER_ARGUMENTS = ''
- CSSTIDY_BINARY = 'csstidy'
- CSSTIDY_ARGUMENTS = '--template=highest'
YUI_BINARY = 'java -jar yuicompressor.jar'
YUI_CSS_ARGUMENTS = ''
YUI_JS_ARGUMENTS = ''
OFFLINE_MANIFEST = 'manifest.json'
# The Context to be used when TemplateFilter is used
TEMPLATE_FILTER_CONTEXT = {}
- # Function that returns the Jinja2 environment to use in offline compression.
+
+ # Returns the Jinja2 environment to use in offline compression.
def JINJA2_GET_ENVIRONMENT():
+ alias = 'Jinja2'
try:
- import jinja2
- return jinja2.Environment()
+ from django.template.loader import _engine_list
+ engines = _engine_list(alias)
+ if engines:
+ engine = engines[0]
+ return engine.env
+ except InvalidTemplateEngineError:
+ raise InvalidTemplateEngineError(
+ "Could not find config for '{}' "
+ "in settings.TEMPLATES. "
+ "COMPRESS_JINJA2_GET_ENVIRONMENT() may "
+ "need to be defined in settings".format(alias))
except ImportError:
return None
and: https://github.com/ojii/django-sekizai.git@0.6 or later
"""
from compressor.templatetags.compress import CompressorNode
-from django.template.base import Template
+from django.template.base import TextNode
def compress(context, data, name):
Name is either 'js' or 'css' (the sekizai namespace)
Basically passes the string through the {% compress 'js' %} template tag
"""
- return CompressorNode(nodelist=Template(data).nodelist, kind=name, mode='file').render(context=context)
+ return CompressorNode(nodelist=TextNode(data), kind=name, mode='file').render(context=context)
class CssCompressor(Compressor):
def __init__(self, content=None, output_prefix="css", context=None):
- super(CssCompressor, self).__init__(content=content,
- output_prefix=output_prefix, context=context)
- self.filters = list(settings.COMPRESS_CSS_FILTERS)
- self.type = output_prefix
+ filters = list(settings.COMPRESS_CSS_FILTERS)
+ super(CssCompressor, self).__init__(content, output_prefix, context, filters)
def split_contents(self):
if self.split_content:
# flake8: noqa
from compressor.filters.base import (FilterBase, CallbackOutputFilter,
- CompilerFilter, FilterError)
+ CompilerFilter, CachedCompilerFilter, FilterError)
import logging
import subprocess
+from importlib import import_module
from platform import system
if system() != "Windows":
from django.core.exceptions import ImproperlyConfigured
from django.core.files.temp import NamedTemporaryFile
-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 compressor.cache import cache, get_precompiler_cachekey
+
from compressor.conf import settings
from compressor.exceptions import FilterError
from compressor.utils import get_mod_func
Subclasses should implement `input` and/or `output` methods which must
return a string (unicode under python 2) or raise a NotImplementedError.
"""
- def __init__(self, content, filter_type=None, filename=None, verbose=0,
- charset=None):
+ def __init__(self, content, attrs=None, filter_type=None, filename=None,
+ verbose=0, charset=None, **kwargs):
self.type = filter_type or getattr(self, 'type', None)
self.content = content
self.verbose = verbose or settings.COMPRESS_VERBOSE
options = ()
default_encoding = settings.FILE_CHARSET
- def __init__(self, content, command=None, *args, **kwargs):
- super(CompilerFilter, self).__init__(content, *args, **kwargs)
+ def __init__(self, content, command=None, **kwargs):
+ super(CompilerFilter, self).__init__(content, **kwargs)
self.cwd = None
if command:
self.infile = self.outfile = None
def input(self, **kwargs):
+
encoding = self.default_encoding
options = dict(self.options)
self.infile.close()
if self.outfile is not None:
self.outfile.close()
-
return smart_text(filtered)
+
+
+class CachedCompilerFilter(CompilerFilter):
+
+ def __init__(self, mimetype, *args, **kwargs):
+ self.mimetype = mimetype
+ super(CachedCompilerFilter, self).__init__(*args, **kwargs)
+
+ def input(self, **kwargs):
+ if self.mimetype in settings.COMPRESS_CACHEABLE_PRECOMPILERS:
+ key = self.get_cache_key()
+ data = cache.get(key)
+ if data is not None:
+ return data
+ filtered = super(CachedCompilerFilter, self).input(**kwargs)
+ cache.set(key, filtered, settings.COMPRESS_REBUILD_TIMEOUT)
+ return filtered
+ else:
+ return super(CachedCompilerFilter, self).input(**kwargs)
+
+ def get_cache_key(self):
+ return get_precompiler_cachekey(self.command, self.content)
from compressor.filters import CallbackOutputFilter
-class CSSMinFilter(CallbackOutputFilter):
+class CSSCompressorFilter(CallbackOutputFilter):
"""
- A filter that utilizes Zachary Voase's Python port of
- the YUI CSS compression algorithm: http://pypi.python.org/pypi/cssmin/
+ A filter that utilizes Yury Selivanov's Python port of the YUI CSS
+ compression algorithm: https://pypi.python.org/pypi/csscompressor
"""
- callback = "compressor.filters.cssmin.cssmin.cssmin"
+ callback = "csscompressor.compress"
+ dependencies = ["csscompressor"]
class rCSSMinFilter(CallbackOutputFilter):
- callback = "compressor.filters.cssmin.rcssmin.cssmin"
+ callback = "rcssmin.cssmin"
+ dependencies = ["rcssmin"]
+ kwargs = {
+ "keep_bang_comments": True
+ }
+
+# This is for backwards compatibility.
+CSSMinFilter = rCSSMinFilter
+++ /dev/null
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-#
-# `cssmin.py` - A Python port of the YUI CSS compressor.
-#
-# Copyright (c) 2010 Zachary Voase
-#
-# Permission is hereby granted, free of charge, to any person
-# obtaining a copy of this software and associated documentation
-# files (the "Software"), to deal in the Software without
-# restriction, including without limitation the rights to use,
-# copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the
-# Software is furnished to do so, subject to the following
-# conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
-# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
-# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
-# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
-# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-# OTHER DEALINGS IN THE SOFTWARE.
-#
-"""`cssmin` - A Python port of the YUI CSS compressor."""
-
-import re
-
-__version__ = '0.1.4'
-
-
-def remove_comments(css):
- """Remove all CSS comment blocks."""
-
- iemac = False
- preserve = False
- comment_start = css.find("/*")
- while comment_start >= 0:
- # Preserve comments that look like `/*!...*/`.
- # Slicing is used to make sure we don"t get an IndexError.
- preserve = css[comment_start + 2:comment_start + 3] == "!"
-
- comment_end = css.find("*/", comment_start + 2)
- if comment_end < 0:
- if not preserve:
- css = css[:comment_start]
- break
- elif comment_end >= (comment_start + 2):
- if css[comment_end - 1] == "\\":
- # This is an IE Mac-specific comment; leave this one and the
- # following one alone.
- comment_start = comment_end + 2
- iemac = True
- elif iemac:
- comment_start = comment_end + 2
- iemac = False
- elif not preserve:
- css = css[:comment_start] + css[comment_end + 2:]
- else:
- comment_start = comment_end + 2
- comment_start = css.find("/*", comment_start)
-
- return css
-
-
-def remove_unnecessary_whitespace(css):
- """Remove unnecessary whitespace characters."""
-
- def pseudoclasscolon(css):
-
- """
- Prevents 'p :link' from becoming 'p:link'.
-
- Translates 'p :link' into 'p ___PSEUDOCLASSCOLON___link'; this is
- translated back again later.
- """
-
- regex = re.compile(r"(^|\})(([^\{\:])+\:)+([^\{]*\{)")
- match = regex.search(css)
- while match:
- css = ''.join([
- css[:match.start()],
- match.group().replace(":", "___PSEUDOCLASSCOLON___"),
- css[match.end():]])
- match = regex.search(css)
- return css
-
- css = pseudoclasscolon(css)
- # Remove spaces from before things.
- css = re.sub(r"\s+([!{};:>+\(\)\],])", r"\1", css)
-
- # If there is a `@charset`, then only allow one, and move to the beginning.
- css = re.sub(r"^(.*)(@charset \"[^\"]*\";)", r"\2\1", css)
- css = re.sub(r"^(\s*@charset [^;]+;\s*)+", r"\1", css)
-
- # Put the space back in for a few cases, such as `@media screen` and
- # `(-webkit-min-device-pixel-ratio:0)`.
- css = re.sub(r"\band\(", "and (", css)
-
- # Put the colons back.
- css = css.replace('___PSEUDOCLASSCOLON___', ':')
-
- # Remove spaces from after things.
- css = re.sub(r"([!{}:;>+\(\[,])\s+", r"\1", css)
-
- return css
-
-
-def remove_unnecessary_semicolons(css):
- """Remove unnecessary semicolons."""
-
- return re.sub(r";+\}", "}", css)
-
-
-def remove_empty_rules(css):
- """Remove empty rules."""
-
- return re.sub(r"[^\}\{]+\{\}", "", css)
-
-
-def normalize_rgb_colors_to_hex(css):
- """Convert `rgb(51,102,153)` to `#336699`."""
-
- regex = re.compile(r"rgb\s*\(\s*([0-9,\s]+)\s*\)")
- match = regex.search(css)
- while match:
- colors = map(lambda s: s.strip(), match.group(1).split(","))
- hexcolor = '#%.2x%.2x%.2x' % tuple(map(int, colors))
- css = css.replace(match.group(), hexcolor)
- match = regex.search(css)
- return css
-
-
-def condense_zero_units(css):
- """Replace `0(px, em, %, etc)` with `0`."""
-
- return re.sub(r"([\s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", r"\1\2", css)
-
-
-def condense_multidimensional_zeros(css):
- """Replace `:0 0 0 0;`, `:0 0 0;` etc. with `:0;`."""
-
- css = css.replace(":0 0 0 0;", ":0;")
- css = css.replace(":0 0 0;", ":0;")
- css = css.replace(":0 0;", ":0;")
-
- # Revert `background-position:0;` to the valid `background-position:0 0;`.
- css = css.replace("background-position:0;", "background-position:0 0;")
-
- return css
-
-
-def condense_floating_points(css):
- """Replace `0.6` with `.6` where possible."""
-
- return re.sub(r"(:|\s)0+\.(\d+)", r"\1.\2", css)
-
-
-def condense_hex_colors(css):
- """Shorten colors from #AABBCC to #ABC where possible."""
-
- regex = re.compile(r"([^\"'=\s])(\s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])")
- match = regex.search(css)
- while match:
- first = match.group(3) + match.group(5) + match.group(7)
- second = match.group(4) + match.group(6) + match.group(8)
- if first.lower() == second.lower():
- css = css.replace(match.group(), match.group(1) + match.group(2) + '#' + first)
- match = regex.search(css, match.end() - 3)
- else:
- match = regex.search(css, match.end())
- return css
-
-
-def condense_whitespace(css):
- """Condense multiple adjacent whitespace characters into one."""
-
- return re.sub(r"\s+", " ", css)
-
-
-def condense_semicolons(css):
- """Condense multiple adjacent semicolon characters into one."""
-
- return re.sub(r";;+", ";", css)
-
-
-def wrap_css_lines(css, line_length):
- """Wrap the lines of the given CSS to an approximate length."""
-
- lines = []
- line_start = 0
- for i, char in enumerate(css):
- # It's safe to break after `}` characters.
- if char == '}' and (i - line_start >= line_length):
- lines.append(css[line_start:i + 1])
- line_start = i + 1
-
- if line_start < len(css):
- lines.append(css[line_start:])
- return '\n'.join(lines)
-
-
-def cssmin(css, wrap=None):
- css = remove_comments(css)
- css = condense_whitespace(css)
- # A pseudo class for the Box Model Hack
- # (see http://tantek.com/CSS/Examples/boxmodelhack.html)
- css = css.replace('"\\"}\\""', "___PSEUDOCLASSBMH___")
- css = remove_unnecessary_whitespace(css)
- css = remove_unnecessary_semicolons(css)
- css = condense_zero_units(css)
- css = condense_multidimensional_zeros(css)
- css = condense_floating_points(css)
- css = normalize_rgb_colors_to_hex(css)
- css = condense_hex_colors(css)
- if wrap is not None:
- css = wrap_css_lines(css, wrap)
- css = css.replace("___PSEUDOCLASSBMH___", '"\\"}\\""')
- css = condense_semicolons(css)
- return css.strip()
-
-
-def main():
- import optparse
- import sys
-
- p = optparse.OptionParser(
- prog="cssmin", version=__version__,
- usage="%prog [--wrap N]",
- description="""Reads raw CSS from stdin, and writes compressed CSS to stdout.""")
-
- p.add_option(
- '-w', '--wrap', type='int', default=None, metavar='N',
- help="Wrap output to approximately N chars per line.")
-
- options, args = p.parse_args()
- sys.stdout.write(cssmin(sys.stdin.read(), wrap=options.wrap))
-
-
-if __name__ == '__main__':
- main()
+++ /dev/null
-#!/usr/bin/env python
-# -*- coding: ascii -*-
-#
-# Copyright 2011, 2012
-# Andr\xe9 Malo or his licensors, as applicable
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-r"""
-==============
- CSS Minifier
-==============
-
-CSS Minifier.
-
-The minifier is based on the semantics of the `YUI compressor`_\, which itself
-is based on `the rule list by Isaac Schlueter`_\.
-
-This module is a re-implementation aiming for speed instead of maximum
-compression, so it can be used at runtime (rather than during a preprocessing
-step). RCSSmin does syntactical compression only (removing spaces, comments
-and possibly semicolons). It does not provide semantic compression (like
-removing empty blocks, collapsing redundant properties etc). It does, however,
-support various CSS hacks (by keeping them working as intended).
-
-Here's a feature list:
-
-- Strings are kept, except that escaped newlines are stripped
-- Space/Comments before the very end or before various characters are
- stripped: ``:{});=>+],!`` (The colon (``:``) is a special case, a single
- space is kept if it's outside a ruleset.)
-- Space/Comments at the very beginning or after various characters are
- stripped: ``{}(=:>+[,!``
-- Optional space after unicode escapes is kept, resp. replaced by a simple
- space
-- whitespaces inside ``url()`` definitions are stripped
-- Comments starting with an exclamation mark (``!``) can be kept optionally.
-- All other comments and/or whitespace characters are replaced by a single
- space.
-- Multiple consecutive semicolons are reduced to one
-- The last semicolon within a ruleset is stripped
-- CSS Hacks supported:
-
- - IE7 hack (``>/**/``)
- - Mac-IE5 hack (``/*\*/.../**/``)
- - The boxmodelhack is supported naturally because it relies on valid CSS2
- strings
- - Between ``:first-line`` and the following comma or curly brace a space is
- inserted. (apparently it's needed for IE6)
- - Same for ``:first-letter``
-
-rcssmin.c is a reimplementation of rcssmin.py in C and improves runtime up to
-factor 50 or so (depending on the input).
-
-Both python 2 (>= 2.4) and python 3 are supported.
-
-.. _YUI compressor: https://github.com/yui/yuicompressor/
-
-.. _the rule list by Isaac Schlueter: https://github.com/isaacs/cssmin/tree/
-"""
-__author__ = "Andr\xe9 Malo"
-__author__ = getattr(__author__, 'decode', lambda x: __author__)('latin-1')
-__docformat__ = "restructuredtext en"
-__license__ = "Apache License, Version 2.0"
-__version__ = '1.0.2'
-__all__ = ['cssmin']
-
-import re as _re
-
-
-def _make_cssmin(python_only=False):
- """
- Generate CSS minifier.
-
- :Parameters:
- `python_only` : ``bool``
- Use only the python variant. If true, the c extension is not even
- tried to be loaded.
-
- :Return: Minifier
- :Rtype: ``callable``
- """
- # pylint: disable = W0612
- # ("unused" variables)
-
- # pylint: disable = R0911, R0912, R0914, R0915
- # (too many anything)
-
- if not python_only:
- try:
- import _rcssmin
- except ImportError:
- pass
- else:
- return _rcssmin.cssmin
-
- nl = r'(?:[\n\f]|\r\n?)' # pylint: disable = C0103
- spacechar = r'[\r\n\f\040\t]'
-
- unicoded = r'[0-9a-fA-F]{1,6}(?:[\040\n\t\f]|\r\n?)?'
- escaped = r'[^\n\r\f0-9a-fA-F]'
- escape = r'(?:\\(?:%(unicoded)s|%(escaped)s))' % locals()
-
- nmchar = r'[^\000-\054\056\057\072-\100\133-\136\140\173-\177]'
- # nmstart = r'[^\000-\100\133-\136\140\173-\177]'
- # ident = (r'(?:'
- # r'-?(?:%(nmstart)s|%(escape)s)%(nmchar)s*(?:%(escape)s%(nmchar)s*)*'
- # r')') % locals()
-
- comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
-
- # only for specific purposes. The bang is grouped:
- _bang_comment = r'(?:/\*(!?)[^*]*\*+(?:[^/*][^*]*\*+)*/)'
-
- string1 = \
- r'(?:\047[^\047\\\r\n\f]*(?:\\[^\r\n\f][^\047\\\r\n\f]*)*\047)'
- string2 = r'(?:"[^"\\\r\n\f]*(?:\\[^\r\n\f][^"\\\r\n\f]*)*")'
- strings = r'(?:%s|%s)' % (string1, string2)
-
- nl_string1 = \
- r'(?:\047[^\047\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^\047\\\r\n\f]*)*\047)'
- nl_string2 = r'(?:"[^"\\\r\n\f]*(?:\\(?:[^\r]|\r\n?)[^"\\\r\n\f]*)*")'
- nl_strings = r'(?:%s|%s)' % (nl_string1, nl_string2)
-
- uri_nl_string1 = r'(?:\047[^\047\\]*(?:\\(?:[^\r]|\r\n?)[^\047\\]*)*\047)'
- uri_nl_string2 = r'(?:"[^"\\]*(?:\\(?:[^\r]|\r\n?)[^"\\]*)*")'
- uri_nl_strings = r'(?:%s|%s)' % (uri_nl_string1, uri_nl_string2)
-
- nl_escaped = r'(?:\\%(nl)s)' % locals()
-
- space = r'(?:%(spacechar)s|%(comment)s)' % locals()
-
- ie7hack = r'(?:>/\*\*/)'
-
- uri = (r'(?:'
- r'(?:[^\000-\040"\047()\\\177]*'
- r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*)'
- r'(?:'
- r'(?:%(spacechar)s+|%(nl_escaped)s+)'
- r'(?:'
- r'(?:[^\000-\040"\047()\\\177]|%(escape)s|%(nl_escaped)s)'
- r'[^\000-\040"\047()\\\177]*'
- r'(?:%(escape)s[^\000-\040"\047()\\\177]*)*'
- r')+'
- r')*'
- r')') % locals()
-
- nl_unesc_sub = _re.compile(nl_escaped).sub
-
- uri_space_sub = _re.compile((
- r'(%(escape)s+)|%(spacechar)s+|%(nl_escaped)s+'
- ) % locals()).sub
- uri_space_subber = lambda m: m.groups()[0] or ''
-
- space_sub_simple = _re.compile((
- r'[\r\n\f\040\t;]+|(%(comment)s+)'
- ) % locals()).sub
- space_sub_banged = _re.compile((
- r'[\r\n\f\040\t;]+|(%(_bang_comment)s+)'
- ) % locals()).sub
-
- post_esc_sub = _re.compile(r'[\r\n\f\t]+').sub
-
- main_sub = _re.compile((
- r'([^\\"\047u>@\r\n\f\040\t/;:{}]+)'
- r'|(?<=[{}(=:>+[,!])(%(space)s+)'
- r'|^(%(space)s+)'
- r'|(%(space)s+)(?=(([:{});=>+\],!])|$)?)'
- r'|;(%(space)s*(?:;%(space)s*)*)(?=(\})?)'
- r'|(\{)'
- r'|(\})'
- r'|(%(strings)s)'
- r'|(?<!%(nmchar)s)url\(%(spacechar)s*('
- r'%(uri_nl_strings)s'
- r'|%(uri)s'
- r')%(spacechar)s*\)'
- r'|(@[mM][eE][dD][iI][aA])(?!%(nmchar)s)'
- r'|(%(ie7hack)s)(%(space)s*)'
- r'|(:[fF][iI][rR][sS][tT]-[lL]'
- r'(?:[iI][nN][eE]|[eE][tT][tT][eE][rR]))'
- r'(%(space)s*)(?=[{,])'
- r'|(%(nl_strings)s)'
- r'|(%(escape)s[^\\"\047u>@\r\n\f\040\t/;:{}]*)'
- ) % locals()).sub
-
- # print main_sub.__self__.pattern
-
- def main_subber(keep_bang_comments):
- """ Make main subber """
- in_macie5, in_rule, at_media = [0], [0], [0]
-
- if keep_bang_comments:
- space_sub = space_sub_banged
- def space_subber(match):
- """ Space|Comment subber """
- if match.lastindex:
- group1, group2 = match.group(1, 2)
- if group2:
- if group1.endswith(r'\*/'):
- in_macie5[0] = 1
- else:
- in_macie5[0] = 0
- return group1
- elif group1:
- if group1.endswith(r'\*/'):
- if in_macie5[0]:
- return ''
- in_macie5[0] = 1
- return r'/*\*/'
- elif in_macie5[0]:
- in_macie5[0] = 0
- return '/**/'
- return ''
- else:
- space_sub = space_sub_simple
- def space_subber(match):
- """ Space|Comment subber """
- if match.lastindex:
- if match.group(1).endswith(r'\*/'):
- if in_macie5[0]:
- return ''
- in_macie5[0] = 1
- return r'/*\*/'
- elif in_macie5[0]:
- in_macie5[0] = 0
- return '/**/'
- return ''
-
- def fn_space_post(group):
- """ space with token after """
- if group(5) is None or (
- group(6) == ':' and not in_rule[0] and not at_media[0]):
- return ' ' + space_sub(space_subber, group(4))
- return space_sub(space_subber, group(4))
-
- def fn_semicolon(group):
- """ ; handler """
- return ';' + space_sub(space_subber, group(7))
-
- def fn_semicolon2(group):
- """ ; handler """
- if in_rule[0]:
- return space_sub(space_subber, group(7))
- return ';' + space_sub(space_subber, group(7))
-
- def fn_open(group):
- """ { handler """
- # pylint: disable = W0613
- if at_media[0]:
- at_media[0] -= 1
- else:
- in_rule[0] = 1
- return '{'
-
- def fn_close(group):
- """ } handler """
- # pylint: disable = W0613
- in_rule[0] = 0
- return '}'
-
- def fn_media(group):
- """ @media handler """
- at_media[0] += 1
- return group(13)
-
- def fn_ie7hack(group):
- """ IE7 Hack handler """
- if not in_rule[0] and not at_media[0]:
- in_macie5[0] = 0
- return group(14) + space_sub(space_subber, group(15))
- return '>' + space_sub(space_subber, group(15))
-
- table = (
- None,
- None,
- None,
- None,
- fn_space_post, # space with token after
- fn_space_post, # space with token after
- fn_space_post, # space with token after
- fn_semicolon, # semicolon
- fn_semicolon2, # semicolon
- fn_open, # {
- fn_close, # }
- lambda g: g(11), # string
- lambda g: 'url(%s)' % uri_space_sub(uri_space_subber, g(12)),
- # url(...)
- fn_media, # @media
- None,
- fn_ie7hack, # ie7hack
- None,
- lambda g: g(16) + ' ' + space_sub(space_subber, g(17)),
- # :first-line|letter followed
- # by [{,] (apparently space
- # needed for IE6)
- lambda g: nl_unesc_sub('', g(18)), # nl_string
- lambda g: post_esc_sub(' ', g(19)), # escape
- )
-
- def func(match):
- """ Main subber """
- idx, group = match.lastindex, match.group
- if idx > 3:
- return table[idx](group)
-
- # shortcuts for frequent operations below:
- elif idx == 1: # not interesting
- return group(1)
- # else: # space with token before or at the beginning
- return space_sub(space_subber, group(idx))
-
- return func
-
- def cssmin(style, keep_bang_comments=False): # pylint: disable = W0621
- """
- Minify CSS.
-
- :Parameters:
- `style` : ``str``
- CSS to minify
-
- `keep_bang_comments` : ``bool``
- Keep comments starting with an exclamation mark? (``/*!...*/``)
-
- :Return: Minified style
- :Rtype: ``str``
- """
- return main_sub(main_subber(keep_bang_comments), style)
-
- return cssmin
-
-cssmin = _make_cssmin()
-
-
-if __name__ == '__main__':
- def main():
- """ Main """
- import sys as _sys
- keep_bang_comments = (
- '-b' in _sys.argv[1:]
- or '-bp' in _sys.argv[1:]
- or '-pb' in _sys.argv[1:]
- )
- if '-p' in _sys.argv[1:] or '-bp' in _sys.argv[1:] \
- or '-pb' in _sys.argv[1:]:
- global cssmin # pylint: disable = W0603
- cssmin = _make_cssmin(python_only=True)
- _sys.stdout.write(cssmin(
- _sys.stdin.read(), keep_bang_comments=keep_bang_comments
- ))
- main()
+++ /dev/null
-from compressor.conf import settings
-from compressor.filters import CompilerFilter
-
-
-class CSSTidyFilter(CompilerFilter):
- command = "{binary} {infile} {args} {outfile}"
- options = (
- ("binary", settings.COMPRESS_CSSTIDY_BINARY),
- ("args", settings.COMPRESS_CSSTIDY_ARGUMENTS),
- )
from __future__ import absolute_import
from compressor.filters import CallbackOutputFilter
-from compressor.filters.jsmin.slimit import SlimItFilter # noqa
class rJSMinFilter(CallbackOutputFilter):
- callback = "compressor.filters.jsmin.rjsmin.jsmin"
+ callback = "rjsmin.jsmin"
+ dependencies = ["rjsmin"]
+ kwargs = {
+ "keep_bang_comments": True
+ }
# This is for backwards compatibility
JSMinFilter = rJSMinFilter
+
+
+class SlimItFilter(CallbackOutputFilter):
+ dependencies = ["slimit"]
+ callback = "slimit.minify"
+ kwargs = {
+ "mangle": True,
+ }
+++ /dev/null
-#!/usr/bin/env python
-# -*- coding: ascii -*-
-#
-# Copyright 2011 - 2013
-# Andr\xe9 Malo or his licensors, as applicable
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-r"""
-=====================
- Javascript Minifier
-=====================
-
-rJSmin is a javascript minifier written in python.
-
-The minifier is based on the semantics of `jsmin.c by Douglas Crockford`_\.
-
-The module is a re-implementation aiming for speed, so it can be used at
-runtime (rather than during a preprocessing step). Usually it produces the
-same results as the original ``jsmin.c``. It differs in the following ways:
-
-- there is no error detection: unterminated string, regex and comment
- literals are treated as regular javascript code and minified as such.
-- Control characters inside string and regex literals are left untouched; they
- are not converted to spaces (nor to \n)
-- Newline characters are not allowed inside string and regex literals, except
- for line continuations in string literals (ECMA-5).
-- "return /regex/" is recognized correctly.
-- "+ +" and "- -" sequences are not collapsed to '++' or '--'
-- Newlines before ! operators are removed more sensibly
-- rJSmin does not handle streams, but only complete strings. (However, the
- module provides a "streamy" interface).
-
-Since most parts of the logic are handled by the regex engine it's way
-faster than the original python port of ``jsmin.c`` by Baruch Even. The speed
-factor varies between about 6 and 55 depending on input and python version
-(it gets faster the more compressed the input already is). Compared to the
-speed-refactored python port by Dave St.Germain the performance gain is less
-dramatic but still between 1.2 and 7. See the docs/BENCHMARKS file for
-details.
-
-rjsmin.c is a reimplementation of rjsmin.py in C and speeds it up even more.
-
-Both python 2 and python 3 are supported.
-
-.. _jsmin.c by Douglas Crockford:
- http://www.crockford.com/javascript/jsmin.c
-"""
-__author__ = "Andr\xe9 Malo"
-__author__ = getattr(__author__, 'decode', lambda x: __author__)('latin-1')
-__docformat__ = "restructuredtext en"
-__license__ = "Apache License, Version 2.0"
-__version__ = '1.0.7'
-__all__ = ['jsmin']
-
-import re as _re
-
-
-def _make_jsmin(python_only=False):
- """
- Generate JS minifier based on `jsmin.c by Douglas Crockford`_
-
- .. _jsmin.c by Douglas Crockford:
- http://www.crockford.com/javascript/jsmin.c
-
- :Parameters:
- `python_only` : ``bool``
- Use only the python variant. If true, the c extension is not even
- tried to be loaded.
-
- :Return: Minifier
- :Rtype: ``callable``
- """
- # pylint: disable = R0912, R0914, W0612
- if not python_only:
- try:
- import _rjsmin
- except ImportError:
- pass
- else:
- return _rjsmin.jsmin
- try:
- xrange
- except NameError:
- xrange = range # pylint: disable = W0622
-
- space_chars = r'[\000-\011\013\014\016-\040]'
-
- line_comment = r'(?://[^\r\n]*)'
- space_comment = r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)'
- string1 = \
- r'(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|\r)[^\047\\\r\n]*)*\047)'
- string2 = r'(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|\r)[^"\\\r\n]*)*")'
- strings = r'(?:%s|%s)' % (string1, string2)
-
- charclass = r'(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\])'
- nospecial = r'[^/\\\[\r\n]'
- regex = r'(?:/(?![\r\n/*])%s*(?:(?:\\[^\r\n]|%s)%s*)*/)' % (
- nospecial, charclass, nospecial
- )
- space = r'(?:%s|%s)' % (space_chars, space_comment)
- newline = r'(?:%s?[\r\n])' % line_comment
-
- def fix_charclass(result):
- """ Fixup string of chars to fit into a regex char class """
- pos = result.find('-')
- if pos >= 0:
- result = r'%s%s-' % (result[:pos], result[pos + 1:])
-
- def sequentize(string):
- """
- Notate consecutive characters as sequence
-
- (1-4 instead of 1234)
- """
- first, last, result = None, None, []
- for char in map(ord, string):
- if last is None:
- first = last = char
- elif last + 1 == char:
- last = char
- else:
- result.append((first, last))
- first = last = char
- if last is not None:
- result.append((first, last))
- return ''.join(['%s%s%s' % (
- chr(first2),
- last2 > first2 + 1 and '-' or '',
- last2 != first2 and chr(last2) or ''
- ) for first2, last2 in result])
-
- return _re.sub(r'([\000-\040\047])', # for better portability
- lambda m: '\\%03o' % ord(m.group(1)), (sequentize(result)
- .replace('\\', '\\\\')
- .replace('[', '\\[')
- .replace(']', '\\]')
- )
- )
-
- def id_literal_(what):
- """ Make id_literal like char class """
- match = _re.compile(what).match
- result = ''.join([
- chr(c) for c in xrange(127) if not match(chr(c))
- ])
- return '[^%s]' % fix_charclass(result)
-
- def not_id_literal_(keep):
- """ Make negated id_literal like char class """
- match = _re.compile(id_literal_(keep)).match
- result = ''.join([
- chr(c) for c in xrange(127) if not match(chr(c))
- ])
- return r'[%s]' % fix_charclass(result)
-
- not_id_literal = not_id_literal_(r'[a-zA-Z0-9_$]')
- preregex1 = r'[(,=:\[!&|?{};\r\n]'
- preregex2 = r'%(not_id_literal)sreturn' % locals()
-
- id_literal = id_literal_(r'[a-zA-Z0-9_$]')
- id_literal_open = id_literal_(r'[a-zA-Z0-9_${\[(!+-]')
- id_literal_close = id_literal_(r'[a-zA-Z0-9_$}\])"\047+-]')
-
- dull = r'[^\047"/\000-\040]'
-
- space_sub = _re.compile((
- r'(%(dull)s+)'
- r'|(%(strings)s%(dull)s*)'
- r'|(?<=%(preregex1)s)'
- r'%(space)s*(?:%(newline)s%(space)s*)*'
- r'(%(regex)s%(dull)s*)'
- r'|(?<=%(preregex2)s)'
- r'%(space)s*(?:%(newline)s%(space)s)*'
- r'(%(regex)s%(dull)s*)'
- r'|(?<=%(id_literal_close)s)'
- r'%(space)s*(?:(%(newline)s)%(space)s*)+'
- r'(?=%(id_literal_open)s)'
- r'|(?<=%(id_literal)s)(%(space)s)+(?=%(id_literal)s)'
- r'|(?<=\+)(%(space)s)+(?=\+)'
- r'|(?<=-)(%(space)s)+(?=-)'
- r'|%(space)s+'
- r'|(?:%(newline)s%(space)s*)+'
- ) % locals()).sub
- # print space_sub.__self__.pattern
-
- def space_subber(match):
- """ Substitution callback """
- # pylint: disable = C0321, R0911
- groups = match.groups()
- if groups[0]: return groups[0]
- elif groups[1]: return groups[1]
- elif groups[2]: return groups[2]
- elif groups[3]: return groups[3]
- elif groups[4]: return '\n'
- elif groups[5] or groups[6] or groups[7]: return ' '
- else: return ''
-
- def jsmin(script): # pylint: disable = W0621
- r"""
- Minify javascript based on `jsmin.c by Douglas Crockford`_\.
-
- Instead of parsing the stream char by char, it uses a regular
- expression approach which minifies the whole script with one big
- substitution regex.
-
- .. _jsmin.c by Douglas Crockford:
- http://www.crockford.com/javascript/jsmin.c
-
- :Parameters:
- `script` : ``str``
- Script to minify
-
- :Return: Minified script
- :Rtype: ``str``
- """
- return space_sub(space_subber, '\n%s\n' % script).strip()
-
- return jsmin
-
-jsmin = _make_jsmin()
-
-
-def jsmin_for_posers(script):
- r"""
- Minify javascript based on `jsmin.c by Douglas Crockford`_\.
-
- Instead of parsing the stream char by char, it uses a regular
- expression approach which minifies the whole script with one big
- substitution regex.
-
- .. _jsmin.c by Douglas Crockford:
- http://www.crockford.com/javascript/jsmin.c
-
- :Warning: This function is the digest of a _make_jsmin() call. It just
- utilizes the resulting regex. It's just for fun here and may
- vanish any time. Use the `jsmin` function instead.
-
- :Parameters:
- `script` : ``str``
- Script to minify
-
- :Return: Minified script
- :Rtype: ``str``
- """
- def subber(match):
- """ Substitution callback """
- groups = match.groups()
- return (
- groups[0] or
- groups[1] or
- groups[2] or
- groups[3] or
- (groups[4] and '\n') or
- (groups[5] and ' ') or
- (groups[6] and ' ') or
- (groups[7] and ' ') or
- ''
- )
-
- return _re.sub(
- r'([^\047"/\000-\040]+)|((?:(?:\047[^\047\\\r\n]*(?:\\(?:[^\r\n]|\r?'
- r'\n|\r)[^\047\\\r\n]*)*\047)|(?:"[^"\\\r\n]*(?:\\(?:[^\r\n]|\r?\n|'
- r'\r)[^"\\\r\n]*)*"))[^\047"/\000-\040]*)|(?<=[(,=:\[!&|?{};\r\n])(?'
- r':[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*'
- r'(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*'
- r'[^*]*\*+(?:[^/*][^*]*\*+)*/))*)*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?:('
- r'?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\['
- r'\r\n]*)*/)[^\047"/\000-\040]*)|(?<=[\000-#%-,./:-@\[-^`{-~-]return'
- r')(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/'
- r'))*(?:(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:'
- r'/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))*((?:/(?![\r\n/*])[^/\\\[\r\n]*(?'
- r':(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/'
- r'\\\[\r\n]*)*/)[^\047"/\000-\040]*)|(?<=[^\000-!#%&(*,./:-@\[\\^`{|'
- r'~])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)'
- r'*/))*(?:((?:(?://[^\r\n]*)?[\r\n]))(?:[\000-\011\013\014\016-\040]'
- r'|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#%-\047)*,./'
- r':-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-\011\013\01'
- r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^\000-#%-,./:'
- r'-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*'
- r'\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\013\014\016-'
- r'\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\000-\011\013'
- r'\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:(?:(?://[^'
- r'\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^'
- r'/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
- ).strip()
-
-
-if __name__ == '__main__':
- import sys as _sys
- _sys.stdout.write(jsmin(_sys.stdin.read()))
+++ /dev/null
-from __future__ import absolute_import
-from compressor.filters import CallbackOutputFilter
-
-
-class SlimItFilter(CallbackOutputFilter):
- dependencies = ["slimit"]
- callback = "slimit.minify"
- kwargs = {
- "mangle": True,
- }
class JsCompressor(Compressor):
def __init__(self, content=None, output_prefix="js", context=None):
- super(JsCompressor, self).__init__(content, output_prefix, context)
- self.filters = list(settings.COMPRESS_JS_FILTERS)
- self.type = output_prefix
+ filters = list(settings.COMPRESS_JS_FILTERS)
+ super(JsCompressor, self).__init__(content, output_prefix, context, filters)
def split_contents(self):
if self.split_content:
import os
import sys
+from collections import OrderedDict
from fnmatch import fnmatch
from optparse import make_option
+from importlib import import_module
import django
-from django.core.management.base import NoArgsCommand, CommandError
+from django.core.management.base import BaseCommand, CommandError
import django.template
from django.template import Context
from django.utils import six
-from django.utils.datastructures import SortedDict
-from django.utils.importlib import import_module
from django.template.loader import get_template # noqa Leave this in to preload template locations
+from django.template.utils import InvalidTemplateEngineError
+from django.template import engines
from compressor.cache import get_offline_hexdigest, write_offline_manifest
from compressor.conf import settings
from compressor.exceptions import (OfflineGenerationError, TemplateSyntaxError,
TemplateDoesNotExist)
from compressor.templatetags.compress import CompressorNode
+from compressor.utils import get_mod_func
if six.PY3:
# there is an 'io' module in python 2.6+, but io.StringIO does not
from StringIO import StringIO
-class Command(NoArgsCommand):
+class Command(BaseCommand):
help = "Compress content outside of the request/response cycle"
- option_list = NoArgsCommand.option_list + (
+ option_list = BaseCommand.option_list + (
make_option('--extension', '-e', action='append', dest='extensions',
help='The file extension(s) to examine (default: ".html", '
'separate multiple extensions with commas, or use -e '
dest="engine"),
)
- requires_model_validation = False
-
def get_loaders(self):
- 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))
+ template_source_loaders = []
+ for e in engines.all():
+ if hasattr(e, 'engine'):
+ 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
The result is cached with a cache-key derived from the content of the
compress nodes (not the content of the possibly linked files!).
"""
+ engine = options.get("engine", "django")
extensions = options.get('extensions')
extensions = self.handle_extensions(extensions or ['html'])
verbosity = int(options.get("verbosity", 0))
raise OfflineGenerationError("No template loaders defined. You "
"must set TEMPLATE_LOADERS in your "
"settings.")
- paths = set()
- for loader in self.get_loaders():
- try:
- module = import_module(loader.__module__)
- get_template_sources = getattr(module,
- 'get_template_sources', None)
- if get_template_sources is None:
- get_template_sources = loader.get_template_sources
- paths.update(list(get_template_sources('')))
- except (ImportError, AttributeError, TypeError):
- # Yeah, this didn't work out so well, let's move on
- pass
- if not paths:
- raise OfflineGenerationError("No template paths found. None of "
- "the configured template loaders "
- "provided template paths. See "
- "http://django.me/template-loaders "
- "for more information on template "
- "loaders.")
- if verbosity > 1:
- log.write("Considering paths:\n\t" + "\n\t".join(paths) + "\n")
templates = set()
- for path in paths:
- for root, dirs, files in os.walk(path,
- followlinks=options.get('followlinks', False)):
- templates.update(os.path.join(root, name)
- for name in files if not name.startswith('.') and
- any(fnmatch(name, "*%s" % glob) for glob in extensions))
+ if engine == 'django':
+ paths = set()
+ for loader in self.get_loaders():
+ try:
+ module = import_module(loader.__module__)
+ get_template_sources = getattr(module,
+ 'get_template_sources', None)
+ if get_template_sources is None:
+ get_template_sources = loader.get_template_sources
+ paths.update(str(origin) for origin in get_template_sources(''))
+ except (ImportError, AttributeError, TypeError):
+ # Yeah, this didn't work out so well, let's move on
+ pass
+
+ if not paths:
+ raise OfflineGenerationError("No template paths found. None of "
+ "the configured template loaders "
+ "provided template paths. See "
+ "https://docs.djangoproject.com/en/1.8/topics/templates/ "
+ "for more information on template "
+ "loaders.")
+ if verbosity > 1:
+ log.write("Considering paths:\n\t" + "\n\t".join(paths) + "\n")
+
+ for path in paths:
+ for root, dirs, files in os.walk(path,
+ followlinks=options.get('followlinks', False)):
+ templates.update(os.path.join(root, name)
+ for name in files if not name.startswith('.') and
+ any(fnmatch(name, "*%s" % glob) for glob in extensions))
+ elif engine == 'jinja2' and django.VERSION >= (1, 8):
+ env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT()
+ if env and hasattr(env, 'list_templates'):
+ templates |= set([env.loader.get_source(env, template)[1] for template in
+ env.list_templates(filter_func=lambda _path:
+ os.path.splitext(_path)[-1] in extensions)])
+
if not templates:
raise OfflineGenerationError("No templates found. Make sure your "
"TEMPLATE_LOADERS and TEMPLATE_DIRS "
if verbosity > 1:
log.write("Found templates:\n\t" + "\n\t".join(templates) + "\n")
- engine = options.get("engine", "django")
parser = self.__get_parser(engine)
-
- compressor_nodes = SortedDict()
+ compressor_nodes = OrderedDict()
for template_name in templates:
try:
template = parser.parse(template_name)
"\n\t".join((t.template_name
for t in compressor_nodes.keys())) + "\n")
+ contexts = settings.COMPRESS_OFFLINE_CONTEXT
+ if isinstance(contexts, six.string_types):
+ try:
+ module, function = get_mod_func(contexts)
+ contexts = getattr(import_module(module), function)()
+ except (AttributeError, ImportError, TypeError) as e:
+ raise ImportError("Couldn't import offline context function %s: %s" %
+ (settings.COMPRESS_OFFLINE_CONTEXT, e))
+ elif not isinstance(contexts, (list, tuple)):
+ contexts = [contexts]
+
log.write("Compressing... ")
- count = 0
+ block_count = context_count = 0
results = []
- offline_manifest = SortedDict()
- init_context = parser.get_init_context(settings.COMPRESS_OFFLINE_CONTEXT)
-
- for template, nodes in compressor_nodes.items():
- context = Context(init_context)
- template._log = log
- template._log_verbosity = verbosity
+ offline_manifest = OrderedDict()
- if not parser.process_template(template, context):
- continue
+ for context_dict in contexts:
+ context_count += 1
+ init_context = parser.get_init_context(context_dict)
- for node in nodes:
- context.push()
- parser.process_node(template, context, node)
- rendered = parser.render_nodelist(template, context, node)
- key = get_offline_hexdigest(rendered)
+ for template, nodes in compressor_nodes.items():
+ context = Context(init_context)
+ template._log = log
+ template._log_verbosity = verbosity
- if key in offline_manifest:
+ if not parser.process_template(template, context):
continue
- try:
- result = parser.render_node(template, context, node)
- except Exception as e:
- raise CommandError("An error occured during rendering %s: "
- "%s" % (template.template_name, e))
- offline_manifest[key] = result
- context.pop()
- results.append(result)
- count += 1
+ for node in nodes:
+ context.push()
+ parser.process_node(template, context, node)
+ rendered = parser.render_nodelist(template, context, node)
+ key = get_offline_hexdigest(rendered)
+
+ if key in offline_manifest:
+ continue
+
+ try:
+ result = parser.render_node(template, context, node)
+ except Exception as e:
+ raise CommandError("An error occurred during rendering %s: "
+ "%s" % (template.template_name, e))
+ offline_manifest[key] = result
+ context.pop()
+ results.append(result)
+ block_count += 1
write_offline_manifest(offline_manifest)
- log.write("done\nCompressed %d block(s) from %d template(s).\n" %
- (count, len(compressor_nodes)))
- return count, results
+ log.write("done\nCompressed %d block(s) from %d template(s) for %d context(s).\n" %
+ (block_count, len(compressor_nodes), context_count))
+ return block_count, results
def handle_extensions(self, extensions=('html',)):
"""
passed by using --extension/-e multiple times.
for example: running 'django-admin compress -e js,txt -e xhtml -a'
- would result in a extension list: ['.js', '.txt', '.xhtml']
+ would result in an extension list: ['.js', '.txt', '.xhtml']
>>> handle_extensions(['.html', 'html,js,py,py,py,.py', 'py,.py'])
['.html', '.js']
ext_list[i] = '.%s' % ext_list[i]
return set(ext_list)
- def handle_noargs(self, **options):
+ def handle(self, **options):
if not settings.COMPRESS_ENABLED and not options.get("force"):
raise CommandError(
"Compressor is disabled. Set the COMPRESS_ENABLED "
"Offline compression is disabled. Set "
"COMPRESS_OFFLINE or use the --force to override.")
self.compress(sys.stdout, **options)
+
+
+
+Command.requires_system_checks = False
import fnmatch
import os
-from optparse import make_option
-from django.core.management.base import NoArgsCommand, CommandError
+from django.core.management.base import BaseCommand, CommandError
from compressor.conf import settings
from compressor.cache import cache, get_mtime, get_mtime_cachekey
-class Command(NoArgsCommand):
+class Command(BaseCommand):
help = "Add or remove all mtime values from the cache"
- option_list = NoArgsCommand.option_list + (
- make_option('-i', '--ignore', action='append', default=[],
+
+ def add_arguments(self, parser):
+ parser.add_argument(
+ '-i', '--ignore', action='append', default=[],
dest='ignore_patterns', metavar='PATTERN',
help="Ignore files or directories matching this glob-style "
- "pattern. Use multiple times to ignore more."),
- make_option('--no-default-ignore', action='store_false',
+ "pattern. Use multiple times to ignore more."),
+ parser.add_argument(
+ '--no-default-ignore', action='store_false',
dest='use_default_ignore_patterns', default=True,
help="Don't ignore the common private glob-style patterns 'CVS', "
- "'.*' and '*~'."),
- make_option('--follow-links', dest='follow_links', action='store_true',
+ "'.*' and '*~'."),
+ parser.add_argument(
+ '--follow-links', dest='follow_links', action='store_true',
help="Follow symlinks when traversing the COMPRESS_ROOT "
- "(which defaults to STATIC_ROOT). Be aware that using this "
- "can lead to infinite recursion if a link points to a parent "
- "directory of itself."),
- make_option('-c', '--clean', dest='clean', action='store_true',
+ "(which defaults to STATIC_ROOT). Be aware that using this "
+ "can lead to infinite recursion if a link points to a parent "
+ "directory of itself."),
+ parser.add_argument(
+ '-c', '--clean', dest='clean', action='store_true',
help="Remove all items"),
- make_option('-a', '--add', dest='add', action='store_true',
+ parser.add_argument(
+ '-a', '--add', dest='add', action='store_true',
help="Add all items"),
- )
def is_ignored(self, path):
"""
return True
return False
- def handle_noargs(self, **options):
+ def handle(self, **options):
ignore_patterns = options['ignore_patterns']
if options['use_default_ignore_patterns']:
ignore_patterns += ['CVS', '.*', '*~']
options['ignore_patterns'] = ignore_patterns
self.ignore_patterns = ignore_patterns
- if (options['add'] and options['clean']) or (not options['add'] and not options['clean']):
+ if ((options['add'] and options['clean']) or
+ (not options['add'] and not options['clean'])):
raise CommandError('Please specify either "--add" or "--clean"')
if not settings.COMPRESS_MTIME_DELAY:
- raise CommandError('mtime caching is currently disabled. Please '
+ raise CommandError(
+ 'mtime caching is currently disabled. Please '
'set the COMPRESS_MTIME_DELAY setting to a number of seconds.')
files_to_add = set()
keys_to_delete = set()
- for root, dirs, files in os.walk(settings.COMPRESS_ROOT, followlinks=options['follow_links']):
+ for root, dirs, files in os.walk(settings.COMPRESS_ROOT,
+ followlinks=options['follow_links']):
for dir_ in dirs:
if self.is_ignored(dir_):
dirs.remove(dir_)
if keys_to_delete:
cache.delete_many(list(keys_to_delete))
- print("Deleted mtimes of %d files from the cache." % len(keys_to_delete))
+ self.stdout.write("Deleted mtimes of %d files from the cache."
+ % len(keys_to_delete))
if files_to_add:
for filename in files_to_add:
get_mtime(filename)
- print("Added mtimes of %d files to cache." % len(files_to_add))
+ self.stdout.write("Added mtimes of %d files to cache."
+ % len(files_to_add))
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
from django.template.base import Node, VariableNode, TextNode, NodeList
from django.template.defaulttags import IfNode
extendsnode.nodelist.get_nodes_by_type(BlockNode))
block_context.add_blocks(blocks)
- context = Context(settings.COMPRESS_OFFLINE_CONTEXT)
+ # Note: we pass an empty context when we find the parent, this breaks
+ # inheritance using variables ({% extends template_var %}) but a refactor
+ # will be needed to support that use-case with multiple offline contexts.
+ context = Context()
if original is not None:
context.template = original
def parse(self, template_name):
try:
- if django.VERSION < (1, 8):
- return get_template(template_name)
- else:
- return get_template(template_name).template
+ return get_template(template_name).template
except template.TemplateSyntaxError as e:
raise TemplateSyntaxError(str(e))
except template.TemplateDoesNotExist as e:
pass
def render_nodelist(self, template, context, node):
- if django.VERSION >= (1, 8):
- context.template = template
+ context.template = template
return node.nodelist.render(context)
def render_node(self, template, context, node):
return nodelist
def walk_nodes(self, node, original=None):
- if django.VERSION >= (1, 8) and original is None:
+ if 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):
+from importlib import import_module
+
from django.utils import six
from django.utils.functional import LazyObject
-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 __future__ import absolute_import
from django.core.exceptions import ImproperlyConfigured
-from django.utils import six
from django.utils.encoding import smart_text
-from compressor.exceptions import ParserError
from compressor.parser import ParserBase
-from compressor.utils.decorators import cached_property
class BeautifulSoupParser(ParserBase):
- @cached_property
- def soup(self):
+ def __init__(self, content):
+ super(BeautifulSoupParser, self).__init__(content)
try:
- if six.PY3:
- from bs4 import BeautifulSoup
- else:
- from BeautifulSoup import BeautifulSoup
- return BeautifulSoup(self.content)
+ from bs4 import BeautifulSoup
+ self.soup = BeautifulSoup(self.content, "html.parser")
except ImportError as err:
raise ImproperlyConfigured("Error while importing BeautifulSoup: %s" % err)
- except Exception as err:
- raise ParserError("Error while initializing Parser: %s" % err)
def css_elems(self):
- if six.PY3:
- return self.soup.find_all({'link': True, 'style': True})
- else:
- return self.soup.findAll({'link': True, 'style': True})
+ return self.soup.find_all({'link': True, 'style': True})
def js_elems(self):
- if six.PY3:
- return self.soup.find_all('script')
- else:
- return self.soup.findAll('script')
+ return self.soup.find_all('script')
def elem_attribs(self, elem):
- return dict(elem.attrs)
+ attrs = dict(elem.attrs)
+ # hack around changed behaviour in bs4, it returns lists now instead of one string, see
+ # http://www.crummy.com/software/BeautifulSoup/bs4/doc/#multi-valued-attributes
+ for key, value in attrs.items():
+ if type(value) is list:
+ attrs[key] = " ".join(value)
+ return attrs
def elem_content(self, elem):
return elem.string
+import sys
+
from django.utils import six
from django.utils.encoding import smart_text
from compressor.parser import ParserBase
+# Starting in Python 3.2, the HTMLParser constructor takes a 'strict'
+# argument which default to True (which we don't want).
+# In Python 3.3, it defaults to False.
+# In Python 3.4, passing it at all raises a deprecation warning.
+# So we only pass it for 3.2.
+# In Python 3.4, it also takes a 'convert_charrefs' argument
+# which raises a warning if we don't pass it.
+major, minor, release = sys.version_info[:3]
+CONSTRUCTOR_TAKES_STRICT = major == 3 and minor == 2
+CONSTRUCTOR_TAKES_CONVERT_CHARREFS = major == 3 and minor >= 4
+HTML_PARSER_ARGS = {}
+if CONSTRUCTOR_TAKES_STRICT:
+ HTML_PARSER_ARGS['strict'] = False
+if CONSTRUCTOR_TAKES_CONVERT_CHARREFS:
+ HTML_PARSER_ARGS['convert_charrefs'] = False
+
+
class DefaultHtmlParser(ParserBase, six.moves.html_parser.HTMLParser):
def __init__(self, content):
- six.moves.html_parser.HTMLParser.__init__(self)
+ six.moves.html_parser.HTMLParser.__init__(self, **HTML_PARSER_ARGS)
self.content = content
self._css_elems = []
self._js_elems = []
def modified_time(self, name):
return datetime.fromtimestamp(os.path.getmtime(self.path(name)))
- def get_available_name(self, name):
+ def get_available_name(self, name, max_length=None):
"""
Deletes the given file if it exists.
"""
def debug_mode(self, context):
if settings.COMPRESS_DEBUG_TOGGLE:
- # Only check for the debug parameter
- # if a RequestContext was used
+ # Only check for the debug parameter if a RequestContext was used
request = context.get('request', None)
if request is not None:
return settings.COMPRESS_DEBUG_TOGGLE in request.GET
return (settings.COMPRESS_ENABLED and
settings.COMPRESS_OFFLINE) or forced
- def render_offline(self, context, forced):
+ def render_offline(self, context):
"""
If enabled and in offline mode, and not forced check the offline cache
and return the result if given
"""
- if self.is_offline_compression_enabled(forced) and not forced:
- key = get_offline_hexdigest(self.get_original_content(context))
- offline_manifest = get_offline_manifest()
- if key in offline_manifest:
- return offline_manifest[key]
- else:
- raise OfflineGenerationError('You have offline compression '
- 'enabled but key "%s" is missing from offline manifest. '
- 'You may need to run "python manage.py compress".' % key)
-
- def render_cached(self, compressor, kind, mode, forced=False):
+ key = get_offline_hexdigest(self.get_original_content(context))
+ offline_manifest = get_offline_manifest()
+ if key in offline_manifest:
+ return offline_manifest[key]
+ else:
+ raise OfflineGenerationError('You have offline compression '
+ 'enabled but key "%s" is missing from offline manifest. '
+ 'You may need to run "python manage.py compress".' % key)
+
+ def render_cached(self, compressor, kind, mode):
"""
If enabled checks the cache for the given compressor's cache key
and return a tuple of cache key and output
"""
- if settings.COMPRESS_ENABLED and not forced:
- cache_key = get_templatetag_cachekey(compressor, mode, kind)
- cache_content = cache_get(cache_key)
- return cache_key, cache_content
- return None, None
+ cache_key = get_templatetag_cachekey(compressor, mode, kind)
+ cache_content = cache_get(cache_key)
+ return cache_key, cache_content
def render_compressed(self, context, kind, mode, forced=False):
# See if it has been rendered offline
- cached_offline = self.render_offline(context, forced=forced)
- if cached_offline:
- return cached_offline
+ if self.is_offline_compression_enabled(forced) and not forced:
+ return self.render_offline(context)
# Take a shortcut if we really don't have anything to do
- if ((not settings.COMPRESS_ENABLED and
- not settings.COMPRESS_PRECOMPILERS) and not forced):
+ if (not settings.COMPRESS_ENABLED and
+ not settings.COMPRESS_PRECOMPILERS and not forced):
return self.get_original_content(context)
context['compressed'] = {'name': getattr(self, 'name', None)}
compressor = self.get_compressor(context, kind)
- # Prepare the actual compressor and check cache
- cache_key, cache_content = self.render_cached(compressor, kind, mode, forced=forced)
- if cache_content is not None:
- return cache_content
-
- # call compressor output method and handle exceptions
- try:
- rendered_output = self.render_output(compressor, mode, forced=forced)
- if cache_key:
- cache_set(cache_key, rendered_output)
- assert isinstance(rendered_output, six.string_types)
- return rendered_output
- except Exception:
- if settings.DEBUG or forced:
- raise
-
- # Or don't do anything in production
- return self.get_original_content(context)
+ # Check cache
+ cache_key = None
+ if settings.COMPRESS_ENABLED and not forced:
+ cache_key, cache_content = self.render_cached(compressor, kind, mode)
+ if cache_content is not None:
+ return cache_content
- def render_output(self, compressor, mode, forced=False):
- return compressor.output(mode, forced=forced)
+ rendered_output = compressor.output(mode, forced=forced)
+ assert isinstance(rendered_output, six.string_types)
+ if cache_key:
+ cache_set(cache_key, rendered_output)
+ return rendered_output
class CompressorNode(CompressorMixin, template.Node):
def get_original_content(self, context):
return self.nodelist.render(context)
- def debug_mode(self, context):
- if settings.COMPRESS_DEBUG_TOGGLE:
- # Only check for the debug parameter
- # if a RequestContext was used
- request = context.get('request', None)
- if request is not None:
- return settings.COMPRESS_DEBUG_TOGGLE in request.GET
-
def render(self, context, forced=False):
# Check if in debug mode
import os
-import django
TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'tests')
INSTALLED_APPS = [
'django.contrib.staticfiles',
'compressor',
- 'coffin',
+ 'sekizai',
]
-if django.VERSION < (1, 8):
- INSTALLED_APPS.append('jingo')
STATICFILES_FINDERS = [
'django.contrib.staticfiles.finders.FileSystemFinder',
STATIC_ROOT = os.path.join(TEST_DIR, 'static')
-TEMPLATE_DIRS = (
- # Specifically choose a name that will not be considered
- # by app_directories loader, to make sure each test uses
- # a specific template without considering the others.
- os.path.join(TEST_DIR, 'test_templates'),
-)
-
-if django.VERSION[:2] < (1, 6):
- TEST_RUNNER = 'discover_runner.DiscoverRunner'
+TEMPLATES = [{
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'APP_DIRS': True,
+ 'DIRS': [
+ # Specifically choose a name that will not be considered
+ # by app_directories loader, to make sure each test uses
+ # a specific template without considering the others.
+ os.path.join(TEST_DIR, 'test_templates'),
+ ],
+}, {
+ 'BACKEND': 'django.template.backends.jinja2.Jinja2',
+ 'APP_DIRS': True,
+ 'DIRS': [
+ # Specifically choose a name that will not be considered
+ # by app_directories loader, to make sure each test uses
+ # a specific template without considering the others.
+ os.path.join(TEST_DIR, 'test_templates_jinja2'),
+ ],
+}]
SECRET_KEY = "iufoj=mibkpdz*%bob952x(%49rqgv8gg45k36kjcg76&-y5=!"
--- /dev/null
+p { background: url('../img/python.png'); }
\ No newline at end of file
from __future__ import with_statement, unicode_literals
import os
import re
+from tempfile import mkdtemp
+from shutil import rmtree, copytree
-try:
- from bs4 import BeautifulSoup
-except ImportError:
- from BeautifulSoup import BeautifulSoup
+from bs4 import BeautifulSoup
-from django.utils import six
from django.core.cache.backends import locmem
from django.test import SimpleTestCase
from django.test.utils import override_settings
from compressor import cache as cachemod
from compressor.base import SOURCE_FILE, SOURCE_HUNK
-from compressor.cache import get_cachekey
+from compressor.cache import get_cachekey, get_precompiler_cachekey, get_hexdigest
from compressor.conf import settings
from compressor.css import CssCompressor
from compressor.exceptions import FilterDoesNotExist, FilterError
from compressor.js import JsCompressor
+from compressor.storage import DefaultStorage
def make_soup(markup):
- # we use html.parser instead of lxml because it doesn't work on python 3.3
- if six.PY3:
- return BeautifulSoup(markup, 'html.parser')
- else:
- return BeautifulSoup(markup)
+ return BeautifulSoup(markup, "html.parser")
def css_tag(href, **kwargs):
return 'OUTPUT'
+class PassthroughPrecompiler(object):
+ """A filter whose outputs the input unmodified """
+ def __init__(self, content, attrs, filter_type=None, filename=None,
+ charset=None):
+ self.content = content
+
+ def input(self, **kwargs):
+ return self.content
+
+
test_dir = os.path.abspath(os.path.join(os.path.dirname(__file__)))
+class PrecompilerAndAbsoluteFilterTestCase(SimpleTestCase):
+
+ def setUp(self):
+ self.html_orig = '<link rel="stylesheet" href="/static/css/relative_url.css" type="text/css" />'
+ self.html_link_to_precompiled_css = '<link rel="stylesheet" href="/static/CACHE/css/relative_url.41a74f6d5864.css" type="text/css" />'
+ self.html_link_to_absolutized_css = '<link rel="stylesheet" href="/static/CACHE/css/relative_url.9b8fd415e521.css" type="text/css" />'
+ self.css_orig = "p { background: url('../img/python.png'); }" # content of relative_url.css
+ self.css_absolutized = "p { background: url('/static/img/python.png?c2281c83670e'); }"
+
+ def helper(self, enabled, use_precompiler, use_absolute_filter, expected_output):
+ precompiler = (('text/css', 'compressor.tests.test_base.PassthroughPrecompiler'),) if use_precompiler else ()
+ filters = ('compressor.filters.css_default.CssAbsoluteFilter',) if use_absolute_filter else ()
+
+ with self.settings(COMPRESS_ENABLED=enabled, COMPRESS_PRECOMPILERS=precompiler, COMPRESS_CSS_FILTERS=filters):
+ css_node = CssCompressor(self.html_orig)
+ output = list(css_node.hunks())[0]
+ self.assertEqual(output, expected_output)
+
+ @override_settings(COMPRESS_CSS_HASHING_METHOD="content")
+ def test_precompiler_enables_absolute(self):
+ """
+ Tests whether specifying a precompiler also runs the CssAbsoluteFilter even if
+ compression is disabled, but only if the CssAbsoluteFilter is actually contained
+ in the filters setting.
+ While at it, ensure that everything runs as expected when compression is enabled.
+ """
+ self.helper(enabled=False, use_precompiler=False, use_absolute_filter=False, expected_output=self.html_orig)
+ self.helper(enabled=False, use_precompiler=False, use_absolute_filter=True, expected_output=self.html_orig)
+ self.helper(enabled=False, use_precompiler=True, use_absolute_filter=False, expected_output=self.html_link_to_precompiled_css)
+ self.helper(enabled=False, use_precompiler=True, use_absolute_filter=True, expected_output=self.html_link_to_absolutized_css)
+ self.helper(enabled=True, use_precompiler=False, use_absolute_filter=False, expected_output=self.css_orig)
+ self.helper(enabled=True, use_precompiler=False, use_absolute_filter=True, expected_output=self.css_absolutized)
+ self.helper(enabled=True, use_precompiler=True, use_absolute_filter=False, expected_output=self.css_orig)
+ self.helper(enabled=True, use_precompiler=True, use_absolute_filter=True, expected_output=self.css_absolutized)
+
+
+@override_settings(
+ COMPRESS_ENABLED=True,
+ COMPRESS_PRECOMPILERS=(),
+ COMPRESS_DEBUG_TOGGLE='nocompress',
+)
class CompressorTestCase(SimpleTestCase):
def setUp(self):
- settings.COMPRESS_ENABLED = True
- settings.COMPRESS_PRECOMPILERS = ()
- settings.COMPRESS_DEBUG_TOGGLE = 'nocompress'
self.css = """\
<link rel="stylesheet" href="/static/css/one.css" type="text/css" />
<style type="text/css">p { border:5px solid green;}</style>
self.assertTrue(is_date.match(str(float(date))),
"mtimes is returning something that doesn't look like a date: %s" % date)
+ @override_settings(COMPRESS_ENABLED=False)
def test_css_return_if_off(self):
- settings.COMPRESS_ENABLED = False
self.assertEqualCollapsed(self.css, self.css_node.output())
def test_cachekey(self):
css_node = CssCompressor(css)
self.assertRaises(FilterError, css_node.output, 'inline')
+ @override_settings(COMPRESS_PRECOMPILERS=(
+ ('text/django', 'compressor.filters.template.TemplateFilter'),
+ ), COMPRESS_ENABLED=True)
+ def test_template_precompiler(self):
+ css = '<style type="text/django">p { border:10px solid {% if 1 %}green{% else %}red{% endif %};}</style>'
+ css_node = CssCompressor(css)
+ output = make_soup(css_node.output('inline'))
+ self.assertEqual(output.text, 'p { border:10px solid green;}')
+
class CssMediaTestCase(SimpleTestCase):
def setUp(self):
def test_css_output(self):
css_node = CssCompressor(self.css)
- if six.PY3:
- links = make_soup(css_node.output()).find_all('link')
- else:
- links = make_soup(css_node.output()).findAll('link')
+ links = make_soup(css_node.output()).find_all('link')
media = ['screen', 'print', 'all', None]
self.assertEqual(len(links), 4)
self.assertEqual(media, [l.get('media', None) for l in links])
css = self.css + '<style type="text/css" media="print">p { border:10px solid red;}</style>'
css_node = CssCompressor(css)
media = ['screen', 'print', 'all', None, 'print']
- if six.PY3:
- links = make_soup(css_node.output()).find_all('link')
- else:
- links = make_soup(css_node.output()).findAll('link')
+ links = make_soup(css_node.output()).find_all('link')
self.assertEqual(media, [l.get('media', None) for l in links])
@override_settings(COMPRESS_PRECOMPILERS=(
<link rel="stylesheet" href="/static/css/two.css" type="text/css" media="screen">
<style type="text/foobar" media="screen">h1 { border:5px solid green;}</style>"""
css_node = CssCompressor(css)
- if six.PY3:
- output = make_soup(css_node.output()).find_all(['link', 'style'])
- else:
- output = make_soup(css_node.output()).findAll(['link', 'style'])
+ output = make_soup(css_node.output()).find_all(['link', 'style'])
self.assertEqual(['/static/css/one.css', '/static/css/two.css', None],
[l.get('href', None) for l in output])
self.assertEqual(['screen', 'screen', 'screen'],
[l.get('media', None) for l in output])
+@override_settings(COMPRESS_VERBOSE=True)
class VerboseTestCase(CompressorTestCase):
-
- def setUp(self):
- super(VerboseTestCase, self).setUp()
- settings.COMPRESS_VERBOSE = True
+ pass
class CacheBackendTestCase(CompressorTestCase):
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]
+ scripts = make_soup(js_node.output()).find_all('script')
+ attrs = [extract_attr(s) for s in scripts]
self.assertEqual(output, attrs)
@override_settings(COMPRESS_CACHE_KEY_FUNCTION='invalid.module')
def test_get_cachekey_invalid_mod(self):
self.assertRaises(ImportError, lambda: get_cachekey("foo"))
+
+ def test_get_precompiler_cachekey(self):
+ try:
+ get_precompiler_cachekey("asdf", "asdf")
+ except TypeError:
+ self.fail("get_precompiler_cachekey raised TypeError unexpectedly")
+
+
+class CompressorInDebugModeTestCase(SimpleTestCase):
+
+ def setUp(self):
+ self.css = '<link rel="stylesheet" href="/static/css/one.css" type="text/css" />'
+ self.tmpdir = mkdtemp()
+ new_static_root = os.path.join(self.tmpdir, "static")
+ copytree(settings.STATIC_ROOT, new_static_root)
+
+ self.override_settings = self.settings(
+ COMPRESS_ENABLED=True,
+ COMPRESS_PRECOMPILERS=(),
+ COMPRESS_DEBUG_TOGGLE='nocompress',
+ DEBUG=True,
+ STATIC_ROOT=new_static_root,
+ COMPRESS_ROOT=new_static_root,
+ STATICFILES_DIRS=[settings.COMPRESS_ROOT]
+ )
+ self.override_settings.__enter__()
+
+ def tearDown(self):
+ rmtree(self.tmpdir)
+ self.override_settings.__exit__(None, None, None)
+
+ def test_filename_in_debug_mode(self):
+ # In debug mode, compressor should look for files using staticfiles
+ # finders only, and not look into the global static directory, where
+ # files can be outdated
+ css_filename = os.path.join(settings.COMPRESS_ROOT, "css", "one.css")
+ # Store the hash of the original file's content
+ css_content = open(css_filename).read()
+ hashed = get_hexdigest(css_content, 12)
+ # Now modify the file in the STATIC_ROOT
+ test_css_content = "p { font-family: 'test' }"
+ with open(css_filename, "a") as css:
+ css.write("\n")
+ css.write(test_css_content)
+ # We should generate a link with the hash of the original content, not
+ # the modified one
+ expected = '<link rel="stylesheet" href="/static/CACHE/css/%s.css" type="text/css" />' % hashed
+ compressor = CssCompressor(self.css)
+ compressor.storage = DefaultStorage()
+ output = compressor.output()
+ self.assertEqual(expected, output)
+ result = open(os.path.join(settings.COMPRESS_ROOT, "CACHE", "css",
+ "%s.css" % hashed), "r").read()
+ self.assertTrue(test_css_content not in result)
import io
import os
import sys
-import textwrap
from django.utils import six
from django.test import TestCase
-from django.utils import unittest
from django.test.utils import override_settings
-from compressor.cache import get_hashed_mtime, get_hashed_content
+from compressor.cache import cache, get_hashed_mtime, get_hashed_content
from compressor.conf import settings
from compressor.css import CssCompressor
-from compressor.utils import find_command
-from compressor.filters.base import CompilerFilter
-from compressor.filters.cssmin import CSSMinFilter
+from compressor.filters.base import CompilerFilter, CachedCompilerFilter
+from compressor.filters.cssmin import CSSCompressorFilter, rCSSMinFilter
from compressor.filters.css_default import CssAbsoluteFilter
+from compressor.filters.jsmin import JSMinFilter
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.filters.cleancss import CleanCSSFilter
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):
- def test_tidy(self):
- content = textwrap.dedent("""\
- /* Some comment */
- font,th,td,p{
- color: black;
- }
- """)
- ret = CSSTidyFilter(content).input()
- self.assertIsInstance(ret, six.text_type)
- self.assertEqual(
- "font,th,td,p{color:#000;}", CSSTidyFilter(content).input())
-
-
+@override_settings(COMPRESS_CACHEABLE_PRECOMPILERS=('text/css',))
class PrecompilerTestCase(TestCase):
def setUp(self):
self.test_precompiler = os.path.join(test_dir, 'precompiler.py')
self.setup_infile()
+ self.cached_precompiler_args = dict(
+ content=self.content, charset=settings.FILE_CHARSET,
+ filename=self.filename, mimetype='text/css')
def setup_infile(self, filename='static/css/one.css'):
self.filename = os.path.join(test_dir, filename)
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
self.assertEqual(type(compiler.input()), six.text_type)
+ def test_precompiler_cache(self):
+ command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
+ compiler = CachedCompilerFilter(command=command, **self.cached_precompiler_args)
+ self.assertEqual("body { color:#990; }", compiler.input())
+ # We tell whether the precompiler actually ran by inspecting compiler.infile. If not None, the compiler had to
+ # write the input out to the file for the external command. If None, it was in the cache and thus skipped.
+ self.assertIsNotNone(compiler.infile) # Not cached
+
+ compiler = CachedCompilerFilter(command=command, **self.cached_precompiler_args)
+ self.assertEqual("body { color:#990; }", compiler.input())
+ self.assertIsNone(compiler.infile) # Cached
+
+ self.cached_precompiler_args['content'] += ' ' # Invalidate cache by slightly changing content
+ compiler = CachedCompilerFilter(command=command, **self.cached_precompiler_args)
+ self.assertEqual("body { color:#990; }", compiler.input())
+ self.assertIsNotNone(compiler.infile) # Not cached
+
+ def test_precompiler_not_cacheable(self):
+ command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
+ self.cached_precompiler_args['mimetype'] = 'text/different'
+ compiler = CachedCompilerFilter(command=command, **self.cached_precompiler_args)
+ self.assertEqual("body { color:#990; }", compiler.input())
+ self.assertIsNotNone(compiler.infile) # Not cached
-class CssMinTestCase(TestCase):
- def test_cssmin_filter(self):
- content = """p {
+ compiler = CachedCompilerFilter(command=command, **self.cached_precompiler_args)
+ self.assertEqual("body { color:#990; }", compiler.input())
+ self.assertIsNotNone(compiler.infile) # Not cached
+
+ def test_precompiler_caches_empty_files(self):
+ command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
+ compiler = CachedCompilerFilter(command=command, **self.cached_precompiler_args)
+ self.assertEqual("body { color:#990; }", compiler.input())
+
+ cache.set(compiler.get_cache_key(), "")
+ compiler = CachedCompilerFilter(command=command, **self.cached_precompiler_args)
+ self.assertEqual("", compiler.input())
+
+
+class CSSCompressorTestCase(TestCase):
+ def test_csscompressor_filter(self):
+ content = """/*!
+ * django-compressor
+ * Copyright (c) 2009-2014 Django Compressor authors
+ */
+ p {
background: rgb(51,102,153) url('../../images/image.gif');
}
"""
- output = "p{background:#369 url('../../images/image.gif')}"
- self.assertEqual(output, CSSMinFilter(content).output())
+ output = """/*!
+ * django-compressor
+ * Copyright (c) 2009-2014 Django Compressor authors
+ */p{background:#369 url('../../images/image.gif')}"""
+ self.assertEqual(output, CSSCompressorFilter(content).output())
+
+
+class rCssMinTestCase(TestCase):
+ def test_rcssmin_filter(self):
+ content = """/*!
+ * django-compressor
+ * Copyright (c) 2009-2014 Django Compressor authors
+ */
+ p {
+
+
+ background: rgb(51,102,153) url('../../images/image.gif');
+ }
+ """
+ output = """/*!
+ * django-compressor
+ * Copyright (c) 2009-2014 Django Compressor authors
+ */p{background:rgb(51,102,153) url('../../images/image.gif')}"""
+ self.assertEqual(output, rCSSMinFilter(content).output())
+
+
+class JsMinTestCase(TestCase):
+ def test_jsmin_filter(self):
+ content = """/*!
+ * django-compressor
+ * Copyright (c) 2009-2014 Django Compressor authors
+ */
+ var foo = "bar";"""
+ output = """/*!
+ * django-compressor
+ * Copyright (c) 2009-2014 Django Compressor authors
+ */var foo="bar";"""
+ self.assertEqual(output, JSMinFilter(content).output())
+
+
+@override_settings(
+ COMPRESS_ENABLED=True,
+ COMPRESS_URL='/static/',
+)
class CssAbsolutizingTestCase(TestCase):
hashing_method = 'mtime'
hashing_func = staticmethod(get_hashed_mtime)
"p { filter: Alpha(src='%(url)simg/python.png%(query)s%(hash)s%(frag)s') }")
def setUp(self):
- self.old_enabled = settings.COMPRESS_ENABLED
- self.old_url = settings.COMPRESS_URL
- self.old_hashing_method = settings.COMPRESS_CSS_HASHING_METHOD
- settings.COMPRESS_ENABLED = True
- settings.COMPRESS_URL = '/static/'
- settings.COMPRESS_CSS_HASHING_METHOD = self.hashing_method
+ self.override_settings = self.settings(COMPRESS_CSS_HASHING_METHOD=self.hashing_method)
+ self.override_settings.__enter__()
+
self.css = """
<link rel="stylesheet" href="/static/css/url/url1.css" type="text/css">
<link rel="stylesheet" href="/static/css/url/2/url2.css" type="text/css">
self.css_node = CssCompressor(self.css)
def tearDown(self):
- settings.COMPRESS_ENABLED = self.old_enabled
- settings.COMPRESS_URL = self.old_url
- settings.COMPRESS_CSS_HASHING_METHOD = self.old_hashing_method
+ self.override_settings.__exit__(None, None, None)
+ @override_settings(COMPRESS_CSS_HASHING_METHOD=None)
def test_css_no_hash(self):
- settings.COMPRESS_CSS_HASHING_METHOD = None
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
content = self.template % blankdict(url='../../')
params = blankdict({
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
- settings.COMPRESS_URL = params['url'] = 'http://static.example.com/'
- output = self.template % params
- filter = CssAbsoluteFilter(content)
- self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+ with self.settings(COMPRESS_URL='http://static.example.com/'):
+ params['url'] = settings.COMPRESS_URL
+ 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):
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
- settings.COMPRESS_URL = params['url'] = 'http://static.example.com/'
- output = self.template % params
- filter = CssAbsoluteFilter(content)
- self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+ with self.settings(COMPRESS_URL='http://static.example.com/'):
+ params['url'] = settings.COMPRESS_URL
+ 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')
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)
- self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+ with self.settings(COMPRESS_URL='http://media.example.com/'):
+ params['url'] = settings.COMPRESS_URL
+ output = self.template % params
+ filter = CssAbsoluteFilter(content)
+ self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter_only_url_fragment(self):
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
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)
- self.assertEqual(content, filter.input(filename=filename, basename='css/url/test.css'))
+ with self.settings(COMPRESS_URL='http://media.example.com/'):
+ filter = CssAbsoluteFilter(content)
+ 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')
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)
- self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+ with self.settings(COMPRESS_URL='http://media.example.com/'):
+ params['url'] = settings.COMPRESS_URL
+ output = self.template % params
+ filter = CssAbsoluteFilter(content)
+ 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')
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
- settings.COMPRESS_URL = params['url'] = 'https://static.example.com/'
- output = self.template % params
- filter = CssAbsoluteFilter(content)
- self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+ with self.settings(COMPRESS_URL='https://static.example.com/'):
+ params['url'] = settings.COMPRESS_URL
+ 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')
filter = CssAbsoluteFilter(content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
- settings.COMPRESS_URL = params['url'] = 'https://static.example.com/'
- output = self.template % params
- filter = CssAbsoluteFilter(content)
- self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
+ with self.settings(COMPRESS_URL='https://static.example.com/'):
+ params['url'] = settings.COMPRESS_URL
+ output = self.template % params
+ 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'
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'))
+
+ with self.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 = {
def test_guess_filename(self):
for base_url in ('/static/', 'http://static.example.com/'):
- settings.COMPRESS_URL = base_url
- url = '%s/img/python.png' % settings.COMPRESS_URL.rstrip('/')
- path = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
- content = "p { background: url('%s') }" % url
- filter = CssAbsoluteFilter(content)
- self.assertEqual(path, filter.guess_filename(url))
+ with self.settings(COMPRESS_URL=base_url):
+ url = '%s/img/python.png' % settings.COMPRESS_URL.rstrip('/')
+ path = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
+ content = "p { background: url('%s') }" % url
+ filter = CssAbsoluteFilter(content)
+ self.assertEqual(path, filter.guess_filename(url))
class CssAbsolutizingTestCaseWithHash(CssAbsolutizingTestCase):
hashing_func = staticmethod(get_hashed_content)
+@override_settings(
+ COMPRESS_ENABLED=True,
+ COMPRESS_CSS_FILTERS=[
+ 'compressor.filters.css_default.CssAbsoluteFilter',
+ 'compressor.filters.datauri.CssDataUriFilter',
+ ],
+ COMPRESS_URL='/static/',
+ COMPRESS_CSS_HASHING_METHOD='mtime'
+)
class CssDataUriTestCase(TestCase):
def setUp(self):
- settings.COMPRESS_ENABLED = True
- settings.COMPRESS_CSS_FILTERS = [
- 'compressor.filters.css_default.CssAbsoluteFilter',
- 'compressor.filters.datauri.CssDataUriFilter',
- ]
- settings.COMPRESS_URL = '/static/'
- settings.COMPRESS_CSS_HASHING_METHOD = 'mtime'
self.css = """
<link rel="stylesheet" href="/static/css/datauri.css" type="text/css">
"""
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')
--- /dev/null
+from django.test import TestCase
+
+from compressor.finders import CompressorFinder
+from compressor.storage import CompressorFileStorage
+
+
+class FinderTestCase(TestCase):
+
+ def test_has_correct_storage(self):
+ finder = CompressorFinder()
+ self.assertTrue(type(finder.storage) is CompressorFileStorage)
+
+ def test_list_returns_empty_list(self):
+ finder = CompressorFinder()
+ self.assertEqual(finder.list([]), [])
from __future__ import with_statement, unicode_literals
import sys
+import unittest
from django.test import TestCase
-from django.utils import unittest, six
+from django.utils import six
from django.test.utils import override_settings
from compressor.conf import settings
from compressor.tests.test_base import css_tag
-@unittest.skipUnless(not six.PY3 or sys.version_info[:2] >= (3, 3),
+@unittest.skipIf(six.PY3 and sys.version_info[:2] < (3, 3),
'Jinja can only run on Python < 3 and >= 3.3')
class TestJinja2CompressorExtension(TestCase):
"""
self.assertEqual(out, template.render(context))
def test_nonascii_inline_css(self):
- org_COMPRESS_ENABLED = settings.COMPRESS_ENABLED
- settings.COMPRESS_ENABLED = False
- template = self.env.from_string('{% compress css %}'
- '<style type="text/css">'
- '/* русский текст */'
- '</style>{% endcompress %}')
+ with self.settings(COMPRESS_ENABLED=False):
+ template = self.env.from_string('{% compress css %}'
+ '<style type="text/css">'
+ '/* русский текст */'
+ '</style>{% endcompress %}')
out = '<link rel="stylesheet" href="/static/CACHE/css/b2cec0f8cb24.css" type="text/css" />'
- settings.COMPRESS_ENABLED = org_COMPRESS_ENABLED
context = {'STATIC_URL': settings.COMPRESS_URL}
self.assertEqual(out, template.render(context))
--- /dev/null
+from django.core.management import call_command
+from django.core.management.base import CommandError
+from django.test import TestCase
+from django.utils.six import StringIO
+
+
+class TestMtimeCacheCommand(TestCase):
+ # FIXME: add actual tests, improve the existing ones.
+
+ exclusion_patterns = [
+ '*CACHE*', '*custom*', '*066cd253eada.js', 'test.txt*'
+ ]
+
+ def default_ignore(self):
+ return ['--ignore=%s' % pattern for pattern in self.exclusion_patterns]
+
+ def test_handle_no_args(self):
+ with self.assertRaises(CommandError):
+ call_command('mtime_cache')
+
+ def test_handle_add(self):
+ out = StringIO()
+ with self.settings(CACHES={}):
+ call_command(
+ 'mtime_cache', '--add', *self.default_ignore(), stdout=out)
+ output = out.getvalue()
+ self.assertIn('Deleted mtimes of 19 files from the cache.', output)
+ self.assertIn('Added mtimes of 19 files to cache.', output)
+
+ def test_handle_clean(self):
+ out = StringIO()
+ with self.settings(CACHES={}):
+ call_command(
+ 'mtime_cache', '--clean', *self.default_ignore(), stdout=out)
+ output = out.getvalue()
+ self.assertIn('Deleted mtimes of 19 files from the cache.', output)
+ self.assertNotIn('Added mtimes of 19 files to cache.', output)
from __future__ import with_statement, unicode_literals
+import copy
import io
import os
import sys
+import unittest
+from importlib import import_module
+
+from mock import patch
+from unittest import SkipTest
-import django
from django.core.management.base import CommandError
from django.template import Template, Context
from django.test import TestCase
-from django.utils import six, unittest
+from django.utils import six
from compressor.cache import flush_offline_manifest, get_offline_manifest
from compressor.conf import settings
from compressor.exceptions import OfflineGenerationError
from compressor.management.commands.compress import Command as CompressCommand
from compressor.storage import default_storage
+from compressor.utils import get_mod_func
if six.PY3:
# there is an 'io' module in python 2.6+, but io.StringIO does not
# The Jinja2 tests fail on Python 3.2 due to the following:
# The line in compressor/management/commands/compress.py:
# compressor_nodes.setdefault(template, []).extend(nodes)
-# causes the error "unhashable type: 'Template'"
+# causes the error 'unhashable type: 'Template''
_TEST_JINJA2 = not(sys.version_info[0] == 3 and sys.version_info[1] == 2)
+def offline_context_generator():
+ for i in range(1, 4):
+ yield {'content': 'OK %d!' % i}
+
+
class OfflineTestCaseMixin(object):
- template_name = "test_compressor_offline.html"
+ template_name = 'test_compressor_offline.html'
verbosity = 0
# Change this for each test class
- templates_dir = ""
- expected_hash = ""
+ templates_dir = ''
+ expected_hash = ''
# Engines to test
if _TEST_JINJA2:
- engines = ("django", "jinja2")
+ engines = ('django', 'jinja2')
else:
- engines = ("django",)
+ engines = ('django',)
+ additional_test_settings = None
def setUp(self):
self.log = StringIO()
# Reset template dirs, because it enables us to force compress to
# consider only a specific directory (helps us make true,
- # independant unit tests).
- # Specify both Jinja2 and Django template locations. When the wrong engine
- # is used to parse a template, the TemplateSyntaxError will cause the
- # 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)
+ # independent unit tests).
+ # Specify both Jinja2 and Django template locations. When the wrong
+ # engine is used to parse a template, the TemplateSyntaxError will
+ # cause the template to be skipped over.
+ # We've hardcoded TEMPLATES[0] to be Django templates backend and
+ # TEMPLATES[1] to be Jinja2 templates backend in test_settings.
+ TEMPLATES = copy.deepcopy(settings.TEMPLATES)
+
+ django_template_dir = os.path.join(
+ TEMPLATES[0]['DIRS'][0], self.templates_dir)
+ jinja2_template_dir = os.path.join(
+ TEMPLATES[1]['DIRS'][0], self.templates_dir)
+
+ TEMPLATES[0]['DIRS'] = [django_template_dir]
+ TEMPLATES[1]['DIRS'] = [jinja2_template_dir]
override_settings = {
- 'TEMPLATE_DIRS': (django_template_dir, jinja2_template_dir,),
+ 'TEMPLATES': TEMPLATES,
'COMPRESS_ENABLED': True,
'COMPRESS_OFFLINE': True
}
- if "jinja2" in self.engines:
- override_settings["COMPRESS_JINJA2_GET_ENVIRONMENT"] = lambda: self._get_jinja2_env()
+ if 'jinja2' in self.engines:
+ override_settings['COMPRESS_JINJA2_GET_ENVIRONMENT'] = (
+ lambda: self._get_jinja2_env())
+
+ if self.additional_test_settings is not None:
+ override_settings.update(self.additional_test_settings)
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)
+ if 'django' in self.engines:
+ self.template_path = os.path.join(
+ django_template_dir, self.template_name)
- with io.open(self.template_path, encoding=settings.FILE_CHARSET) as file:
- self.template = Template(file.read())
+ with io.open(self.template_path,
+ encoding=settings.FILE_CHARSET) as file_:
+ self.template = Template(file_.read())
- if "jinja2" in self.engines:
- jinja2_env = override_settings["COMPRESS_JINJA2_GET_ENVIRONMENT"]()
- self.template_path_jinja2 = os.path.join(jinja2_template_dir, self.template_name)
+ if 'jinja2' in self.engines:
+ self.template_path_jinja2 = os.path.join(
+ jinja2_template_dir, self.template_name)
+ jinja2_env = override_settings['COMPRESS_JINJA2_GET_ENVIRONMENT']()
- with io.open(self.template_path_jinja2, encoding=settings.FILE_CHARSET) as file:
- self.template_jinja2 = jinja2_env.from_string(file.read())
+ 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):
self.override_settings.__exit__(None, None, None)
if default_storage.exists(manifest_path):
default_storage.delete(manifest_path)
+ def _prepare_contexts(self, engine):
+ if engine == 'django':
+ return [Context(settings.COMPRESS_OFFLINE_CONTEXT)]
+ if engine == 'jinja2':
+ return [settings.COMPRESS_OFFLINE_CONTEXT]
+ return None
+
def _render_template(self, engine):
- if engine == "django":
- return self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
- elif engine == "jinja2":
- return self.template_jinja2.render(settings.COMPRESS_OFFLINE_CONTEXT) + "\n"
- else:
- return None
+ contexts = self._prepare_contexts(engine)
+ if engine == 'django':
+ return ''.join(self.template.render(c) for c in contexts)
+ if engine == 'jinja2':
+ return '\n'.join(
+ self.template_jinja2.render(c) for c in contexts) + '\n'
+ return None
def _test_offline(self, engine):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
- self.assertEqual(1, count)
+ hashes = self.expected_hash
+ if not isinstance(hashes, (list, tuple)):
+ hashes = [hashes]
+ count, result = CompressCommand().compress(
+ log=self.log, verbosity=self.verbosity, engine=engine)
+ self.assertEqual(len(hashes), count)
self.assertEqual([
- '<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (self.expected_hash, ),
- ], result)
+ '<script type="text/javascript" src="/static/CACHE/js/'
+ '%s.js"></script>' % h for h in hashes], result)
rendered_template = self._render_template(engine)
- self.assertEqual(rendered_template, "".join(result) + "\n")
+ self.assertEqual(rendered_template, '\n'.join(result) + '\n')
- def test_offline(self):
- for engine in self.engines:
- self._test_offline(engine=engine)
+ def test_offline_django(self):
+ if 'django' not in self.engines:
+ raise SkipTest('This test class does not support django engine.')
+ self._test_offline(engine='django')
+
+ def test_offline_jinja2(self):
+ if 'jinja2' not in self.engines:
+ raise SkipTest('This test class does not support jinja2 engine.')
+ self._test_offline(engine='jinja2')
def _get_jinja2_env(self):
import jinja2
def _get_jinja2_loader(self):
import jinja2
- loader = jinja2.FileSystemLoader(settings.TEMPLATE_DIRS, encoding=settings.FILE_CHARSET)
+ loader = jinja2.FileSystemLoader(
+ settings.TEMPLATES[1]['DIRS'], encoding=settings.FILE_CHARSET)
return loader
-class OfflineGenerationSkipDuplicatesTestCase(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_duplicate"
+class OfflineCompressBasicTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'basic'
+ expected_hash = 'f5e179b8eca4'
+
+ @patch.object(CompressCommand, 'compress')
+ def test_handle_no_args(self, compress_mock):
+ CompressCommand().handle()
+ self.assertEqual(compress_mock.call_count, 1)
+
+ @patch.object(CompressCommand, 'compress')
+ def test_handle_compress_disabled(self, compress_mock):
+ with self.settings(COMPRESS_ENABLED=False):
+ with self.assertRaises(CommandError):
+ CompressCommand().handle()
+ self.assertEqual(compress_mock.call_count, 0)
+
+ @patch.object(CompressCommand, 'compress')
+ def test_handle_compress_offline_disabled(self, compress_mock):
+ with self.settings(COMPRESS_OFFLINE=False):
+ with self.assertRaises(CommandError):
+ CompressCommand().handle()
+ self.assertEqual(compress_mock.call_count, 0)
+
+ @patch.object(CompressCommand, 'compress')
+ def test_handle_compress_offline_disabled_force(self, compress_mock):
+ with self.settings(COMPRESS_OFFLINE=False):
+ CompressCommand().handle(force=True)
+ self.assertEqual(compress_mock.call_count, 1)
+
+ def test_rendering_without_manifest_raises_exception(self):
+ # flush cached manifest
+ flush_offline_manifest()
+ self.assertRaises(OfflineGenerationError,
+ self.template.render, Context({}))
+
+ @unittest.skipIf(not _TEST_JINJA2, 'No Jinja2 testing')
+ def test_rendering_without_manifest_raises_exception_jinja2(self):
+ # flush cached manifest
+ flush_offline_manifest()
+ self.assertRaises(OfflineGenerationError,
+ self.template_jinja2.render, {})
+
+ def _test_deleting_manifest_does_not_affect_rendering(self, engine):
+ count, result = CompressCommand().compress(
+ log=self.log, verbosity=self.verbosity, engine=engine)
+ get_offline_manifest()
+ manifest_path = os.path.join('CACHE', 'manifest.json')
+ if default_storage.exists(manifest_path):
+ default_storage.delete(manifest_path)
+ self.assertEqual(1, count)
+ self.assertEqual([
+ '<script type="text/javascript" src="/static/CACHE/js/'
+ '%s.js"></script>' % (self.expected_hash, )], result)
+ rendered_template = self._render_template(engine)
+ self.assertEqual(rendered_template, ''.join(result) + '\n')
+
+ def test_deleting_manifest_does_not_affect_rendering(self):
+ for engine in self.engines:
+ self._test_deleting_manifest_does_not_affect_rendering(engine)
+
+ def test_get_loaders(self):
+ TEMPLATE_LOADERS = (
+ ('django.template.loaders.cached.Loader', (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ )),
+ )
+ with self.settings(TEMPLATE_LOADERS=TEMPLATE_LOADERS):
+ from django.template.loaders.filesystem import (
+ Loader as FileSystemLoader)
+ from django.template.loaders.app_directories import (
+ Loader as AppDirectoriesLoader)
+ loaders = CompressCommand().get_loaders()
+ self.assertTrue(isinstance(loaders[0], FileSystemLoader))
+ self.assertTrue(isinstance(loaders[1], AppDirectoriesLoader))
+
+
+class OfflineCompressSkipDuplicatesTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_duplicate'
# We don't need to test multiples engines here.
- engines = ("django",)
+ engines = ('django',)
def _test_offline(self, engine):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ count, result = CompressCommand().compress(
+ log=self.log, verbosity=self.verbosity, engine=engine)
# Only one block compressed, the second identical one was skipped.
self.assertEqual(1, count)
# Only 1 <script> block in returned result as well.
self.assertEqual([
- '<script type="text/javascript" src="/static/CACHE/js/f5e179b8eca4.js"></script>',
+ '<script type="text/javascript" src="/static/CACHE/js/'
+ 'f5e179b8eca4.js"></script>',
], result)
rendered_template = self._render_template(engine)
# But rendering the template returns both (identical) scripts.
- self.assertEqual(rendered_template, "".join(result * 2) + "\n")
+ self.assertEqual(rendered_template, ''.join(result * 2) + '\n')
-class OfflineGenerationBlockSuperTestCase(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_block_super"
- expected_hash = "7c02d201f69d"
+class OfflineCompressBlockSuperTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_block_super'
+ expected_hash = '7c02d201f69d'
# Block.super not supported for Jinja2 yet.
- engines = ("django",)
+ engines = ('django',)
-class OfflineGenerationBlockSuperMultipleTestCase(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_block_super_multiple"
- expected_hash = "f8891c416981"
+class OfflineCompressBlockSuperMultipleTestCase(
+ OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_block_super_multiple'
+ expected_hash = 'f8891c416981'
# Block.super not supported for Jinja2 yet.
- engines = ("django",)
+ engines = ('django',)
-class OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_block_super_multiple_cached"
- expected_hash = "2f6ef61c488e"
+class OfflineCompressBlockSuperMultipleCachedLoaderTestCase(
+ OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_block_super_multiple_cached'
+ expected_hash = '2f6ef61c488e'
# Block.super not supported for Jinja2 yet.
- engines = ("django",)
-
- def setUp(self):
- self._old_template_loaders = settings.TEMPLATE_LOADERS
- settings.TEMPLATE_LOADERS = (
+ engines = ('django',)
+ additional_test_settings = {
+ 'TEMPLATE_LOADERS': (
('django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)),
)
- super(OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase, self).setUp()
-
- def tearDown(self):
- super(OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase, self).tearDown()
- settings.TEMPLATE_LOADERS = self._old_template_loaders
+ }
-class OfflineGenerationBlockSuperTestCaseWithExtraContent(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_block_super_extra"
+class OfflineCompressBlockSuperTestCaseWithExtraContent(
+ OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_block_super_extra'
# Block.super not supported for Jinja2 yet.
- engines = ("django",)
+ engines = ('django',)
def _test_offline(self, engine):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ count, result = CompressCommand().compress(
+ log=self.log, verbosity=self.verbosity, engine=engine)
self.assertEqual(2, count)
self.assertEqual([
- '<script type="text/javascript" src="/static/CACHE/js/ced14aec5856.js"></script>',
- '<script type="text/javascript" src="/static/CACHE/js/7c02d201f69d.js"></script>'
+ '<script type="text/javascript" src="/static/CACHE/js/'
+ 'ced14aec5856.js"></script>',
+ '<script type="text/javascript" src="/static/CACHE/js/'
+ '7c02d201f69d.js"></script>',
], result)
rendered_template = self._render_template(engine)
- self.assertEqual(rendered_template, "".join(result) + "\n")
-
+ self.assertEqual(rendered_template, ''.join(result) + '\n')
-class OfflineGenerationConditionTestCase(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_condition"
- expected_hash = "4e3758d50224"
- def setUp(self):
- self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
- settings.COMPRESS_OFFLINE_CONTEXT = {
+class OfflineCompressConditionTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_condition'
+ expected_hash = '4e3758d50224'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': {
'condition': 'red',
}
- super(OfflineGenerationConditionTestCase, self).setUp()
+ }
- def tearDown(self):
- self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
- super(OfflineGenerationConditionTestCase, self).tearDown()
+class OfflineCompressTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_templatetag'
+ expected_hash = 'a27e1d3a619a'
-class OfflineGenerationTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_templatetag"
- expected_hash = "a27e1d3a619a"
+class OfflineCompressStaticTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_static_templatetag'
+ expected_hash = 'dfa2bb387fa8'
-class OfflineGenerationStaticTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_static_templatetag"
- expected_hash = "dfa2bb387fa8"
+class OfflineCompressTestCaseWithContext(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context'
+ expected_hash = '5838e2fd66af'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': {
+ 'content': 'OK!',
+ }
+ }
-class OfflineGenerationTestCaseWithContext(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_with_context"
- expected_hash = "5838e2fd66af"
- def setUp(self):
- self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
- settings.COMPRESS_OFFLINE_CONTEXT = {
+class OfflineCompressTestCaseWithContextSuper(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context_super'
+ expected_hash = 'b1d0a333a4ef'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': {
'content': 'OK!',
}
- super(OfflineGenerationTestCaseWithContext, self).setUp()
+ }
+ # Block.super not supported for Jinja2 yet.
+ engines = ('django',)
+
+
+class OfflineCompressTestCaseWithContextList(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context'
+ expected_hash = ['f8bcaea049b3', 'db12749b1e80', 'e9f4a0054a06']
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': list(offline_context_generator())
+ }
+
+ def _prepare_contexts(self, engine):
+ if engine == 'django':
+ return [Context(c) for c in settings.COMPRESS_OFFLINE_CONTEXT]
+ if engine == 'jinja2':
+ return settings.COMPRESS_OFFLINE_CONTEXT
+ return None
+
+
+class OfflineCompressTestCaseWithContextListSuper(
+ OfflineCompressTestCaseWithContextList):
+ templates_dir = 'test_with_context_super'
+ expected_hash = ['b11543f1e174', 'aedf6d2a7ec7', '0dbb8c29f23a']
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': list(offline_context_generator())
+ }
+ # Block.super not supported for Jinja2 yet.
+ engines = ('django',)
+
+
+class OfflineCompressTestCaseWithContextGenerator(
+ OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context'
+ expected_hash = ['f8bcaea049b3', 'db12749b1e80', 'e9f4a0054a06']
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': 'compressor.tests.test_offline.'
+ 'offline_context_generator'
+ }
+
+ def _prepare_contexts(self, engine):
+ module, function = get_mod_func(settings.COMPRESS_OFFLINE_CONTEXT)
+ contexts = getattr(import_module(module), function)()
+ if engine == 'django':
+ return (Context(c) for c in contexts)
+ if engine == 'jinja2':
+ return contexts
+ return None
+
+
+class OfflineCompressTestCaseWithContextGeneratorSuper(
+ OfflineCompressTestCaseWithContextGenerator):
+ templates_dir = 'test_with_context_super'
+ expected_hash = ['b11543f1e174', 'aedf6d2a7ec7', '0dbb8c29f23a']
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': 'compressor.tests.test_offline.'
+ 'offline_context_generator'
+ }
+ # Block.super not supported for Jinja2 yet.
+ engines = ('django',)
- def tearDown(self):
- settings.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
- super(OfflineGenerationTestCaseWithContext, self).tearDown()
+class OfflineCompressTestCaseWithContextGeneratorImportError(
+ OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context'
-class OfflineGenerationTestCaseErrors(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_error_handling"
+ def _test_offline(self, engine):
+ # Test that we are properly generating ImportError when
+ # COMPRESS_OFFLINE_CONTEXT looks like a function but can't be imported
+ # for whatever reason.
+
+ with self.settings(
+ COMPRESS_OFFLINE_CONTEXT='invalid_mod.invalid_func'):
+ # Path with invalid module name -- ImportError:
+ self.assertRaises(
+ ImportError, CompressCommand().compress, engine=engine)
+
+ with self.settings(COMPRESS_OFFLINE_CONTEXT='compressor'):
+ # Valid module name only without function -- AttributeError:
+ self.assertRaises(
+ ImportError, CompressCommand().compress, engine=engine)
+
+ with self.settings(
+ COMPRESS_OFFLINE_CONTEXT='compressor.tests.invalid_function'):
+ # Path with invalid function name -- AttributeError:
+ self.assertRaises(
+ ImportError, CompressCommand().compress, engine=engine)
+
+ with self.settings(
+ COMPRESS_OFFLINE_CONTEXT='compressor.tests.test_offline'):
+ # Path without function attempts call on module -- TypeError:
+ self.assertRaises(
+ ImportError, CompressCommand().compress, engine=engine)
+
+ valid_path = 'compressor.tests.test_offline.offline_context_generator'
+ with self.settings(COMPRESS_OFFLINE_CONTEXT=valid_path):
+ # Valid path to generator function -- no ImportError:
+
+ try:
+ CompressCommand().compress(engine=engine)
+ except ImportError:
+ self.fail('Valid path to offline context generator must'
+ ' not raise ImportError.')
+
+
+class OfflineCompressTestCaseErrors(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_error_handling'
def _test_offline(self, engine):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ count, result = CompressCommand().compress(
+ log=self.log, verbosity=self.verbosity, engine=engine)
- if engine == "django":
+ if engine == 'django':
self.assertEqual(2, count)
else:
# Because we use env.parse in Jinja2Parser, the engine does not
- # actually load the "extends" and "includes" templates, and so
- # it is unable to detect that they are missing. So all the "compress"
- # nodes are processed correctly.
+ # actually load the 'extends' and 'includes' templates, and so
+ # it is unable to detect that they are missing. So all the
+ # 'compress' nodes are processed correctly.
self.assertEqual(4, count)
- self.assertEqual(engine, "jinja2")
- self.assertIn('<link rel="stylesheet" href="/static/CACHE/css/78bd7a762e2d.css" type="text/css" />', result)
- self.assertIn('<link rel="stylesheet" href="/static/CACHE/css/e31030430724.css" type="text/css" />', result)
-
- self.assertIn('<script type="text/javascript" src="/static/CACHE/js/3872c9ae3f42.js"></script>', result)
- self.assertIn('<script type="text/javascript" src="/static/CACHE/js/cd8870829421.js"></script>', result)
-
-
-class OfflineGenerationTestCaseWithError(OfflineTestCaseMixin, TestCase):
+ self.assertEqual(engine, 'jinja2')
+ self.assertIn(
+ '<link rel="stylesheet" href="/static/CACHE/css/'
+ '78bd7a762e2d.css" type="text/css" />', result)
+ self.assertIn(
+ '<link rel="stylesheet" href="/static/CACHE/css/'
+ 'e31030430724.css" type="text/css" />', result)
+
+ self.assertIn(
+ '<script type="text/javascript" src="/static/CACHE/js/'
+ '3872c9ae3f42.js"></script>', result)
+ self.assertIn(
+ '<script type="text/javascript" src="/static/CACHE/js/'
+ 'cd8870829421.js"></script>', result)
+
+
+class OfflineCompressTestCaseWithError(OfflineTestCaseMixin, TestCase):
templates_dir = 'test_error_handling'
-
- def setUp(self):
- self._old_compress_precompilers = settings.COMPRESS_PRECOMPILERS
- settings.COMPRESS_PRECOMPILERS = (('text/coffeescript', 'non-existing-binary'),)
- super(OfflineGenerationTestCaseWithError, self).setUp()
+ additional_test_settings = {
+ 'COMPRESS_PRECOMPILERS': (('text/coffeescript', 'nonexisting-binary'),)
+ }
def _test_offline(self, engine):
"""
True, as otherwise errors in configuration will never show in
production.
"""
- self._old_debug = settings.DEBUG
-
- try:
- settings.DEBUG = True
- self.assertRaises(CommandError, CompressCommand().compress, engine=engine)
-
- settings.DEBUG = False
- self.assertRaises(CommandError, CompressCommand().compress, engine=engine)
-
- finally:
- settings.DEBUG = self._old_debug
-
- def tearDown(self):
- settings.COMPRESS_PRECOMPILERS = self._old_compress_precompilers
- super(OfflineGenerationTestCaseWithError, self).tearDown()
-
-
-class OfflineGenerationTestCase(OfflineTestCaseMixin, TestCase):
- templates_dir = "basic"
- expected_hash = "f5e179b8eca4"
-
- def test_rendering_without_manifest_raises_exception(self):
- # flush cached manifest
- flush_offline_manifest()
- self.assertRaises(OfflineGenerationError,
- self.template.render, Context({}))
-
- @unittest.skipIf(not _TEST_JINJA2, "No Jinja2 testing")
- def test_rendering_without_manifest_raises_exception_jinja2(self):
- # flush cached manifest
- flush_offline_manifest()
- self.assertRaises(OfflineGenerationError,
- self.template_jinja2.render, {})
-
- def _test_deleting_manifest_does_not_affect_rendering(self, engine):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
- get_offline_manifest()
- manifest_path = os.path.join('CACHE', 'manifest.json')
- if default_storage.exists(manifest_path):
- default_storage.delete(manifest_path)
- self.assertEqual(1, count)
- self.assertEqual([
- '<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (self.expected_hash, ),
- ], result)
- rendered_template = self._render_template(engine)
- self.assertEqual(rendered_template, "".join(result) + "\n")
+ with self.settings(DEBUG=True):
+ self.assertRaises(
+ CommandError, CompressCommand().compress, engine=engine)
+
+ with self.settings(DEBUG=False):
+ self.assertRaises(
+ CommandError, CompressCommand().compress, engine=engine)
+
+
+class OfflineCompressEmptyTag(OfflineTestCaseMixin, TestCase):
+ """
+ In case of a compress template tag with no content, an entry
+ will be added to the manifest with an empty string as value.
+ This test makes sure there is no recompression happening when
+ compressor encounters such an emptystring in the manifest.
+ """
+ templates_dir = 'basic'
+ expected_hash = 'f5e179b8eca4'
+ engines = ('django',)
- def test_deleting_manifest_does_not_affect_rendering(self):
- for engine in self.engines:
- self._test_deleting_manifest_does_not_affect_rendering(engine)
-
- def test_requires_model_validation(self):
- self.assertFalse(CompressCommand.requires_model_validation)
-
- def test_get_loaders(self):
- old_loaders = settings.TEMPLATE_LOADERS
- settings.TEMPLATE_LOADERS = (
- ('django.template.loaders.cached.Loader', (
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
- )),
- )
- try:
- from django.template.loaders.filesystem import Loader as FileSystemLoader
- from django.template.loaders.app_directories import Loader as AppDirectoriesLoader
- except ImportError:
- pass
- else:
- loaders = CompressCommand().get_loaders()
- self.assertTrue(isinstance(loaders[0], FileSystemLoader))
- self.assertTrue(isinstance(loaders[1], AppDirectoriesLoader))
- finally:
- settings.TEMPLATE_LOADERS = old_loaders
+ def _test_offline(self, engine):
+ count, result = CompressCommand().compress(
+ log=self.log, verbosity=self.verbosity, engine=engine)
+ manifest = get_offline_manifest()
+ manifest[list(manifest)[0]] = ''
+ self.assertEqual(self._render_template(engine), '\n')
-class OfflineGenerationBlockSuperBaseCompressed(OfflineTestCaseMixin, TestCase):
- template_names = ["base.html", "base2.html", "test_compressor_offline.html"]
+class OfflineCompressBlockSuperBaseCompressed(OfflineTestCaseMixin, TestCase):
+ template_names = ['base.html', 'base2.html',
+ 'test_compressor_offline.html']
templates_dir = 'test_block_super_base_compressed'
expected_hash = ['028c3fc42232', '2e9d3f5545a6', 'f8891c416981']
# Block.super not supported for Jinja2 yet.
- engines = ("django",)
+ engines = ('django',)
def setUp(self):
- super(OfflineGenerationBlockSuperBaseCompressed, self).setUp()
+ super(OfflineCompressBlockSuperBaseCompressed, self).setUp()
self.template_paths = []
self.templates = []
for template_name in self.template_names:
- template_path = os.path.join(settings.TEMPLATE_DIRS[0], template_name)
+ template_path = os.path.join(
+ settings.TEMPLATES[0]['DIRS'][0], template_name)
self.template_paths.append(template_path)
- with io.open(template_path, encoding=settings.FILE_CHARSET) as file:
- template = Template(file.read())
+ with io.open(template_path,
+ encoding=settings.FILE_CHARSET) as file_:
+ template = Template(file_.read())
self.templates.append(template)
def _render_template(self, template, engine):
- if engine == "django":
+ if engine == 'django':
return template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
- elif engine == "jinja2":
- return template.render(settings.COMPRESS_OFFLINE_CONTEXT) + "\n"
+ elif engine == 'jinja2':
+ return template.render(settings.COMPRESS_OFFLINE_CONTEXT) + '\n'
else:
return None
def _test_offline(self, engine):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ count, result = CompressCommand().compress(
+ log=self.log, verbosity=self.verbosity, engine=engine)
self.assertEqual(len(self.expected_hash), count)
for expected_hash, template in zip(self.expected_hash, self.templates):
- expected_output = '<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (expected_hash, )
- self.assertIn(expected_output, result)
+ expected = ('<script type="text/javascript" src="/static/CACHE/js/'
+ '%s.js"></script>' % (expected_hash, ))
+ self.assertIn(expected, result)
rendered_template = self._render_template(template, engine)
- self.assertEqual(rendered_template, expected_output + '\n')
-
+ self.assertEqual(rendered_template, expected + '\n')
-class OfflineGenerationInlineNonAsciiTestCase(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_inline_non_ascii"
- def setUp(self):
- self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
- settings.COMPRESS_OFFLINE_CONTEXT = {
+class OfflineCompressInlineNonAsciiTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_inline_non_ascii'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': {
'test_non_ascii_value': '\u2014',
}
- super(OfflineGenerationInlineNonAsciiTestCase, self).setUp()
-
- def tearDown(self):
- self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
- super(OfflineGenerationInlineNonAsciiTestCase, self).tearDown()
+ }
def _test_offline(self, engine):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ count, result = CompressCommand().compress(
+ log=self.log, verbosity=self.verbosity, engine=engine)
rendered_template = self._render_template(engine)
- self.assertEqual(rendered_template, "".join(result) + "\n")
+ self.assertEqual(rendered_template, ''.join(result) + '\n')
-class OfflineGenerationComplexTestCase(OfflineTestCaseMixin, TestCase):
- templates_dir = "test_complex"
-
- def setUp(self):
- self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
- settings.COMPRESS_OFFLINE_CONTEXT = {
+class OfflineCompressComplexTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_complex'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': {
'condition': 'OK!',
# Django templating does not allow definition of tuples in the
- # templates. Make sure this is same as test_templates_jinja2/test_complex.
- 'my_names': ("js/one.js", "js/nonasc.js"),
+ # templates.
+ # Make sure this is same as test_templates_jinja2/test_complex.
+ 'my_names': ('js/one.js', 'js/nonasc.js'),
}
- super(OfflineGenerationComplexTestCase, self).setUp()
-
- def tearDown(self):
- self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
- super(OfflineGenerationComplexTestCase, self).tearDown()
+ }
def _test_offline(self, engine):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+ count, result = CompressCommand().compress(
+ log=self.log, verbosity=self.verbosity, engine=engine)
self.assertEqual(3, count)
self.assertEqual([
- '<script type="text/javascript" src="/static/CACHE/js/0e8807bebcee.js"></script>',
- '<script type="text/javascript" src="/static/CACHE/js/eed1d222933e.js"></script>',
- '<script type="text/javascript" src="/static/CACHE/js/00b4baffe335.js"></script>',
+ '<script type="text/javascript" src="/static/CACHE/js/'
+ '0e8807bebcee.js"></script>',
+ '<script type="text/javascript" src="/static/CACHE/js/'
+ 'eed1d222933e.js"></script>',
+ '<script type="text/javascript" src="/static/CACHE/js/'
+ '00b4baffe335.js"></script>',
], result)
rendered_template = self._render_template(engine)
result = (result[0], result[2])
- self.assertEqual(rendered_template, "".join(result) + "\n")
-
-
-# Coffin does not work on Python 3.2+ due to:
-# The line at coffin/template/__init__.py:15
-# from library import *
-# causing 'ImportError: No module named library'.
-# 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"
- engines = ("jinja2",)
-
- def _get_jinja2_env(self):
- import jinja2
- from coffin.common import env
- from compressor.contrib.jinja2ext import CompressorExtension
-
- # Could have used the env.add_extension method, but it's only available
- # in Jinja2 v2.5
- new_env = jinja2.Environment(extensions=[CompressorExtension])
- env.extensions.update(new_env.extensions)
-
- return env
-
-
-# Jingo does not work when using Python 3.2 due to the use of Unicode string
-# prefix (and possibly other stuff), but it actually works when using Python 3.3
-# since it tolerates the use of the Unicode string prefix. Python 3.3 support
-# 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"
- engines = ("jinja2",)
-
- def _get_jinja2_env(self):
- import jinja2
- import jinja2.ext
- from jingo import env
- from compressor.contrib.jinja2ext import CompressorExtension
- from compressor.offline.jinja2 import SpacelessExtension, url_for
-
- # Could have used the env.add_extension method, but it's only available
- # in Jinja2 v2.5
- new_env = jinja2.Environment(extensions=[CompressorExtension, SpacelessExtension, jinja2.ext.with_])
- env.extensions.update(new_env.extensions)
- env.globals['url_for'] = url_for
-
- return env
+ self.assertEqual(rendered_template, ''.join(result) + '\n')
from __future__ import with_statement
import os
+import unittest
try:
import lxml
except ImportError:
html5lib = None
-try:
- from BeautifulSoup import BeautifulSoup
-except ImportError:
- BeautifulSoup = None
-
-from django.utils import unittest
from django.test.utils import override_settings
from compressor.base import SOURCE_HUNK, SOURCE_FILE
class ParserTestCase(object):
def setUp(self):
- self.old_parser = settings.COMPRESS_PARSER
- settings.COMPRESS_PARSER = self.parser_cls
+ self.override_settings = self.settings(COMPRESS_PARSER=self.parser_cls)
+ self.override_settings.__enter__()
super(ParserTestCase, self).setUp()
def tearDown(self):
- settings.COMPRESS_PARSER = self.old_parser
+ self.override_settings.__exit__(None, None, None)
@unittest.skipIf(lxml is None, 'lxml not found')
split[1][3].attrib,
split[1][3].text))
+ @override_settings(COMPRESS_ENABLED=False)
def test_css_return_if_off(self):
- settings.COMPRESS_ENABLED = False
# Yes, they are semantically equal but attributes might be
# scrambled in unpredictable order. A more elaborate check
# would require parsing both arguments with a different parser
self.assertEqual(len(self.js), len(self.js_node.output()))
-@unittest.skipIf(BeautifulSoup is None, 'BeautifulSoup not found')
class BeautifulSoupParserTests(ParserTestCase, CompressorTestCase):
parser_cls = 'compressor.parser.BeautifulSoupParser'
+ # just like in the Html5LibParserTests, provide special tests because
+ # in bs4 attributes are held in dictionaries
+
+ def test_css_split(self):
+ split = self.css_node.split_contents()
+ out0 = (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'css', 'one.css'),
+ 'css/one.css',
+ None,
+ None,
+ )
+ self.assertEqual(out0, split[0][:3] + (split[0][3].tag,
+ split[0][3].attrib))
+ out1 = (
+ SOURCE_HUNK,
+ 'p { border:5px solid green;}',
+ None,
+ '<style type="text/css">p { border:5px solid green;}</style>',
+ )
+ self.assertEqual(out1, split[1][:3] +
+ (self.css_node.parser.elem_str(split[1][3]),))
+ out2 = (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'css', 'two.css'),
+ 'css/two.css',
+ None,
+ None,
+ )
+ self.assertEqual(out2, split[2][:3] + (split[2][3].tag,
+ split[2][3].attrib))
+
+ @override_settings(COMPRESS_ENABLED=False)
+ def test_css_return_if_off(self):
+ # in addition to unspecified attribute order,
+ # bs4 output doesn't have the extra space, so we add that here
+ fixed_output = self.css_node.output().replace('"/>', '" />')
+ self.assertEqual(len(self.css), len(fixed_output))
class HtmlParserTests(ParserTestCase, CompressorTestCase):
from django.test import TestCase
+from django.test.utils import override_settings
from mock import Mock
-from compressor.conf import settings
from compressor.css import CssCompressor
from compressor.js import JsCompressor
from compressor.signals import post_compress
+@override_settings(
+ COMPRESS_ENABLED=True,
+ COMPRESS_PRECOMPILERS=(),
+ COMPRESS_DEBUG_TOGGLE='nocompress'
+)
class PostCompressSignalTestCase(TestCase):
def setUp(self):
- settings.COMPRESS_ENABLED = True
- settings.COMPRESS_PRECOMPILERS = ()
- settings.COMPRESS_DEBUG_TOGGLE = 'nocompress'
self.css = """\
<link rel="stylesheet" href="/static/css/one.css" type="text/css" />
<style type="text/css">p { border:5px solid green;}</style>
from django.core.files.base import ContentFile
from django.core.files.storage import get_storage_class
from django.test import TestCase
+from django.test.utils import override_settings
from django.utils.functional import LazyObject
from compressor import storage
self._wrapped = get_storage_class('compressor.storage.GzipCompressorFileStorage')()
+@override_settings(COMPRESS_ENABLED=True)
class StorageTestCase(TestCase):
def setUp(self):
- self.old_enabled = settings.COMPRESS_ENABLED
- settings.COMPRESS_ENABLED = True
self.default_storage = storage.default_storage
storage.default_storage = GzipStorage()
def tearDown(self):
storage.default_storage = self.default_storage
- settings.COMPRESS_ENABLED = self.old_enabled
def test_gzip_storage(self):
storage.default_storage.save('test.txt', ContentFile('yeah yeah'))
--- /dev/null
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("{{ content|default:"Ooops!" }}");
+ </script>
+{% endblock %}
+{% endspaceless %}
--- /dev/null
+{% extends "base.html" %}
+{% load compress %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ {{ block.super }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
+
+{% block css %}{% endblock %}
from compressor.signals import post_compress
from compressor.tests.test_base import css_tag, test_dir
+from sekizai.context import SekizaiContext
-def render(template_string, context_dict=None):
+
+def render(template_string, context_dict=None, context=None):
"""
A shortcut for testing template output.
"""
if context_dict is None:
context_dict = {}
- c = Context(context_dict)
+ if context is None:
+ context = Context
+ c = context(context_dict)
t = Template(template_string)
return t.render(c).strip()
+@override_settings(COMPRESS_ENABLED=True)
class TemplatetagTestCase(TestCase):
def setUp(self):
- self.old_enabled = settings.COMPRESS_ENABLED
- settings.COMPRESS_ENABLED = True
self.context = {'STATIC_URL': settings.COMPRESS_URL}
- def tearDown(self):
- settings.COMPRESS_ENABLED = self.old_enabled
-
def test_empty_tag(self):
template = """{% load compress %}{% compress js %}{% block js %}
{% endblock %}{% endcompress %}"""
context = kwargs['context']
self.assertEqual('foo', context['compressed']['name'])
+ def test_sekizai_only_once(self):
+ template = """{% load sekizai_tags %}{% addtoblock "js" %}
+ <script type="text/javascript">var tmpl="{% templatetag openblock %} if x == 3 %}x IS 3{% templatetag openblock %} endif %}"</script>
+ {% endaddtoblock %}{% render_block "js" postprocessor "compressor.contrib.sekizai.compress" %}
+ """
+ out = '<script type="text/javascript" src="/static/CACHE/js/e9fce10d884d.js"></script>'
+ self.assertEqual(out, render(template, self.context, SekizaiContext))
+
class PrecompilerTemplatetagTestCase(TestCase):
def setUp(self):
- self.old_enabled = settings.COMPRESS_ENABLED
- self.old_precompilers = settings.COMPRESS_PRECOMPILERS
-
precompiler = os.path.join(test_dir, 'precompiler.py')
python = sys.executable
- settings.COMPRESS_ENABLED = True
- settings.COMPRESS_PRECOMPILERS = (
- ('text/coffeescript', '%s %s' % (python, precompiler)),
- ('text/less', '%s %s' % (python, precompiler)),
- )
+ override_settings = {
+ 'COMPRESS_ENABLED': True,
+ 'COMPRESS_PRECOMPILERS': (
+ ('text/coffeescript', '%s %s' % (python, precompiler)),
+ ('text/less', '%s %s' % (python, precompiler)),
+ )
+ }
+ self.override_settings = self.settings(**override_settings)
+ self.override_settings.__enter__()
+
self.context = {'STATIC_URL': settings.COMPRESS_URL}
def tearDown(self):
- settings.COMPRESS_ENABLED = self.old_enabled
- settings.COMPRESS_PRECOMPILERS = self.old_precompilers
+ self.override_settings.__exit__(None, None, None)
def test_compress_coffeescript_tag(self):
template = """{% load compress %}{% compress js %}
--- /dev/null
+from django.test import TestCase
+from django.test.utils import override_settings
+from django.conf import settings
+import django.contrib.staticfiles.finders
+import django
+
+import compressor.utils.staticfiles
+
+from imp import reload
+
+
+def get_apps_without_staticfiles(apps):
+ return [x for x in apps if x != 'django.contrib.staticfiles']
+
+
+def get_apps_with_staticfiles_using_appconfig(apps):
+ return get_apps_without_staticfiles(apps) + [
+ 'django.contrib.staticfiles.apps.StaticFilesConfig',
+ ]
+
+
+class StaticFilesTestCase(TestCase):
+
+ def test_has_finders_from_staticfiles(self):
+ self.assertTrue(compressor.utils.staticfiles.finders is
+ django.contrib.staticfiles.finders)
+
+ def test_has_finders_from_staticfiles_if_configured_per_appconfig(self):
+ apps = get_apps_with_staticfiles_using_appconfig(
+ settings.INSTALLED_APPS)
+ try:
+ with override_settings(INSTALLED_APPS=apps):
+ reload(compressor.utils.staticfiles)
+ self.assertTrue(compressor.utils.staticfiles.finders is
+ django.contrib.staticfiles.finders)
+ finally:
+ reload(compressor.utils.staticfiles)
+
+ def test_finders_is_none_if_staticfiles_is_not_installed(self):
+ apps = get_apps_without_staticfiles(settings.INSTALLED_APPS)
+ try:
+ with override_settings(INSTALLED_APPS=apps):
+ reload(compressor.utils.staticfiles)
+ self.assertTrue(compressor.utils.staticfiles.finders is None)
+ finally:
+ reload(compressor.utils.staticfiles)
from __future__ import absolute_import, unicode_literals
+from django.apps import apps
from django.core.exceptions import ImproperlyConfigured
from compressor.conf import settings
-if "django.contrib.staticfiles" in settings.INSTALLED_APPS:
+
+if apps.is_installed("django.contrib.staticfiles"):
from django.contrib.staticfiles import finders # noqa
if ("compressor.finders.CompressorFinder"
+++ /dev/null
-# -*- coding: utf-8 -*-
-"""Advanced string formatting for Python >= 2.4.
-
-An implementation of the advanced string formatting (PEP 3101).
-
-Author: Florent Xicluna
-"""
-
-from __future__ import unicode_literals
-
-import re
-
-from django.utils import six
-
-_format_str_re = re.compile(
- r'((?<!{)(?:{{)+' # '{{'
- r'|(?:}})+(?!})' # '}}
- r'|{(?:[^{](?:[^{}]+|{[^{}]*})*)?})' # replacement field
-)
-_format_sub_re = re.compile(r'({[^{}]*})') # nested replacement field
-_format_spec_re = re.compile(
- r'((?:[^{}]?[<>=^])?)' # alignment
- r'([-+ ]?)' # sign
- r'(#?)' r'(\d*)' r'(,?)' # base prefix, minimal width, thousands sep
- r'((?:\.\d+)?)' # precision
- r'(.?)$' # type
-)
-_field_part_re = re.compile(
- r'(?:(\[)|\.|^)' # start or '.' or '['
- r'((?(1)[^]]*|[^.[]*))' # part
- r'(?(1)(?:\]|$)([^.[]+)?)' # ']' and invalid tail
-)
-
-_format_str_sub = _format_str_re.sub
-
-
-def _is_integer(value):
- return hasattr(value, '__index__')
-
-
-def _strformat(value, format_spec=""):
- """Internal string formatter.
-
- It implements the Format Specification Mini-Language.
- """
- m = _format_spec_re.match(str(format_spec))
- if not m:
- raise ValueError('Invalid conversion specification')
- align, sign, prefix, width, comma, precision, conversion = m.groups()
- is_numeric = hasattr(value, '__float__')
- is_integer = is_numeric and _is_integer(value)
- if prefix and not is_integer:
- raise ValueError('Alternate form (#) not allowed in %s format '
- 'specifier' % (is_numeric and 'float' or 'string'))
- if is_numeric and conversion == 'n':
- # Default to 'd' for ints and 'g' for floats
- conversion = is_integer and 'd' or 'g'
- elif sign:
- if not is_numeric:
- raise ValueError("Sign not allowed in string format specifier")
- if conversion == 'c':
- raise ValueError("Sign not allowed with integer "
- "format specifier 'c'")
- if comma:
- # TODO: thousand separator
- pass
- try:
- if ((is_numeric and conversion == 's') or (not is_integer and conversion in set('cdoxX'))):
- raise ValueError
- if conversion == 'c':
- conversion = 's'
- value = chr(value % 256)
- rv = ('%' + prefix + precision + (conversion or 's')) % (value,)
- except ValueError:
- raise ValueError("Unknown format code %r for object of type %r" %
- (conversion, value.__class__.__name__))
- if sign not in '-' and value >= 0:
- # sign in (' ', '+')
- rv = sign + rv
- if width:
- zero = (width[0] == '0')
- width = int(width)
- else:
- zero = False
- width = 0
- # Fastpath when alignment is not required
- if width <= len(rv):
- if not is_numeric and (align == '=' or (zero and not align)):
- raise ValueError("'=' alignment not allowed in string format "
- "specifier")
- return rv
- fill, align = align[:-1], align[-1:]
- if not fill:
- fill = zero and '0' or ' '
- if align == '^':
- padding = width - len(rv)
- # tweak the formatting if the padding is odd
- if padding % 2:
- rv += fill
- rv = rv.center(width, fill)
- elif align == '=' or (zero and not align):
- if not is_numeric:
- raise ValueError("'=' alignment not allowed in string format "
- "specifier")
- if value < 0 or sign not in '-':
- rv = rv[0] + rv[1:].rjust(width - 1, fill)
- else:
- rv = rv.rjust(width, fill)
- elif align in ('>', '=') or (is_numeric and not align):
- # numeric value right aligned by default
- rv = rv.rjust(width, fill)
- else:
- rv = rv.ljust(width, fill)
- return rv
-
-
-def _format_field(value, parts, conv, spec, want_bytes=False):
- """Format a replacement field."""
- for k, part, _ in parts:
- if k:
- if part.isdigit():
- value = value[int(part)]
- else:
- value = value[part]
- else:
- value = getattr(value, part)
- if conv:
- value = ((conv == 'r') and '%r' or '%s') % (value,)
- if hasattr(value, '__format__'):
- value = value.__format__(spec)
- elif hasattr(value, 'strftime') and spec:
- value = value.strftime(str(spec))
- else:
- value = _strformat(value, spec)
- if want_bytes and isinstance(value, six.text_type):
- return str(value)
- return value
-
-
-class FormattableString(object):
- """Class which implements method format().
-
- The method format() behaves like str.format() in python 2.6+.
-
- >>> FormattableString('{a:5}').format(a=42)
- ... # Same as '{a:5}'.format(a=42)
- ' 42'
-
- """
-
- __slots__ = '_index', '_kwords', '_nested', '_string', 'format_string'
-
- def __init__(self, format_string):
- self._index = 0
- self._kwords = {}
- self._nested = {}
-
- self.format_string = format_string
- self._string = _format_str_sub(self._prepare, format_string)
-
- def __eq__(self, other):
- if isinstance(other, FormattableString):
- return self.format_string == other.format_string
- # Compare equal with the original string.
- return self.format_string == other
-
- def _prepare(self, match):
- # Called for each replacement field.
- part = match.group(0)
- if part[0] == part[-1]:
- # '{{' or '}}'
- assert part == part[0] * len(part)
- return part[:len(part) // 2]
- repl = part[1:-1]
- field, _, format_spec = repl.partition(':')
- literal, sep, conversion = field.partition('!')
- if sep and not conversion:
- raise ValueError("end of format while looking for "
- "conversion specifier")
- if len(conversion) > 1:
- raise ValueError("expected ':' after format specifier")
- if conversion not in 'rsa':
- raise ValueError("Unknown conversion specifier %s" %
- str(conversion))
- name_parts = _field_part_re.findall(literal)
- if literal[:1] in '.[':
- # Auto-numbering
- if self._index is None:
- raise ValueError("cannot switch from manual field "
- "specification to automatic field numbering")
- name = str(self._index)
- self._index += 1
- if not literal:
- del name_parts[0]
- else:
- name = name_parts.pop(0)[1]
- if name.isdigit() and self._index is not None:
- # Manual specification
- if self._index:
- raise ValueError("cannot switch from automatic field "
- "numbering to manual field specification")
- self._index = None
- empty_attribute = False
- for k, v, tail in name_parts:
- if not v:
- empty_attribute = True
- if tail:
- raise ValueError("Only '.' or '[' may follow ']' "
- "in format field specifier")
- if name_parts and k == '[' and not literal[-1] == ']':
- raise ValueError("Missing ']' in format string")
- if empty_attribute:
- raise ValueError("Empty attribute in format string")
- if '{' in format_spec:
- format_spec = _format_sub_re.sub(self._prepare, format_spec)
- rv = (name_parts, conversion, format_spec)
- self._nested.setdefault(name, []).append(rv)
- else:
- rv = (name_parts, conversion, format_spec)
- self._kwords.setdefault(name, []).append(rv)
- return r'%%(%s)s' % id(rv)
-
- def format(self, *args, **kwargs):
- """Same as str.format() and unicode.format() in Python 2.6+."""
- if args:
- kwargs.update(dict((str(i), value)
- for (i, value) in enumerate(args)))
- # Encode arguments to ASCII, if format string is bytes
- want_bytes = isinstance(self._string, str)
- params = {}
- for name, items in self._kwords.items():
- value = kwargs[name]
- for item in items:
- parts, conv, spec = item
- params[str(id(item))] = _format_field(value, parts, conv, spec,
- want_bytes)
- for name, items in self._nested.items():
- value = kwargs[name]
- for item in items:
- parts, conv, spec = item
- spec = spec % params
- params[str(id(item))] = _format_field(value, parts, conv, spec,
- want_bytes)
- return self._string % params
-
-
-def selftest():
- import datetime
- F = FormattableString
-
- assert F("{0:{width}.{precision}s}").format('hello world',
- width=8, precision=5) == 'hello '
-
- d = datetime.date(2010, 9, 7)
- assert F("The year is {0.year}").format(d) == "The year is 2010"
- assert F("Tested on {0:%Y-%m-%d}").format(d) == "Tested on 2010-09-07"
- print('Test successful')
-
-if __name__ == '__main__':
- selftest()
Changelog
=========
+v2.0 (01/07/2015)
+-----------------
+
+`Full Changelog <https://github.com/django-compressor/django-compressor/compare/1.6...2.0>`_
+
+- Add Django 1.9 compatibility
+
+- Remove official support for Django 1.4 and 1.7
+
+- Add official support for Python 3.5
+
+- Remove official support for Python 2.6
+
+- Remove support for coffin and jingo
+
+- Fix Jinja2 compatibility for Django 1.8+
+
+- Stop bundling vendored versions of rcssmin and rjsmin, make them proper dependencies
+
+- Remove support for CSSTidy
+
+- Remove support for beautifulsoup 3.
+
+- Replace cssmin by csscompressor (cssmin is still available for backwards-compatibility but points to rcssmin)
+
+
+v1.6 (11/19/2015)
+-----------------
+
+`Full Changelog <https://github.com/django-compressor/django-compressor/compare/1.5...1.6>`_
+
+- Upgrade rcssmin and rjsmin
+
+- Apply CssAbsoluteFilter to precompiled css even when compression is disabled
+
+- Add optional caching to CompilerFilter to avoid re-compiling unchanged files
+
+- Fix various deprecation warnings on Django 1.7 / 1.8
+
+- Fix TemplateFilter
+
+- Fix double-rendering bug with sekizai extension
+
+- Fix debug mode using destination directory instead of staticfiles finders first
+
+- Removed some silent exception catching in compress command
+
+
v1.5 (03/27/2015)
-----------------
-`Full Changelog <https://github.com/django-compressor/django-compressor/compare/1.4...HEAD>`_
+`Full Changelog <https://github.com/django-compressor/django-compressor/compare/1.4...1.5>`_
- Fix compress command and run automated tests for Django 1.8
- Made ``CssCompressor`` class easier to extend.
-- Added support for explictly stating the block being ended.
+- Added support for explicitly stating the block being ended.
- Added rcssmin and updated rjsmin.
------
- Fixed staticfiles support to also use its finder API to find files during
- developement -- when the static files haven't been collected in
+ development -- when the static files haven't been collected in
``STATIC_ROOT``.
- Fixed regression with the ``COMPRESS`` setting, pre-compilation and
compress template tag does. See the
:ref:`pre-compression <pre-compression>` docs for more information.
-- Various perfomance improvements by better caching and mtime cheking.
+- Various performance improvements by better caching and mtime cheking.
- Deprecated ``COMPRESS_LESSC_BINARY`` setting because it's now
superseded by the :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS`
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.
+#. Head over to #django-compressor on the `freenode`_ IRC network for help and to
+ discuss the development.
+#. Open an issue on GitHub explaining your ideas.
In a nutshell
behind-the-scenes
jinja2
django-sekizai
+ reactjs
contributing
changelog
]))
template.render({'STATIC_URL': settings.STATIC_URL})
-For coffin users
-----------------
-
-Coffin_ makes it very easy to include additional Jinja2_ extensions as it
-only requires to add extension to ``JINJA2_EXTENSIONS`` at main settings
-module::
-
- JINJA2_EXTENSIONS = [
- 'compressor.contrib.jinja2ext.CompressorExtension',
- ]
-
-And that's it - our extension is loaded and ready to be used.
-
Jinja2 Offline Compression Support
==================================
If you're using Jinja2, you're likely to have a Jinja2 template loader in the
``TEMPLATE_LOADERS`` setting, otherwise Django won't know how to load Jinja2
-templates. You could use Jingo_ or your own custom loader. Coffin_ works
-differently by providing a custom rendering method instead of a custom loader.
-
-Unfortunately, Jingo_ does not implement such a method in its loader;
-Coffin_ does not seem to have a template loader in the first place.
-Read on to understand how to make Compressor work nicely with Jingo_
-and Coffin_.
+templates.
By default, if you don't override the ``TEMPLATE_LOADERS`` setting,
it will include the app directories loader that searches for templates under
``TEMPLATE_LOADERS`` setting and specify the custom location in the
``TEMPLATE_DIRS`` setting.
-For Jingo users
----------------
-You should configure ``TEMPLATE_LOADERS`` as such::
-
- TEMPLATE_LOADERS = (
- 'jingo.Loader',
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
- )
-
- def COMPRESS_JINJA2_GET_ENVIRONMENT():
- # TODO: ensure the CompressorExtension is installed with Jingo via
- # Jingo's JINJA_CONFIG setting.
- # Additional globals, filters, tests,
- # and extensions used within {%compress%} blocks must be configured
- # with Jingo.
- from jingo import env
-
- return env
-
-This will enable the Jingo_ loader to load Jinja2 templates and the other
-loaders to report the templates location(s).
-
-For Coffin users
-----------------
-You might want to configure ``TEMPLATE_LOADERS`` as such::
-
- TEMPLATE_LOADERS = (
- 'django.template.loaders.filesystem.Loader',
- 'django.template.loaders.app_directories.Loader',
- )
-
- def COMPRESS_JINJA2_GET_ENVIRONMENT():
- # TODO: ensure the CompressorExtension is installed with Coffin
- # as described in the "In-Request Support" section above.
- # Additional globals, filters, tests,
- # and extensions used within {%compress%} blocks must be configured
- # with Coffin.
- from coffin.common import env
-
- return env
-
-Again, if you have the Jinja2 templates in the app template directories, you're
-done here. Otherwise, specify the location in ``TEMPLATE_DIRS``.
-
Using your custom loader
------------------------
You should configure ``TEMPLATE_LOADERS`` as such::
You could implement the `get_template_sources` method in your loader or make
use of the Django's builtin loaders to report the Jinja2 template location(s).
-Python 3 Support
-----------------
-Jingo with Jinja2 are tested and work on Python 2.6, 2.7, and 3.3.
-Coffin with Jinja2 are tested and work on Python 2.6 and 2.7 only.
-Jinja2 alone (with custom loader) are tested and work on Python 2.6, 2.7 and
-3.3 only.
.. _Jinja2: http://jinja.pocoo.org/docs/
-.. _Coffin: http://pypi.python.org/pypi/Coffin
-.. _Jingo: https://jingo.readthedocs.org/en/latest/
)
* Define :attr:`COMPRESS_ROOT <django.conf.settings.COMPRESS_ROOT>` in settings
- if you don't have already ``STATIC_ROOT`` or if you want it in a different
+ if you don't have already ``STATIC_ROOT`` or if you want it in a different
folder.
.. _staticfiles: http://docs.djangoproject.com/en/dev/ref/contrib/staticfiles/
.. _dependencies:
-Dependencies
+Optional Dependencies
------------
-Required
-^^^^^^^^
-
-In case you're installing Django Compressor differently
-(e.g. from the Git repo), make sure to install the following
-dependencies.
-
-- django-appconf_
-
- Used internally to handle Django's settings, this is
- automatically installed when following the above
- installation instructions.
-
- pip install django-appconf
-
-Optional
-^^^^^^^^
-
- BeautifulSoup_
For the :attr:`parser <django.conf.settings.COMPRESS_PARSER>`
``compressor.parser.BeautifulSoupParser`` and
``compressor.parser.LxmlParser``::
- pip install "BeautifulSoup<4.0"
+ pip install beautifulsoup4
- lxml_
pip install slimit
+- `csscompressor`_
+
+ For the :ref:`csscompressor filter <csscompressor_filter>`
+ ``compressor.filters.cssmin.CSSCompressorFilter``::
+
+ pip install csscompressor
+
.. _BeautifulSoup: http://www.crummy.com/software/BeautifulSoup/
-.. _lxml: http://codespeak.net/lxml/
+.. _lxml: http://lxml.de/
.. _libxml2: http://xmlsoft.org/
-.. _html5lib: http://code.google.com/p/html5lib/
+.. _html5lib: https://github.com/html5lib/html5lib-python
.. _`Slim It`: https://github.com/rspivak/slimit
.. _django-appconf: http://pypi.python.org/pypi/django-appconf/
.. _versiontools: http://pypi.python.org/pypi/versiontools/
--- /dev/null
+.. _reactjs_support:
+
+Facebook React Support
+======================
+
+Assuming you have `npm` available, you can install `babel` via `npm install -g babel` and integrate React with
+Django Compressor by following the `react-tools installation instructions`_ and adding an appropriate
+``COMPRESS_PRECOMPILERS`` setting:
+
+.. code-block:: django
+
+ COMPRESS_PRECOMPILERS = (
+ ('text/jsx', 'cat {infile} | babel > {outfile}'),
+ )
+
+
+.. _react-tools installation instructions: http://facebook.github.io/react/docs/tooling-integration.html#productionizing-precompiled-jsx
+
+If the above approach is not suitable for you, compiling React's jsx files can be done by creating
+a custom precompressor.
+
+Requirements
+------------
+* PyReact>=0.5.2 for compiling jsx files
+* PyExecJS>=1.1.0 required by PyReact (automatically installed when using pip)
+* A Javascript runtime : options include PyV8, Node.js, PhantomJS among others
+
+
+The full list of supported javascript engines can be found here:
+ https://github.com/doloopwhile/PyExecJS
+
+
+Installation
+------------
+1. Place the following code in a Python file (e.g.
+ ``third_party/react_compressor.py``). Also make sure that
+ ``third_party/__init__.py`` exists so the directory is recognized as a
+ Python package.
+
+.. code-block:: django
+
+ from compressor.filters import FilterBase
+ from react import jsx
+
+
+ class ReactFilter(FilterBase):
+
+ def __init__(self, content, *args, **kwargs):
+ self.content = content
+ kwargs.pop('filter_type')
+ super(ReactFilter, self).__init__(content, *args, **kwargs)
+
+ def input(self, **kwargs):
+ return jsx.transform_string(self.content)
+
+
+2. In your Django settings, add the following line:
+
+.. code-block:: django
+
+ COMPRESS_PRECOMPILERS = (
+ ('text/jsx', 'third_party.react_compressor.ReactFilter'),
+ )
+
+Where ``third_party.react_compressor.ReactFilter`` is the full name of your ``ReactFilter`` class.
+
+
+Troubleshooting
+---------------
+If you get "file not found" errors, open your Python command line and make sure
+you are able to import your ``ReactFilter`` class:
+
+.. code-block:: django
+
+ __import__('third_party.react_compressor.ReactFilter')
+
^^^^^^^^^^^^^^^
So assuming your CDN is `Amazon S3`_, you can use the boto_ storage backend
-from the 3rd party app `django-storages`_. Some required settings are::
+from the 3rd party app `django-storages-redux`_. Some required settings are::
AWS_ACCESS_KEY_ID = 'XXXXXXXXXXXXXXXXXXXXX'
AWS_SECRET_ACCESS_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'
#. You need to create a subclass of the remote storage backend you want
to use; below is an example of the boto S3 storage backend from
- django-storages_::
+ django-storages-redux_::
from django.core.files.storage import get_storage_class
from storages.backends.s3boto import S3BotoStorage
.. _CDN: http://en.wikipedia.org/wiki/Content_delivery_network
.. _Amazon S3: https://s3.amazonaws.com/
.. _boto: http://boto.cloudhackers.com/
-.. _django-storages: http://code.welldev.org/django-storages/
+.. _django-storages-redux: http://github.com/jschneier/django-storages
.. _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
feature, and the ``'content'`` in case you're using multiple servers
to serve your content.
- - ``compressor.filters.csstidy.CSSTidyFilter``
-
- A filter that passes the CSS content to the CSSTidy_ tool.
-
- .. attribute:: COMPRESS_CSSTIDY_BINARY
-
- The CSSTidy binary filesystem path.
-
- .. attribute:: COMPRESS_CSSTIDY_ARGUMENTS
-
- The arguments passed to CSSTidy.
-
- ``compressor.filters.datauri.CssDataUriFilter``
A filter for embedding media as `data: URIs`_ in the CSS.
The arguments passed to the compressor. Defaults to --terminal.
- - ``compressor.filters.cssmin.CSSMinFilter``
+ .. _csscompressor_filter:
+
+ - ``compressor.filters.cssmin.CSSCompressorFilter``
- A filter that uses Zachary Voase's Python port of the YUI CSS compression
- algorithm cssmin_.
+ A filter that uses Yury Selivanov's Python port of the YUI CSS compression
+ algorithm csscompressor_.
+
+ - ``compressor.filters.cssmin.rCSSMinFilter``
+
+ A filter that uses the cssmin implementation rCSSmin_ to compress CSS
+ (installed by default).
- ``compressor.filters.cleancss.CleanCSSFilter``
A filter that passes the CSS content to the `clean-css`_ tool.
- .. attribute:: CLEAN_CSS_BINARY
+ .. attribute:: COMPRESS_CLEAN_CSS_BINARY
The clean-css binary filesystem path.
- .. attribute:: CLEAN_CSS_ARGUMENTS
+ .. attribute:: COMPRESS_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/
+ .. _csscompressor: http://pypi.python.org/pypi/csscompressor/
+ .. _rCSSmin: http://opensource.perlig.de/rcssmin/
.. _`clean-css`: https://github.com/GoalSmashers/clean-css/
- ``compressor.filters.jsmin.JSMinFilter``
A filter that uses the jsmin implementation rJSmin_ to compress
- JavaScript code.
+ JavaScript code (installed by default).
.. _slimit_filter:
.. note::
Depending on the implementation, some precompilers might not support
outputting to something else than ``stdout``, so you'll need to omit the
- ``{outfile}`` parameter when working with those. For instance, if you
- are using the Ruby version of lessc, you'll need to set up the
+ ``{outfile}`` parameter when working with those. For instance, if you
+ are using the Ruby version of lessc, you'll need to set up the
precompiler like this::
('text/less', 'lessc {infile}'),
:attr:`~django.conf.settings.COMPRESS_REBUILD_TIMEOUT` and
:attr:`~django.conf.settings.COMPRESS_MINT_DELAY`.
+.. attribute:: COMPRESS_CACHEABLE_PRECOMPILERS
+
+ :Default: ``()``
+
+ An iterable of precompiler mimetypes as defined in :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS`
+ for which the compiler output can be cached based solely on the contents
+ of the input file. This lets Django Compressor avoid recompiling unchanged
+ files. Caching is appropriate for compilers such as CoffeeScript where files
+ are compiled one-to-one, but not for compilers such as SASS that have an
+ ``import`` mechanism for including one file from another. If caching is enabled
+ for such a compiler, Django Compressor will not know to recompile files when a file
+ they import is modified.
+
.. attribute:: COMPRESS_DEBUG_TOGGLE
:Default: None
and the ``django.core.context_processors.request`` context processor.
.. _RequestContext: http://docs.djangoproject.com/en/dev/ref/templates/api/#django.template.RequestContext
-
+
.. attribute:: COMPRESS_CACHE_KEY_FUNCTION
:Default: ``'compressor.cache.simple_cachekey'``
-
+
The function to use when generating the cache key. The function must take
one argument which is the partial key based on the source's hex digest.
It must return the full key as a string.
If available, the ``STATIC_URL`` setting is also added to the context.
+ .. note::
+
+ It is also possible to perform offline compression for multiple
+ contexts by providing a list or tuple of dictionaries, or by providing
+ a dotted string pointing to a generator function.
+
+ This makes it easier to generate contexts dynamically for situations
+ where a user might be able to select a different theme in their user
+ profile, or be served different stylesheets based on other criteria.
+
+ An example of multiple offline contexts by providing a list or tuple::
+
+ # project/settings.py:
+ COMPRESS_OFFLINE_CONTEXT = [
+ {'THEME': 'plain', 'STATIC_URL': STATIC_URL},
+ {'THEME': 'fancy', 'STATIC_URL': STATIC_URL},
+ # ...
+ ]
+
+ An example of multiple offline contexts generated dynamically::
+
+ # project/settings.py:
+ COMPRESS_OFFLINE_CONTEXT = 'project.module.offline_context'
+
+ # project/module.py:
+ from django.conf import settings
+ def offline_context():
+ from project.models import Company
+ for theme in set(Company.objects.values_list('theme', flat=True)):
+ yield {'THEME': theme, 'STATIC_URL': settings.STATIC_URL}
+
.. attribute:: COMPRESS_OFFLINE_MANIFEST
:Default: ``manifest.json``
access it in the `post_compress signal <signals>`.
.. _memcached: http://memcached.org/
-.. _caching documentation: http://docs.djangoproject.com/en/1.2/topics/cache/#memcached
+.. _caching documentation: https://docs.djangoproject.com/en/1.8/topics/cache/#memcached
.. _pre-compression:
The result of running the ``compress`` management command will be cached
in a file called ``manifest.json`` using the :attr:`configured storage
-<django.conf.settings.COMPRESS_STORAGE>` to be able to be transfered from your developement
+<django.conf.settings.COMPRESS_STORAGE>` to be able to be transferred from your development
computer to the server easily.
.. _TEMPLATE_LOADERS: http://docs.djangoproject.com/en/dev/ref/settings/#template-loaders
-flake8
-coverage
-html5lib
-mock
-jinja2
-lxml
-BeautifulSoup
-unittest2
-coffin
-jingo
+flake8==2.4.0
+coverage==3.7.1
+html5lib==0.9999999
+mock==1.0.1
+Jinja2==2.7.3
+lxml==3.4.2
+beautifulsoup4==4.4.0
+django-sekizai==0.9.0
+csscompressor==0.9.4
+rcssmin==1.0.6
+rjsmin==1.0.12
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
'Topic :: Internet :: WWW/HTTP',
],
zip_safe=False,
install_requires=[
'django-appconf >= 0.4',
+ 'rcssmin == 1.0.6',
+ 'rjsmin == 1.0.12',
],
)
[deps]
two =
- flake8
- coverage
- html5lib
- mock
- jinja2
- lxml
- BeautifulSoup
- unittest2
- jingo
- coffin
+ flake8==2.4.0
+ coverage==3.7.1
+ html5lib==0.9999999
+ mock==1.0.1
+ Jinja2==2.7.3
+ lxml==3.4.2
+ beautifulsoup4==4.4.0
+ django-sekizai==0.9.0
+ csscompressor==0.9.4
+ rcssmin==1.0.6
+ rjsmin==1.0.12
three =
- flake8
- coverage
- html5lib
- mock
- jinja2
- lxml
- BeautifulSoup4
- jingo
- coffin
+ flake8==2.4.0
+ coverage==3.7.1
+ html5lib==0.9999999
+ mock==1.0.1
+ Jinja2==2.7.3
+ lxml==3.4.2
+ beautifulsoup4==4.4.0
+ django-sekizai==0.9.0
+ csscompressor==0.9.4
+ rcssmin==1.0.6
+ rjsmin==1.0.12
three_two =
- flake8
- coverage
- html5lib
- mock
- jinja2==2.6
- lxml
- BeautifulSoup4
- jingo
- coffin
-
+ flake8==2.4.0
+ coverage==3.7.1
+ html5lib==0.9999999
+ mock==1.0.1
+ Jinja2==2.6
+ lxml==3.4.2
+ beautifulsoup4==4.4.0
+ django-sekizai==0.9.0
+ csscompressor==0.9.4
+ rcssmin==1.0.6
+ rjsmin==1.0.12
[tox]
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.8.X}
+ {py27,py32,py33,py34,py35}-{1.8.X}
+ {py27,py34,py35}-{1.9.X}
[testenv]
basepython =
- py26: python2.6
py27: python2.7
py32: python3.2
py33: python3.3
py34: python3.4
+ py35: python3.5
usedevelop = true
setenv =
CPPFLAGS=-O0
django-admin.py --version
make test
deps =
- 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
1.8.X: Django>=1.8,<1.9
- py26: {[deps]two}
+ 1.9.X: Django>=1.9,<1.10
py27: {[deps]two}
py32: {[deps]three_two}
py33: {[deps]three}
py34: {[deps]three}
+ py35: {[deps]three}
django-discover-runner