]> review.fuel-infra Code Review - packages/trusty/python-django-compressor.git/commitdiff
Update python-django-compressor package 35/29835/1
authorIvan Udovichenko <iudovichenko@mirantis.com>
Fri, 13 Jan 2017 14:49:31 +0000 (16:49 +0200)
committerIvan Udovichenko <iudovichenko@mirantis.com>
Fri, 13 Jan 2017 14:51:16 +0000 (16:51 +0200)
Version: 2.0

* Source:
   http://archive.ubuntu.com/ubuntu/pool/main/p/python-django-compressor/
   python-django-compressor_2.0-1ubuntu1.dsc

Change-Id: Ifb5bf59f5fce60033439c5c9422800cfd5bc515c

66 files changed:
debian/changelog
debian/control
debian/copyright
debian/patches/disable-django-sekizai.patch [new file with mode: 0644]
debian/patches/remove-failed-test.patch [deleted file]
debian/patches/series
debian/pydist-overrides
debian/rules
python-django-compressor/.travis.yml
python-django-compressor/AUTHORS
python-django-compressor/LICENSE
python-django-compressor/Makefile
python-django-compressor/README.rst
python-django-compressor/compressor/__init__.py
python-django-compressor/compressor/base.py
python-django-compressor/compressor/cache.py
python-django-compressor/compressor/conf.py
python-django-compressor/compressor/contrib/sekizai.py
python-django-compressor/compressor/css.py
python-django-compressor/compressor/filters/__init__.py
python-django-compressor/compressor/filters/base.py
python-django-compressor/compressor/filters/cssmin/__init__.py
python-django-compressor/compressor/filters/cssmin/cssmin.py [deleted file]
python-django-compressor/compressor/filters/cssmin/rcssmin.py [deleted file]
python-django-compressor/compressor/filters/csstidy.py [deleted file]
python-django-compressor/compressor/filters/jsmin/__init__.py
python-django-compressor/compressor/filters/jsmin/rjsmin.py [deleted file]
python-django-compressor/compressor/filters/jsmin/slimit.py [deleted file]
python-django-compressor/compressor/js.py
python-django-compressor/compressor/management/commands/compress.py
python-django-compressor/compressor/management/commands/mtime_cache.py
python-django-compressor/compressor/offline/django.py
python-django-compressor/compressor/parser/__init__.py
python-django-compressor/compressor/parser/beautifulsoup.py
python-django-compressor/compressor/parser/default_htmlparser.py
python-django-compressor/compressor/storage.py
python-django-compressor/compressor/templatetags/compress.py
python-django-compressor/compressor/test_settings.py
python-django-compressor/compressor/tests/static/css/relative_url.css [new file with mode: 0644]
python-django-compressor/compressor/tests/test_base.py
python-django-compressor/compressor/tests/test_filters.py
python-django-compressor/compressor/tests/test_finder.py [new file with mode: 0644]
python-django-compressor/compressor/tests/test_jinja2ext.py
python-django-compressor/compressor/tests/test_mtime_cache.py [new file with mode: 0644]
python-django-compressor/compressor/tests/test_offline.py
python-django-compressor/compressor/tests/test_parsers.py
python-django-compressor/compressor/tests/test_signals.py
python-django-compressor/compressor/tests/test_storages.py
python-django-compressor/compressor/tests/test_templates/test_with_context_super/base.html [new file with mode: 0644]
python-django-compressor/compressor/tests/test_templates/test_with_context_super/test_compressor_offline.html [new file with mode: 0644]
python-django-compressor/compressor/tests/test_templatetags.py
python-django-compressor/compressor/tests/test_utils.py [new file with mode: 0644]
python-django-compressor/compressor/utils/staticfiles.py
python-django-compressor/compressor/utils/stringformat.py [deleted file]
python-django-compressor/docs/changelog.txt
python-django-compressor/docs/contributing.txt
python-django-compressor/docs/index.txt
python-django-compressor/docs/jinja2.txt
python-django-compressor/docs/quickstart.txt
python-django-compressor/docs/reactjs.txt [new file with mode: 0644]
python-django-compressor/docs/remote-storages.txt
python-django-compressor/docs/settings.txt
python-django-compressor/docs/usage.txt
python-django-compressor/requirements/tests.txt
python-django-compressor/setup.py
python-django-compressor/tox.ini

index 32e02f630eb38eb988fdf253c77bd514d87af6d0..a7072740e3fcb8b6142e32b171faed09e12532b9 100644 (file)
@@ -1,3 +1,65 @@
+python-django-compressor (2.0-1~u14.04+mos1) mos8.0; urgency=medium
+
+  * Source:
+      http://archive.ubuntu.com/ubuntu/pool/main/p/python-django-compressor/
+      python-django-compressor_2.0-1ubuntu1.dsc
+
+ -- Ivan Udovichenko <iudovichenko@mirantis.com>  Fri, 13 Jan 2017 16:46:17 +0200
+
+python-django-compressor (2.0-1ubuntu1) xenial; urgency=low
+
+  * Merge from Debian unstable (LP: #1554134). Remaining changes:
+    - d/p/disable-coffin-tests.patch: Dropped, no longer required as
+      upstream dropped support for coffin.
+    - d/control: Drop BD's on django-discover-runner and csstidy; not
+      required in Ubuntu.
+    - d/control: Drop BD's on coffin and jingo, no longer supported
+      upstream.
+    - d/control: Drop (Build-)Depends on django-overextends and
+      django-sekizai, as these are optional dependencies.
+    - d/p/disable-django-sekizai.patch: Skip django-sekizai if its not
+      installed.
+
+ -- James Page <james.page@ubuntu.com>  Fri, 11 Mar 2016 10:14:29 +0000
+
+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-3ubuntu1) xenial; urgency=low
+
+  * Merge from Debian unstable.  Remaining changes:
+    - d/control: Drop BD on python-coffin.
+    - d/p/disable-coffin-tests.patch: Disable coffin tests for Ubuntu.
+    - d/control: Drop BD's on django-discover-runner and csstidy; not
+      required in Ubuntu.
+
+ -- James Page <james.page@ubuntu.com>  Mon, 23 Nov 2015 09:59:19 +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
index d9711499bcf3063ed3ea132eaa49846bfbe7d5a3..388146684d616910fe8b8f75044b1fd87f5f3e22 100644 (file)
@@ -1,7 +1,8 @@
 Source: python-django-compressor
 Section: python
 Priority: optional
-Maintainer: PKG OpenStack <openstack-devel@lists.alioth.debian.org>
+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
+XSBC-Original-Maintainer: PKG OpenStack <openstack-devel@lists.alioth.debian.org>
 Uploaders: Thomas Goirand <zigo@debian.org>,
 Build-Depends: debhelper (>= 9),
                dh-python,
@@ -9,46 +10,51 @@ Build-Depends: debhelper (>= 9),
                python-all,
                python-setuptools,
                python3-all,
-               python3-setuptools
-Build-Depends-Indep: csstidy,
-                     python-appconf,
+               python3-setuptools,
+Build-Depends-Indep: python-appconf,
                      python-bs4,
-                     python-coffin,
                      python-coverage,
+                     python-csscompressor,
                      python-django,
-                     python-django-discover-runner,
                      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-coverage,
+                     python3-csscompressor,
                      python3-django,
-                     python3-django-discover-runner,
                      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,
+Depends: python-csscompressor,
          python-django,
+         python-django-appconf,
+         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
@@ -56,16 +62,51 @@ Description: Compresses linked, inline JS or CSS into single cached files - Pyth
  .
  This package contains the Python 2.7 module.
 
-Package: python3-compressor
+Package: python3-django-compressor
 Architecture: all
-Depends: python3-appconf,
+Depends: python3-csscompressor,
          python3-django,
+         python3-django-appconf,
+         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,
+         ${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,
+         ${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.
index c573e422556f29c6028c56e31600c0114044efa8..d84bef8ebf992b7668e6a03ab6a8fd86dbe51c7c 100644 (file)
@@ -6,10 +6,6 @@ Files: debian/*
 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
@@ -18,10 +14,6 @@ Files: compressor/utils/decorators.py
 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>
@@ -49,28 +41,6 @@ License: MIT
  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
@@ -90,19 +60,3 @@ License: BSD-2-clauses
  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
diff --git a/debian/patches/disable-django-sekizai.patch b/debian/patches/disable-django-sekizai.patch
new file mode 100644 (file)
index 0000000..b716c34
--- /dev/null
@@ -0,0 +1,40 @@
+--- a/compressor/test_settings.py
++++ b/compressor/test_settings.py
+@@ -20,7 +20,6 @@ DATABASES = {
+ INSTALLED_APPS = [
+     'django.contrib.staticfiles',
+     'compressor',
+-    'sekizai',
+ ]
+ STATICFILES_FINDERS = [
+--- a/compressor/tests/test_templatetags.py
++++ b/compressor/tests/test_templatetags.py
+@@ -4,6 +4,7 @@ import os
+ import sys
+ from mock import Mock
++from unittest import skipIf
+ from django.template import Template, Context, TemplateSyntaxError
+ from django.test import TestCase
+@@ -13,7 +14,10 @@ from compressor.conf import settings
+ from compressor.signals import post_compress
+ from compressor.tests.test_base import css_tag, test_dir
+-from sekizai.context import SekizaiContext
++try:
++    from sekizai.context import SekizaiContext
++except ImportError:
++    SekizaiContext = None
+ def render(template_string, context_dict=None, context=None):
+@@ -130,6 +134,7 @@ class TemplatetagTestCase(TestCase):
+         context = kwargs['context']
+         self.assertEqual('foo', context['compressed']['name'])
++    @skipIf(SekizaiContext is None, "django-sekizai not installed, skipping test")
+     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>
diff --git a/debian/patches/remove-failed-test.patch b/debian/patches/remove-failed-test.patch
deleted file mode 100644 (file)
index 6f835f3..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-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):
index 59436156e0f7c68d48638fa47070710929cac9c8..6c186faf380ce028ed8d71bcb886e0089cf2772c 100644 (file)
@@ -1 +1 @@
-remove-failed-test.patch
+disable-django-sekizai.patch
index 3a0edc633ad714834fa0e4516dbec1522a2ec1e9..f8f4f1a08df1f66de2d8f895fafdd95fe660266f 100644 (file)
@@ -1 +1,2 @@
 django_appconf python-appconf
+coffin
index 46c1c969e5224043911977438099920ecd81c542..38b16aa6567918479d5e6bb54c4b0fa0902363ff 100755 (executable)
@@ -3,7 +3,7 @@
 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
 
@@ -28,11 +28,11 @@ override_dh_auto_build:
 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/*
index b350b181b6b33a2e281d28f7461442fce38752ba..5f1a39a889ba6731b33127d5727c43ae588a6f11 100644 (file)
@@ -1,27 +1,27 @@
 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
index 7b9a5719dc8e59acd52afc40ae56f907ea27a0b7..39e21d6435e585d6e7b6b7e9250c0ea2aaee33ff 100644 (file)
@@ -70,6 +70,7 @@ Matthew Tretter
 Mehmet S. Catalbas
 Michael van de Waeter
 Mike Yumatov
+Nick Pope
 Nicolas Charlot
 Niran Babalola
 Paul McMillan
index d9432d525f6838dab0f5e7ec882b22272d36e58b..43f09331c31aa786f2d5eb7bbaa54e5860a9f3e3 100644 (file)
@@ -1,6 +1,6 @@
 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
index 0c4c65f6f04cc825e4c3652f0d954a7b23cd06c5..6d4b2bfafd534326be2fcd4d6b49150b51951d07 100644 (file)
@@ -3,9 +3,15 @@ testenv:
        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
index 86ed3d7ae6ff1983fb505a93c4ed158504edf92d..4ebfcf846eb4ad0e0a0579308611ceb7a353b964 100644 (file)
@@ -1,8 +1,8 @@
 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
@@ -50,10 +50,10 @@ default. As an alternative Django Compressor provides a BeautifulSoup_ and a
 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
@@ -71,14 +71,12 @@ The in-development version of Django Compressor can be installed with
 
 .. _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
-
index 5e617817b468f25579a01aaa2527cb2e3c1818aa..5a1d40f586529543fc201fc669f633fe7dde8218 100644 (file)
@@ -1,2 +1,2 @@
 # following PEP 386
-__version__ = "1.5"
+__version__ = "2.0"
index 54b16f39c530a13c736e18fa388467d606ee0f78..79ab0ac2aa667c99418faf576eff96fbee2c0f7b 100644 (file)
@@ -1,26 +1,19 @@
 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
@@ -36,17 +29,19 @@ class Compressor(object):
     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
 
@@ -117,18 +112,21 @@ class Compressor(object):
         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:
@@ -202,24 +200,28 @@ class Compressor(object):
                 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):
         """
@@ -242,37 +244,36 @@ class Compressor(object):
             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:
@@ -338,8 +339,14 @@ class Compressor(object):
 
         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)
index f80bf54547827cd8023bcc9a2a990822f9c1d0c1..6cc7344f671a3e6cbce3d4e56042d24e6823b877 100644 (file)
@@ -3,23 +3,13 @@ import hashlib
 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
@@ -135,6 +125,10 @@ def get_hashed_content(filename, length=12):
         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:
@@ -158,4 +152,4 @@ def cache_set(key, val, refreshed=False, timeout=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])
index 87f9d697006b44cbb8ecc8efcbd2ff7efb564a47..c9dd95364d7026dfdbf6af67babc86f3ecaaa8e5 100644 (file)
@@ -2,6 +2,7 @@ from __future__ import unicode_literals
 import os
 from django.conf import settings
 from django.core.exceptions import ImproperlyConfigured
+from django.template.utils import InvalidTemplateEngineError
 
 from appconf import AppConf
 
@@ -35,10 +36,9 @@ class CompressorConf(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 = ''
@@ -70,11 +70,22 @@ class CompressorConf(AppConf):
     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
 
index 87966c5707c13335847f133632b4f584ed8362af..38e4c01ded22e899e00b7d44817cca914946fea7 100644 (file)
@@ -6,7 +6,7 @@
  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):
@@ -15,4 +15,4 @@ 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)
index 45cdcd2b8a565ef6035b65697b1b11d7a113d3ca..bd3e09138d7b9221f1df7ea5e23dcd0f03f41e3d 100644 (file)
@@ -5,10 +5,8 @@ from compressor.conf import settings
 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:
index cd317fa57d32b823f438282297b4085e21005e81..efeee86891877c066b0e8f9fe51745031e2df92f 100644 (file)
@@ -1,3 +1,3 @@
 # flake8: noqa
 from compressor.filters.base import (FilterBase, CallbackOutputFilter,
-                                     CompilerFilter, FilterError)
+                                     CompilerFilter, CachedCompilerFilter, FilterError)
index ee14b823b7d492bbbc28730343096916c52677f3..dcdb96fa61a115d7e58ed125e948d767819f42f0 100644 (file)
@@ -3,6 +3,7 @@ import io
 import logging
 import subprocess
 
+from importlib import import_module
 from platform import system
 
 if system() != "Windows":
@@ -19,14 +20,11 @@ else:
 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
@@ -42,8 +40,8 @@ class FilterBase(object):
     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
@@ -116,8 +114,8 @@ class CompilerFilter(FilterBase):
     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:
@@ -140,6 +138,7 @@ class CompilerFilter(FilterBase):
         self.infile = self.outfile = None
 
     def input(self, **kwargs):
+
         encoding = self.default_encoding
         options = dict(self.options)
 
@@ -208,5 +207,26 @@ class CompilerFilter(FilterBase):
                 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)
index 073303d95a9832b446adaab5afeecb0aeb16a8dd..1f66b2384714317f504a6ea7d29fa1e18ae854a8 100644 (file)
@@ -1,13 +1,21 @@
 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
diff --git a/python-django-compressor/compressor/filters/cssmin/cssmin.py b/python-django-compressor/compressor/filters/cssmin/cssmin.py
deleted file mode 100644 (file)
index e8a02b0..0000000
+++ /dev/null
@@ -1,245 +0,0 @@
-#!/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()
diff --git a/python-django-compressor/compressor/filters/cssmin/rcssmin.py b/python-django-compressor/compressor/filters/cssmin/rcssmin.py
deleted file mode 100644 (file)
index ff8e273..0000000
+++ /dev/null
@@ -1,360 +0,0 @@
-#!/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()
diff --git a/python-django-compressor/compressor/filters/csstidy.py b/python-django-compressor/compressor/filters/csstidy.py
deleted file mode 100644 (file)
index 4b7e4c7..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-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),
-    )
index 48d8007f6acf8008d1ff480ee813e1d13ce08772..f32e6a420dcc632112a986752dd650162bb20a57 100644 (file)
@@ -1,10 +1,21 @@
 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,
+    }
diff --git a/python-django-compressor/compressor/filters/jsmin/rjsmin.py b/python-django-compressor/compressor/filters/jsmin/rjsmin.py
deleted file mode 100755 (executable)
index 6eedf2f..0000000
+++ /dev/null
@@ -1,300 +0,0 @@
-#!/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()))
diff --git a/python-django-compressor/compressor/filters/jsmin/slimit.py b/python-django-compressor/compressor/filters/jsmin/slimit.py
deleted file mode 100644 (file)
index 9ffc7f4..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-from __future__ import absolute_import
-from compressor.filters import CallbackOutputFilter
-
-
-class SlimItFilter(CallbackOutputFilter):
-    dependencies = ["slimit"]
-    callback = "slimit.minify"
-    kwargs = {
-        "mangle": True,
-    }
index f60cc7e3c219f17fe28e215e92afa4145e81f9e0..651300f4b31ddf4b469cafdf7eacacd063d6c755 100644 (file)
@@ -5,9 +5,8 @@ from compressor.base import Compressor, SOURCE_HUNK, SOURCE_FILE
 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:
index 8cf19266a32bf665b9d1b3b78e6d02b3642d1d27..a6fb1a1e6c3fef3b85288f865c30ee3c0f6bbedb 100644 (file)
@@ -2,23 +2,26 @@
 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
@@ -31,9 +34,9 @@ else:
         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 '
@@ -51,33 +54,12 @@ class Command(NoArgsCommand):
             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
@@ -119,6 +101,7 @@ class Command(NoArgsCommand):
         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))
@@ -128,34 +111,44 @@ class Command(NoArgsCommand):
             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 "
@@ -163,10 +156,8 @@ class Command(NoArgsCommand):
         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)
@@ -208,44 +199,58 @@ class Command(NoArgsCommand):
                       "\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',)):
         """
@@ -253,7 +258,7 @@ class Command(NoArgsCommand):
         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']
@@ -268,7 +273,7 @@ class Command(NoArgsCommand):
                 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 "
@@ -279,3 +284,7 @@ class Command(NoArgsCommand):
                     "Offline compression is disabled. Set "
                     "COMPRESS_OFFLINE or use the --force to override.")
         self.compress(sys.stdout, **options)
+
+
+
+Command.requires_system_checks = False
index e96f004f180c2e9a3ee9bcfc9f4f199aa73d603a..afd23906bfe3fdd8f1a9c971cfbc2a4d0ccbdb7b 100644 (file)
@@ -1,34 +1,38 @@
 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):
         """
@@ -40,24 +44,27 @@ class Command(NoArgsCommand):
                 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_)
@@ -74,9 +81,11 @@ class Command(NoArgsCommand):
 
         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))
index 107c6e4d7f72c73d6a73eb8318104919e15940ed..2c088858707742308deacf757fb0b66682a4369d 100644 (file)
@@ -1,9 +1,7 @@
 from __future__ import absolute_import
 from copy import copy
 
-import django
 from django import template
-from django.conf import settings
 from django.template import Context
 from django.template.base import Node, VariableNode, TextNode, NodeList
 from django.template.defaulttags import IfNode
@@ -26,7 +24,10 @@ def handle_extendsnode(extendsnode, block_context=None, original=None):
                   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
 
@@ -99,10 +100,7 @@ class DjangoParser(object):
 
     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:
@@ -118,8 +116,7 @@ class DjangoParser(object):
         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):
@@ -144,7 +141,7 @@ class DjangoParser(object):
         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):
index 19beb01aa8d3bac080a8dae5a8a62d2114ad0641..a53f2a438efd797370854f10010d1aa3a23952e1 100644 (file)
@@ -1,9 +1,7 @@
+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
index d143df4d6dfaee8219cc7a3e709382936a9802c2..1068abe2264ed442b534a1f36a71aa299b298668 100644 (file)
@@ -1,42 +1,34 @@
 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
index 80272cbfe2a6da0344ad6f8bc0e82b3e9b4507b3..825808be88138028d62c9bfe439608fb4e249013 100644 (file)
@@ -1,3 +1,5 @@
+import sys
+
 from django.utils import six
 from django.utils.encoding import smart_text
 
@@ -5,9 +7,26 @@ from compressor.exceptions import ParserError
 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 = []
index 16419a881fa1402a2fcf9bc53d118afb31dd5665..6fe994ea9b3d4ab41cfde7dda54a376d4e488483 100644 (file)
@@ -36,7 +36,7 @@ class CompressorFileStorage(FileSystemStorage):
     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.
         """
index a45f4546dccab1e591fd27cb7883a75ae5a434d2..f92d2c744d73e5babff2a23395802a35e3598cab 100644 (file)
@@ -40,8 +40,7 @@ class CompressorMixin(object):
 
     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
@@ -57,68 +56,55 @@ class CompressorMixin(object):
         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):
@@ -132,14 +118,6 @@ 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
index 7fb021cb06d9bc3ae21815c87f24e474232fce89..facf66a9ea441a4bee8e680a04a29ec4b73f30d2 100644 (file)
@@ -1,5 +1,4 @@
 import os
-import django
 
 TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'tests')
 
@@ -21,10 +20,8 @@ DATABASES = {
 INSTALLED_APPS = [
     'django.contrib.staticfiles',
     'compressor',
-    'coffin',
+    'sekizai',
 ]
-if django.VERSION < (1, 8):
-    INSTALLED_APPS.append('jingo')
 
 STATICFILES_FINDERS = [
     'django.contrib.staticfiles.finders.FileSystemFinder',
@@ -37,15 +34,25 @@ STATIC_URL = '/static/'
 
 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=!"
 
diff --git a/python-django-compressor/compressor/tests/static/css/relative_url.css b/python-django-compressor/compressor/tests/static/css/relative_url.css
new file mode 100644 (file)
index 0000000..c690cde
--- /dev/null
@@ -0,0 +1 @@
+p { background: url('../img/python.png'); }
\ No newline at end of file
index e8255db12e463ee1f25d4977921edcf8713a04d5..dc6d2e94bf5e39aaa1f4d51327a0f17a9dfc2f9f 100644 (file)
@@ -1,32 +1,27 @@
 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):
@@ -45,15 +40,63 @@ class TestPrecompiler(object):
         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>
@@ -128,8 +171,8 @@ class CompressorTestCase(SimpleTestCase):
             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):
@@ -226,6 +269,15 @@ class CompressorTestCase(SimpleTestCase):
         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):
@@ -237,10 +289,7 @@ class CssMediaTestCase(SimpleTestCase):
 
     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])
@@ -249,10 +298,7 @@ class CssMediaTestCase(SimpleTestCase):
         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=(
@@ -264,21 +310,16 @@ class CssMediaTestCase(SimpleTestCase):
 <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):
@@ -307,12 +348,8 @@ class JsAsyncDeferTestCase(SimpleTestCase):
                 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)
 
 
@@ -331,3 +368,57 @@ class CacheTestCase(SimpleTestCase):
     @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)
index 784d89a53c71d448fb10f582761c12d6eb9837a0..fb75146b31ebcf57225a79f312f22ead981be083 100644 (file)
@@ -3,23 +3,20 @@ from collections import defaultdict
 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
@@ -30,26 +27,14 @@ def blankdict(*args, **kwargs):
     return defaultdict(lambda: '', *args, **kwargs)
 
 
-@unittest.skipIf(find_command(settings.COMPRESS_CSSTIDY_BINARY) is None,
-                 'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY)
-class CssTidyTestCase(TestCase):
-    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)
@@ -101,10 +86,51 @@ class PrecompilerTestCase(TestCase):
         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');
@@ -112,10 +138,52 @@ class CssMinTestCase(TestCase):
 
         }
         """
-        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)
@@ -123,12 +191,9 @@ class CssAbsolutizingTestCase(TestCase):
                 "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">
@@ -136,12 +201,10 @@ class CssAbsolutizingTestCase(TestCase):
         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({
@@ -151,10 +214,11 @@ class CssAbsolutizingTestCase(TestCase):
         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')
@@ -168,10 +232,11 @@ class CssAbsolutizingTestCase(TestCase):
         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')
@@ -186,10 +251,11 @@ class CssAbsolutizingTestCase(TestCase):
         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')
@@ -197,9 +263,9 @@ class CssAbsolutizingTestCase(TestCase):
         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')
@@ -214,10 +280,11 @@ class CssAbsolutizingTestCase(TestCase):
         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')
@@ -231,10 +298,11 @@ class CssAbsolutizingTestCase(TestCase):
         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')
@@ -248,10 +316,11 @@ class CssAbsolutizingTestCase(TestCase):
         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'
@@ -262,11 +331,12 @@ class CssAbsolutizingTestCase(TestCase):
         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 = {
@@ -290,12 +360,12 @@ p { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/static/img/
 
     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):
@@ -303,15 +373,17 @@ 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">
         """
@@ -352,10 +424,6 @@ class SpecializedFiltersTest(TestCase):
         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')
diff --git a/python-django-compressor/compressor/tests/test_finder.py b/python-django-compressor/compressor/tests/test_finder.py
new file mode 100644 (file)
index 0000000..0420cd4
--- /dev/null
@@ -0,0 +1,15 @@
+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([]), [])
index 04adb9a3b674a912651e5bb2b266064a7638e24c..fc334da955bcd57aab557738e074a1a1dff9d290 100644 (file)
@@ -2,16 +2,17 @@
 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):
     """
@@ -143,13 +144,11 @@ 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))
diff --git a/python-django-compressor/compressor/tests/test_mtime_cache.py b/python-django-compressor/compressor/tests/test_mtime_cache.py
new file mode 100644 (file)
index 0000000..fd0006c
--- /dev/null
@@ -0,0 +1,37 @@
+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)
index 59a3d62085b297d2b9c9fdc6e291783270353173..6433911b005fa771b6d2a74275cf5712affb9286 100644 (file)
@@ -1,19 +1,25 @@
 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
@@ -28,58 +34,81 @@ else:
 # 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)
@@ -88,26 +117,44 @@ class OfflineTestCaseMixin(object):
         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
@@ -131,151 +178,338 @@ class OfflineTestCaseMixin(object):
     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):
         """
@@ -283,219 +517,115 @@ class OfflineGenerationTestCaseWithError(OfflineTestCaseMixin, TestCase):
         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')
index d9b4dd6873500723298d09e237784300e22b4f9b..6b3750396701b62f9dd59e53e47373c898d4096d 100644 (file)
@@ -1,5 +1,6 @@
 from __future__ import with_statement
 import os
+import unittest
 
 try:
     import lxml
@@ -11,12 +12,6 @@ try:
 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
@@ -26,12 +21,12 @@ from compressor.tests.test_base import CompressorTestCase
 
 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')
@@ -101,8 +96,8 @@ class Html5LibParserTests(ParserTestCase, CompressorTestCase):
                                                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
@@ -116,9 +111,46 @@ class Html5LibParserTests(ParserTestCase, CompressorTestCase):
         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):
index 13d5eedb1f05d808230e73c441c5190b94bb94e1..e3645b866d1e979d91459bea41743e1d7e3b4016 100644 (file)
@@ -1,18 +1,20 @@
 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>
index 91a36f2937fc0e6340001e02d0d7c5cbd3741d6b..9bbc026205d57d3bac45638baf16e2a89f2c519e 100644 (file)
@@ -5,6 +5,7 @@ import os
 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
@@ -18,16 +19,14 @@ class GzipStorage(LazyObject):
         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'))
diff --git a/python-django-compressor/compressor/tests/test_templates/test_with_context_super/base.html b/python-django-compressor/compressor/tests/test_templates/test_with_context_super/base.html
new file mode 100644 (file)
index 0000000..4de1806
--- /dev/null
@@ -0,0 +1,7 @@
+{% spaceless %}
+{% block js %}
+    <script type="text/javascript">
+        alert("{{ content|default:"Ooops!" }}");
+    </script>
+{% endblock %}
+{% endspaceless %}
diff --git a/python-django-compressor/compressor/tests/test_templates/test_with_context_super/test_compressor_offline.html b/python-django-compressor/compressor/tests/test_templates/test_with_context_super/test_compressor_offline.html
new file mode 100644 (file)
index 0000000..9a144e7
--- /dev/null
@@ -0,0 +1,13 @@
+{% 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 %}
index db0d1b7e7d2fba75ffc5e0e9462e26b48190469a..f41194a19bfd5e2fc418ba073118215471a6da2b 100644 (file)
@@ -13,27 +13,27 @@ from compressor.conf import settings
 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 %}"""
@@ -130,25 +130,34 @@ class TemplatetagTestCase(TestCase):
         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 %}
diff --git a/python-django-compressor/compressor/tests/test_utils.py b/python-django-compressor/compressor/tests/test_utils.py
new file mode 100644 (file)
index 0000000..a275f0e
--- /dev/null
@@ -0,0 +1,46 @@
+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)
index 2d9ed0024f395a3b64111d85452ce24d08673f56..09a8a9b6946e1fa2648ab909ec444a029151b9b4 100644 (file)
@@ -1,10 +1,12 @@
 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"
diff --git a/python-django-compressor/compressor/utils/stringformat.py b/python-django-compressor/compressor/utils/stringformat.py
deleted file mode 100644 (file)
index 9311e78..0000000
+++ /dev/null
@@ -1,260 +0,0 @@
-# -*- 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()
index 07864df093753b7a428e7069b14f6479dc0604f3..bf40c16abc8f29def2038dd8b94ddb2eeaac8e95 100644 (file)
@@ -1,10 +1,58 @@
 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
 
@@ -57,7 +105,7 @@ v1.4 (06/20/2014)
 
 - 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.
 
@@ -368,7 +416,7 @@ v0.6.1
 ------
 
 - 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
@@ -394,7 +442,7 @@ Major improvements and a lot of bugfixes, some of which are:
   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`
index 8f0cd50427921db0121f0c9b8e8068f78d6330b0..fa33dad25f89da0f45eb8f5187c5735c4705e0a6 100644 (file)
@@ -11,9 +11,9 @@ Community
 
 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
index f1adfa4fa537cef1d6cdf97d40715df95ed3c4ff..7cb58f24992a6fc46b51258c76b25e4e8de72744 100644 (file)
@@ -43,5 +43,6 @@ Contents
  behind-the-scenes
  jinja2
  django-sekizai
+ reactjs
  contributing
  changelog
index f4922792ab117e7b562c7228fa56ee48d1a6b233..c352da17a6f385e61453de005cabf00c8dfd5489 100644 (file)
@@ -24,19 +24,6 @@ From now on, you can use same code you'd normally use within Django templates::
     ]))
     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
 ==================================
@@ -85,13 +72,7 @@ method, and is in the ``TEMPLATE_LOADERS`` setting.
 
 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
@@ -104,51 +85,6 @@ the filesystem loader (``django.template.loaders.filesystem.Loader``) in the
 ``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::
@@ -161,14 +97,6 @@ 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/
index 00931ea4198d5aaa109baaa5b6fcccf8f6d84b44..9dc58ee9b297e0f35b8934efcd91bc9169f9d00c 100644 (file)
@@ -31,7 +31,7 @@ Installation
      )
 
 * 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/
@@ -39,34 +39,16 @@ Installation
 
 .. _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_
 
@@ -89,10 +71,17 @@ Optional
 
       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/
diff --git a/python-django-compressor/docs/reactjs.txt b/python-django-compressor/docs/reactjs.txt
new file mode 100644 (file)
index 0000000..c1dfc8b
--- /dev/null
@@ -0,0 +1,76 @@
+.. _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')
+
index 8af6934855b5d98978cc05d6488e4741d6b54da4..30d6fb3ea1b0ed1112cc2f8b898c4c789b4689f1 100644 (file)
@@ -17,7 +17,7 @@ django-storages
 ^^^^^^^^^^^^^^^
 
 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'
@@ -51,7 +51,7 @@ apps can be integrated.
 
 #. 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
@@ -82,7 +82,7 @@ apps can be integrated.
 .. _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
index 268f6455d0dac87b449d279e1958661125fecf98..b175bff8c79de17f347993f55e16f769bd9f8000 100644 (file)
@@ -87,18 +87,6 @@ Backend settings
          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.
@@ -132,27 +120,34 @@ Backend settings
 
          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/
 
 
@@ -178,7 +173,7 @@ Backend settings
     - ``compressor.filters.jsmin.JSMinFilter``
 
       A filter that uses the jsmin implementation rJSmin_ to compress
-      JavaScript code.
+      JavaScript code (installed by default).
 
     .. _slimit_filter:
 
@@ -274,8 +269,8 @@ Backend settings
     .. 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}'),
@@ -412,6 +407,19 @@ Caching settings
     :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
@@ -434,11 +442,11 @@ Caching settings
         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.
@@ -473,6 +481,37 @@ Offline settings
 
     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``
index 5bf665e4b4b9db1a8cfe7bba9dcf13be96e3b599..aea8fcdd63646b03308c9a910840247d10a7befe 100644 (file)
@@ -105,7 +105,7 @@ of that particular compress tag.  This is then added to the context so you can
 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:
 
@@ -153,7 +153,7 @@ the commonly used setting to refer to saved files ``STATIC_URL``.
 
 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
index 775874f2dd2226014df8e9891e956310306a7389..6cd420e3556c309a1214792f5bb679bcaa4aaaf9 100644 (file)
@@ -1,10 +1,11 @@
-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
index 81f5dde0de394e50c3a2692b3b773d5783caf303..f3e116f85b348f1f73d2b58b0f742936a337f3c2 100644 (file)
@@ -131,15 +131,18 @@ setup(
         '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',
     ],
 )
index 78d6039af98ee5793a1a79e35d3d88d67a83ffac..2ca649b7fd9c62f758b89baba05b7a37d0a2c371 100644 (file)
@@ -1,49 +1,51 @@
 [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
@@ -53,14 +55,11 @@ commands =
     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