summaryrefslogtreecommitdiff
path: root/compressor
diff options
context:
space:
mode:
Diffstat (limited to 'compressor')
-rw-r--r--compressor/__init__.py2
-rw-r--r--compressor/base.py94
-rw-r--r--compressor/cache.py29
-rw-r--r--compressor/conf.py16
-rw-r--r--compressor/contrib/jinja2ext.py18
-rw-r--r--compressor/contrib/sekizai.py2
-rw-r--r--compressor/css.py2
-rw-r--r--compressor/exceptions.py14
-rw-r--r--compressor/filters/base.py119
-rw-r--r--compressor/filters/cssmin/__init__.py4
-rw-r--r--compressor/filters/cssmin/cssmin.py6
-rw-r--r--compressor/filters/cssmin/rcssmin.py360
-rw-r--r--compressor/filters/datauri.py6
-rwxr-xr-xcompressor/filters/jsmin/rjsmin.py68
-rw-r--r--compressor/filters/yuglify.py26
-rw-r--r--compressor/management/commands/compress.py194
-rw-r--r--compressor/management/commands/mtime_cache.py4
-rw-r--r--compressor/offline/__init__.py0
-rw-r--r--compressor/offline/django.py143
-rw-r--r--compressor/offline/jinja2.py125
-rw-r--r--compressor/parser/__init__.py6
-rw-r--r--compressor/parser/beautifulsoup.py24
-rw-r--r--compressor/parser/default_htmlparser.py14
-rw-r--r--compressor/parser/html5lib.py37
-rw-r--r--compressor/parser/lxml.py54
-rw-r--r--compressor/storage.py34
-rw-r--r--compressor/templatetags/compress.py6
-rw-r--r--compressor/test_settings.py22
-rw-r--r--compressor/tests/precompiler.py2
-rw-r--r--compressor/tests/test_base.py213
-rw-r--r--compressor/tests/test_filters.py86
-rw-r--r--compressor/tests/test_jinja2ext.py74
-rw-r--r--compressor/tests/test_offline.py325
-rw-r--r--compressor/tests/test_parsers.py107
-rw-r--r--compressor/tests/test_signals.py16
-rw-r--r--compressor/tests/test_storages.py34
-rw-r--r--compressor/tests/test_templates/test_block_super_base_compressed/base.html10
-rw-r--r--compressor/tests/test_templates/test_block_super_base_compressed/base2.html8
-rw-r--r--compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html8
-rw-r--r--compressor/tests/test_templates/test_block_super_multiple/base2.html7
-rw-r--r--compressor/tests/test_templates/test_complex/test_compressor_offline.html20
-rw-r--r--compressor/tests/test_templates/test_duplicate/test_compressor_offline.html13
-rw-r--r--compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html8
-rw-r--r--compressor/tests/test_templates_jinja2/test_block_super/base.html15
-rw-r--r--compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html12
-rw-r--r--compressor/tests/test_templates_jinja2/test_block_super_extra/base.html15
-rw-r--r--compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html18
-rw-r--r--compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html15
-rw-r--r--compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html3
-rw-r--r--compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html10
-rw-r--r--compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html15
-rw-r--r--compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html3
-rw-r--r--compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html10
-rw-r--r--compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html11
-rw-r--r--compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html24
-rw-r--r--compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html7
-rw-r--r--compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html9
-rw-r--r--compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html10
-rw-r--r--compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html9
-rw-r--r--compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html8
-rw-r--r--compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html5
-rw-r--r--compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html7
-rw-r--r--compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html11
-rw-r--r--compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html6
-rw-r--r--compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html7
-rw-r--r--compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html7
-rw-r--r--compressor/tests/test_templatetags.py182
-rw-r--r--compressor/utils/__init__.py18
-rw-r--r--compressor/utils/staticfiles.py2
-rw-r--r--compressor/utils/stringformat.py22
70 files changed, 2125 insertions, 696 deletions
diff --git a/compressor/__init__.py b/compressor/__init__.py
index 6524f6c..fdbc16e 100644
--- a/compressor/__init__.py
+++ b/compressor/__init__.py
@@ -1,2 +1,2 @@
# following PEP 386
-__version__ = "1.3"
+__version__ = "1.4a1"
diff --git a/compressor/base.py b/compressor/base.py
index 4e91d4d..de9c9ce 100644
--- a/compressor/base.py
+++ b/compressor/base.py
@@ -1,22 +1,24 @@
-from __future__ import with_statement
+from __future__ import with_statement, unicode_literals
import os
import codecs
-import urllib
from django.core.files.base import ContentFile
from django.template import Context
from django.template.loader import render_to_string
-from django.utils.encoding import smart_unicode
from django.utils.importlib import import_module
from django.utils.safestring import mark_safe
-from compressor.cache import get_hexdigest, get_mtime
+try:
+ from urllib.request import url2pathname
+except ImportError:
+ from urllib import url2pathname
+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.storage import default_storage, compressor_file_storage
+from compressor.storage import compressor_file_storage
from compressor.signals import post_compress
from compressor.utils import get_class, get_mod_func, staticfiles
from compressor.utils.decorators import cached_property
@@ -34,16 +36,21 @@ class Compressor(object):
type = None
def __init__(self, content=None, output_prefix=None, context=None, *args, **kwargs):
- self.content = content or ""
+ 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.storage = default_storage
self.split_content = []
self.context = context or {}
self.extra_context = {}
self.all_mimetypes = dict(settings.COMPRESS_PRECOMPILERS)
self.finders = staticfiles.finders
+ self._storage = None
+
+ @cached_property
+ def storage(self):
+ from compressor.storage import default_storage
+ return default_storage
def split_contents(self):
"""
@@ -65,6 +72,10 @@ class Compressor(object):
return "compressor/%s_%s.html" % (self.type, mode)
def get_basename(self, url):
+ """
+ Takes full path to a static file (eg. "/static/css/style.css") and
+ returns path with storage's base url removed (eg. "css/style.css").
+ """
try:
base_url = self.storage.base_url
except AttributeError:
@@ -78,6 +89,17 @@ class Compressor(object):
return basename.split("?", 1)[0]
def get_filepath(self, content, basename=None):
+ """
+ Returns file path for an output file based on contents.
+
+ Returned path is relative to compressor storage's base url, for
+ example "CACHE/css/e41ba2cc6982.css".
+
+ When `basename` argument is provided then file name (without extension)
+ will be used as a part of returned file name, for example:
+
+ get_filepath(content, "my_file.css") -> 'CACHE/css/my_file.e41ba2cc6982.css'
+ """
parts = []
if basename:
filename = os.path.split(basename)[1]
@@ -86,6 +108,11 @@ class Compressor(object):
return os.path.join(self.output_dir, self.output_prefix, '.'.join(parts))
def get_filename(self, basename):
+ """
+ Returns full path to a file, for example:
+
+ get_filename('css/one.css') -> '/full/path/to/static/css/one.css'
+ """
filename = None
# first try finding the file in the root
try:
@@ -100,7 +127,7 @@ class Compressor(object):
filename = compressor_file_storage.path(basename)
# secondly try to find it with staticfiles (in debug mode)
if not filename and self.finders:
- filename = self.finders.find(urllib.url2pathname(basename))
+ filename = self.finders.find(url2pathname(basename))
if filename:
return filename
# or just raise an exception as the last resort
@@ -110,13 +137,16 @@ class Compressor(object):
self.finders and " or with staticfiles." or "."))
def get_filecontent(self, filename, charset):
- with codecs.open(filename, 'rb', charset) as fd:
+ """
+ Reads file contents using given `charset` and returns it as text.
+ """
+ with codecs.open(filename, 'r', charset) as fd:
try:
return fd.read()
- except IOError, e:
+ except IOError as e:
raise UncompressableFileError("IOError while processing "
"'%s': %s" % (filename, e))
- except UnicodeDecodeError, e:
+ except UnicodeDecodeError as e:
raise UncompressableFileError("UnicodeDecodeError while "
"processing '%s' with "
"charset %s: %s" %
@@ -143,7 +173,7 @@ class Compressor(object):
def hunks(self, forced=False):
"""
- The heart of content parsing, iterates of the
+ The heart of content parsing, iterates over the
list of split contents and looks at its kind
to decide what to do with it. Should yield a
bunch of precompiled and/or rendered hunks.
@@ -159,6 +189,7 @@ class Compressor(object):
'elem': elem,
'kind': kind,
'basename': basename,
+ 'charset': charset,
}
if kind == SOURCE_FILE:
@@ -169,12 +200,11 @@ class Compressor(object):
precompiled, value = self.precompile(value, **options)
if enabled:
- value = self.filter(value, **options)
- yield smart_unicode(value, charset.lower())
+ yield self.filter(value, **options)
else:
if precompiled:
- value = self.handle_output(kind, value, forced=True, basename=basename)
- yield smart_unicode(value, charset.lower())
+ yield self.handle_output(kind, value, forced=True,
+ basename=basename)
else:
yield self.parser.elem_str(elem)
@@ -195,7 +225,13 @@ class Compressor(object):
content.append(hunk)
return content
- def precompile(self, content, kind=None, elem=None, filename=None, **kwargs):
+ def precompile(self, content, kind=None, elem=None, filename=None,
+ charset=None, **kwargs):
+ """
+ Processes file using a pre compiler.
+
+ This is the place where files like coffee script are processed.
+ """
if not kind:
return False, content
attrs = self.parser.elem_attribs(elem)
@@ -212,18 +248,21 @@ class Compressor(object):
try:
mod = import_module(mod_name)
except ImportError:
- return True, CompilerFilter(content, filter_type=self.type,
- command=filter_or_command, filename=filename).input(
- **kwargs)
+ 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:
- return True, precompiler_class(content, attrs,
- filter_type=self.type, filename=filename).input(
- **kwargs)
+ 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):
@@ -243,11 +282,10 @@ class Compressor(object):
any custom modification. Calls other mode specific methods or simply
returns the content directly.
"""
- content = self.filter_input(forced)
- if not content:
- return ''
+ output = '\n'.join(self.filter_input(forced))
- output = '\n'.join(c.encode(self.charset) for c in content)
+ if not output:
+ return ''
if settings.COMPRESS_ENABLED or forced:
filtered_output = self.filter_output(output)
@@ -271,7 +309,7 @@ class Compressor(object):
"""
new_filepath = self.get_filepath(content, basename=basename)
if not self.storage.exists(new_filepath) or forced:
- self.storage.save(new_filepath, ContentFile(content))
+ self.storage.save(new_filepath, ContentFile(content.encode(self.charset)))
url = mark_safe(self.storage.url(new_filepath))
return self.render_output(mode, {"url": url})
diff --git a/compressor/cache.py b/compressor/cache.py
index 1caeded..4847939 100644
--- a/compressor/cache.py
+++ b/compressor/cache.py
@@ -1,3 +1,4 @@
+import json
import hashlib
import os
import socket
@@ -5,8 +6,7 @@ import time
from django.core.cache import get_cache
from django.core.files.base import ContentFile
-from django.utils import simplejson
-from django.utils.encoding import smart_str
+from django.utils.encoding import force_text, smart_bytes
from django.utils.functional import SimpleLazyObject
from django.utils.importlib import import_module
@@ -18,18 +18,18 @@ _cachekey_func = None
def get_hexdigest(plaintext, length=None):
- digest = hashlib.md5(smart_str(plaintext)).hexdigest()
+ digest = hashlib.md5(smart_bytes(plaintext)).hexdigest()
if length:
return digest[:length]
return digest
def simple_cachekey(key):
- return 'django_compressor.%s' % smart_str(key)
+ return 'django_compressor.%s' % force_text(key)
def socket_cachekey(key):
- return "django_compressor.%s.%s" % (socket.gethostname(), smart_str(key))
+ return 'django_compressor.%s.%s' % (socket.gethostname(), force_text(key))
def get_cachekey(*args, **kwargs):
@@ -39,7 +39,7 @@ def get_cachekey(*args, **kwargs):
mod_name, func_name = get_mod_func(
settings.COMPRESS_CACHE_KEY_FUNCTION)
_cachekey_func = getattr(import_module(mod_name), func_name)
- except (AttributeError, ImportError), e:
+ except (AttributeError, ImportError) as e:
raise ImportError("Couldn't import cache key function %s: %s" %
(settings.COMPRESS_CACHE_KEY_FUNCTION, e))
return _cachekey_func(*args, **kwargs)
@@ -70,7 +70,8 @@ def get_offline_manifest():
if _offline_manifest is None:
filename = get_offline_manifest_filename()
if default_storage.exists(filename):
- _offline_manifest = simplejson.load(default_storage.open(filename))
+ with default_storage.open(filename) as fp:
+ _offline_manifest = json.loads(fp.read().decode('utf8'))
else:
_offline_manifest = {}
return _offline_manifest
@@ -83,8 +84,8 @@ def flush_offline_manifest():
def write_offline_manifest(manifest):
filename = get_offline_manifest_filename()
- default_storage.save(filename,
- ContentFile(simplejson.dumps(manifest, indent=2)))
+ content = json.dumps(manifest, indent=2).encode('utf8')
+ default_storage.save(filename, ContentFile(content))
flush_offline_manifest()
@@ -118,12 +119,10 @@ def get_hashed_content(filename, length=12):
filename = os.path.realpath(filename)
except OSError:
return None
- hash_file = open(filename)
- try:
- content = hash_file.read()
- finally:
- hash_file.close()
- return get_hexdigest(content, length)
+
+ # should we make sure that file is utf-8 encoded?
+ with open(filename, 'rb') as file:
+ return get_hexdigest(file.read(), length)
def cache_get(key):
diff --git a/compressor/conf.py b/compressor/conf.py
index 5ba7bee..e9763d9 100644
--- a/compressor/conf.py
+++ b/compressor/conf.py
@@ -1,3 +1,4 @@
+from __future__ import unicode_literals
import os
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
@@ -11,7 +12,7 @@ class CompressorConf(AppConf):
# Allows changing verbosity from the settings.
VERBOSE = False
# GET variable that disables compressor e.g. "nocompress"
- DEBUG_TOGGLE = 'None'
+ DEBUG_TOGGLE = None
# the backend to use when parsing the JavaScript or Stylesheet files
PARSER = 'compressor.parser.AutoSelectParser'
OUTPUT_DIR = 'CACHE'
@@ -41,6 +42,9 @@ class CompressorConf(AppConf):
YUI_BINARY = 'java -jar yuicompressor.jar'
YUI_CSS_ARGUMENTS = ''
YUI_JS_ARGUMENTS = ''
+ YUGLIFY_BINARY = 'yuglify'
+ YUGLIFY_CSS_ARGUMENTS = '--terminal'
+ YUGLIFY_JS_ARGUMENTS = '--terminal'
DATA_URI_MAX_SIZE = 1024
# the cache backend to use
@@ -64,6 +68,13 @@ 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.
+ def JINJA2_GET_ENVIRONMENT():
+ try:
+ import jinja2
+ return jinja2.Environment()
+ except ImportError:
+ return None
class Meta:
prefix = 'compress'
@@ -73,7 +84,8 @@ class CompressorConf(AppConf):
if value is None:
value = settings.STATIC_ROOT
if value is None:
- raise ImproperlyConfigured("COMPRESS_ROOT setting must be set")
+ raise ImproperlyConfigured('COMPRESS_ROOT defaults to ' +
+ 'STATIC_ROOT, please define either')
return os.path.normcase(os.path.abspath(value))
def configure_url(self, value):
diff --git a/compressor/contrib/jinja2ext.py b/compressor/contrib/jinja2ext.py
index baf76d5..7215d4d 100644
--- a/compressor/contrib/jinja2ext.py
+++ b/compressor/contrib/jinja2ext.py
@@ -10,7 +10,7 @@ class CompressorExtension(CompressorMixin, Extension):
tags = set(['compress'])
def parse(self, parser):
- lineno = parser.stream.next().lineno
+ lineno = next(parser.stream).lineno
kindarg = parser.parse_expression()
# Allow kind to be defined as jinja2 name node
if isinstance(kindarg, nodes.Name):
@@ -28,14 +28,22 @@ class CompressorExtension(CompressorMixin, Extension):
args.append(modearg)
else:
args.append(nodes.Const('file'))
+
body = parser.parse_statements(['name:endcompress'], drop_needle=True)
- return nodes.CallBlock(self.call_method('_compress', args), [], [],
+
+ # Skip the kind if used in the endblock, by using the kind in the
+ # endblock the templates are slightly more readable.
+ parser.stream.skip_if('name:' + kindarg.value)
+ return nodes.CallBlock(self.call_method('_compress_normal', args), [], [],
body).set_lineno(lineno)
- def _compress(self, kind, mode, caller):
- # This extension assumes that we won't force compression
- forced = False
+ def _compress_forced(self, kind, mode, caller):
+ return self._compress(kind, mode, caller, True)
+
+ def _compress_normal(self, kind, mode, caller):
+ return self._compress(kind, mode, caller, False)
+ def _compress(self, kind, mode, caller, forced):
mode = mode or OUTPUT_FILE
original_content = caller()
context = {
diff --git a/compressor/contrib/sekizai.py b/compressor/contrib/sekizai.py
index 7d5ac19..87966c5 100644
--- a/compressor/contrib/sekizai.py
+++ b/compressor/contrib/sekizai.py
@@ -2,7 +2,7 @@
source: https://gist.github.com/1311010
Get django-sekizai, django-compessor (and django-cms) playing nicely together
re: https://github.com/ojii/django-sekizai/issues/4
- using: https://github.com/jezdez/django_compressor.git
+ using: https://github.com/django-compressor/django-compressor.git
and: https://github.com/ojii/django-sekizai.git@0.6 or later
"""
from compressor.templatetags.compress import CompressorNode
diff --git a/compressor/css.py b/compressor/css.py
index ffd0069..e10697b 100644
--- a/compressor/css.py
+++ b/compressor/css.py
@@ -33,7 +33,7 @@ class CssCompressor(Compressor):
if append_to_previous and settings.COMPRESS_ENABLED:
self.media_nodes[-1][1].split_content.append(data)
else:
- node = CssCompressor(content=self.parser.elem_str(elem),
+ node = self.__class__(content=self.parser.elem_str(elem),
context=self.context)
node.split_content.append(data)
self.media_nodes.append((media, node))
diff --git a/compressor/exceptions.py b/compressor/exceptions.py
index 07d79a1..c2d7c60 100644
--- a/compressor/exceptions.py
+++ b/compressor/exceptions.py
@@ -38,3 +38,17 @@ class FilterDoesNotExist(Exception):
Raised when a filter class cannot be found.
"""
pass
+
+
+class TemplateDoesNotExist(Exception):
+ """
+ This exception is raised when a template does not exist.
+ """
+ pass
+
+
+class TemplateSyntaxError(Exception):
+ """
+ This exception is raised when a template syntax error is encountered.
+ """
+ pass
diff --git a/compressor/filters/base.py b/compressor/filters/base.py
index 641cf6b..284afcb 100644
--- a/compressor/filters/base.py
+++ b/compressor/filters/base.py
@@ -1,27 +1,37 @@
-from __future__ import absolute_import
+from __future__ import absolute_import, unicode_literals
+import io
import logging
import subprocess
from django.core.exceptions import ImproperlyConfigured
from django.core.files.temp import NamedTemporaryFile
from django.utils.importlib import import_module
+from django.utils.encoding import smart_text
+from django.utils import six
from compressor.conf import settings
from compressor.exceptions import FilterError
from compressor.utils import get_mod_func
-from compressor.utils.stringformat import FormattableString as fstr
+
logger = logging.getLogger("compressor.filters")
class FilterBase(object):
+ """
+ A base class for filters that does nothing.
- def __init__(self, content, filter_type=None, filename=None, verbose=0):
+ 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):
self.type = filter_type
self.content = content
self.verbose = verbose or settings.COMPRESS_VERBOSE
self.logger = logger
self.filename = filename
+ self.charset = charset
def input(self, **kwargs):
raise NotImplementedError
@@ -31,6 +41,16 @@ class FilterBase(object):
class CallbackOutputFilter(FilterBase):
+ """
+ A filter which takes function path in `callback` attribute, imports it
+ and uses that function to filter output string::
+
+ class MyFilter(CallbackOutputFilter):
+ callback = 'path.to.my.callback'
+
+ Callback should be a function which takes a string as first argument and
+ returns a string (unicode under python 2).
+ """
callback = None
args = []
kwargs = {}
@@ -39,12 +59,13 @@ class CallbackOutputFilter(FilterBase):
def __init__(self, *args, **kwargs):
super(CallbackOutputFilter, self).__init__(*args, **kwargs)
if self.callback is None:
- raise ImproperlyConfigured("The callback filter %s must define"
- "a 'callback' attribute." % self)
+ raise ImproperlyConfigured(
+ "The callback filter %s must define a 'callback' attribute." %
+ self.__class__.__name__)
try:
mod_name, func_name = get_mod_func(self.callback)
func = getattr(import_module(mod_name), func_name)
- except ImportError, e:
+ except ImportError:
if self.dependencies:
if len(self.dependencies) == 1:
warning = "dependency (%s) is" % self.dependencies[0]
@@ -53,17 +74,19 @@ class CallbackOutputFilter(FilterBase):
", ".join([dep for dep in self.dependencies]))
else:
warning = ""
- raise ImproperlyConfigured("The callback %s couldn't be imported. "
- "Make sure the %s correctly installed."
- % (self.callback, warning))
- except AttributeError, e:
- raise ImproperlyConfigured("An error occured while importing the "
+ raise ImproperlyConfigured(
+ "The callback %s couldn't be imported. Make sure the %s "
+ "correctly installed." % (self.callback, warning))
+ except AttributeError as e:
+ raise ImproperlyConfigured("An error occurred while importing the "
"callback filter %s: %s" % (self, e))
else:
self._callback_func = func
def output(self, **kwargs):
- return self._callback_func(self.content, *self.args, **self.kwargs)
+ ret = self._callback_func(self.content, *self.args, **self.kwargs)
+ assert isinstance(ret, six.text_type)
+ return ret
class CompilerFilter(FilterBase):
@@ -73,71 +96,93 @@ class CompilerFilter(FilterBase):
"""
command = None
options = ()
+ default_encoding = settings.FILE_CHARSET
def __init__(self, content, command=None, *args, **kwargs):
super(CompilerFilter, self).__init__(content, *args, **kwargs)
self.cwd = None
+
if command:
self.command = command
if self.command is None:
raise FilterError("Required attribute 'command' not given")
+
if isinstance(self.options, dict):
+ # turn dict into a tuple
new_options = ()
- for item in kwargs.iteritems():
+ for item in kwargs.items():
new_options += (item,)
self.options = new_options
- for item in kwargs.iteritems():
+
+ # append kwargs to self.options
+ for item in kwargs.items():
self.options += (item,)
- self.stdout = subprocess.PIPE
- self.stdin = subprocess.PIPE
- self.stderr = subprocess.PIPE
- self.infile, self.outfile = None, None
+
+ self.stdout = self.stdin = self.stderr = subprocess.PIPE
+ self.infile = self.outfile = None
def input(self, **kwargs):
+ encoding = self.default_encoding
options = dict(self.options)
- if self.infile is None:
- if "{infile}" in self.command:
- if self.filename is None:
- self.infile = NamedTemporaryFile(mode="w")
- self.infile.write(self.content.encode('utf8'))
- self.infile.flush()
- options["infile"] = self.infile.name
- else:
- self.infile = open(self.filename)
- options["infile"] = self.filename
- if "{outfile}" in self.command and not "outfile" in options:
+ if self.infile is None and "{infile}" in self.command:
+ # create temporary input file if needed
+ if self.filename is None:
+ self.infile = NamedTemporaryFile(mode='wb')
+ self.infile.write(self.content.encode(encoding))
+ self.infile.flush()
+ options["infile"] = self.infile.name
+ else:
+ # we use source file directly, which may be encoded using
+ # something different than utf8. If that's the case file will
+ # be included with charset="something" html attribute and
+ # charset will be available as filter's charset attribute
+ encoding = self.charset # or self.default_encoding
+ self.infile = open(self.filename)
+ options["infile"] = self.filename
+
+ if "{outfile}" in self.command and "outfile" not in options:
+ # create temporary output file if needed
ext = self.type and ".%s" % self.type or ""
self.outfile = NamedTemporaryFile(mode='r+', suffix=ext)
options["outfile"] = self.outfile.name
+
try:
- command = fstr(self.command).format(**options)
- proc = subprocess.Popen(command, shell=True, cwd=self.cwd,
- stdout=self.stdout, stdin=self.stdin, stderr=self.stderr)
+ command = self.command.format(**options)
+ proc = subprocess.Popen(
+ command, shell=True, cwd=self.cwd, stdout=self.stdout,
+ stdin=self.stdin, stderr=self.stderr)
if self.infile is None:
- filtered, err = proc.communicate(self.content.encode('utf8'))
+ # if infile is None then send content to process' stdin
+ filtered, err = proc.communicate(
+ self.content.encode(encoding))
else:
filtered, err = proc.communicate()
- except (IOError, OSError), e:
+ filtered, err = filtered.decode(encoding), err.decode(encoding)
+ except (IOError, OSError) as e:
raise FilterError('Unable to apply %s (%r): %s' %
(self.__class__.__name__, self.command, e))
else:
if proc.wait() != 0:
+ # command failed, raise FilterError exception
if not err:
err = ('Unable to apply %s (%s)' %
(self.__class__.__name__, self.command))
if filtered:
err += '\n%s' % filtered
raise FilterError(err)
+
if self.verbose:
self.logger.debug(err)
+
outfile_path = options.get('outfile')
if outfile_path:
- self.outfile = open(outfile_path, 'r')
+ with io.open(outfile_path, 'r', encoding=encoding) as file:
+ filtered = file.read()
finally:
if self.infile is not None:
self.infile.close()
if self.outfile is not None:
- filtered = self.outfile.read()
self.outfile.close()
- return filtered
+
+ return smart_text(filtered)
diff --git a/compressor/filters/cssmin/__init__.py b/compressor/filters/cssmin/__init__.py
index a71f016..073303d 100644
--- a/compressor/filters/cssmin/__init__.py
+++ b/compressor/filters/cssmin/__init__.py
@@ -7,3 +7,7 @@ class CSSMinFilter(CallbackOutputFilter):
the YUI CSS compression algorithm: http://pypi.python.org/pypi/cssmin/
"""
callback = "compressor.filters.cssmin.cssmin.cssmin"
+
+
+class rCSSMinFilter(CallbackOutputFilter):
+ callback = "compressor.filters.cssmin.rcssmin.cssmin"
diff --git a/compressor/filters/cssmin/cssmin.py b/compressor/filters/cssmin/cssmin.py
index 3dc0cc7..e8a02b0 100644
--- a/compressor/filters/cssmin/cssmin.py
+++ b/compressor/filters/cssmin/cssmin.py
@@ -28,14 +28,8 @@
#
"""`cssmin` - A Python port of the YUI CSS compressor."""
-
-try:
- from cStringIO import StringIO
-except ImportError:
- from StringIO import StringIO # noqa
import re
-
__version__ = '0.1.4'
diff --git a/compressor/filters/cssmin/rcssmin.py b/compressor/filters/cssmin/rcssmin.py
new file mode 100644
index 0000000..ff8e273
--- /dev/null
+++ b/compressor/filters/cssmin/rcssmin.py
@@ -0,0 +1,360 @@
+#!/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/compressor/filters/datauri.py b/compressor/filters/datauri.py
index 29ae40f..ee67eeb 100644
--- a/compressor/filters/datauri.py
+++ b/compressor/filters/datauri.py
@@ -1,3 +1,4 @@
+from __future__ import unicode_literals
import os
import re
import mimetypes
@@ -36,10 +37,11 @@ class DataUriFilter(FilterBase):
def data_uri_converter(self, matchobj):
url = matchobj.group(1).strip(' \'"')
- if not url.startswith('data:'):
+ if not url.startswith('data:') and not url.startswith('//'):
path = self.get_file_path(url)
if os.stat(path).st_size <= settings.COMPRESS_DATA_URI_MAX_SIZE:
- data = b64encode(open(path, 'rb').read())
+ with open(path, 'rb') as file:
+ data = b64encode(file.read()).decode('ascii')
return 'url("data:%s;base64,%s")' % (
mimetypes.guess_type(path)[0], data)
return 'url("%s")' % url
diff --git a/compressor/filters/jsmin/rjsmin.py b/compressor/filters/jsmin/rjsmin.py
index ff31b17..6eedf2f 100755
--- a/compressor/filters/jsmin/rjsmin.py
+++ b/compressor/filters/jsmin/rjsmin.py
@@ -1,8 +1,7 @@
#!/usr/bin/env python
# -*- coding: ascii -*-
-# flake8: noqa
#
-# Copyright 2011, 2012
+# Copyright 2011 - 2013
# Andr\xe9 Malo or his licensors, as applicable
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -60,7 +59,7 @@ __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.5'
+__version__ = '1.0.7'
__all__ = ['jsmin']
import re as _re
@@ -135,10 +134,10 @@ def _make_jsmin(python_only=False):
if last is not None:
result.append((first, last))
return ''.join(['%s%s%s' % (
- chr(first),
- last > first + 1 and '-' or '',
- last != first and chr(last) or ''
- ) for first, last in result])
+ 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)
@@ -172,11 +171,17 @@ def _make_jsmin(python_only=False):
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'([^\047"/\000-\040]+)'
- r'|(%(strings)s[^\047"/\000-\040]*)'
- r'|(?:(?<=%(preregex1)s)%(space)s*(%(regex)s[^\047"/\000-\040]*))'
- r'|(?:(?<=%(preregex2)s)%(space)s*(%(regex)s[^\047"/\000-\040]*))'
+ 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)'
@@ -186,7 +191,7 @@ def _make_jsmin(python_only=False):
r'|%(space)s+'
r'|(?:%(newline)s%(space)s*)+'
) % locals()).sub
- #print space_sub.__self__.pattern
+ # print space_sub.__self__.pattern
def space_subber(match):
""" Substitution callback """
@@ -265,25 +270,28 @@ def jsmin_for_posers(script):
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'\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]*(?:(?:\\[^\r\n]|(?:\[[^\\\]\r\n]*'
- r'(?:\\[^\r\n][^\\\]\r\n]*)*\]))[^/\\\[\r\n]*)*/)[^\047"/\000-\040]*'
- r'))|(?:(?<=[\000-#%-,./:-@\[-^`{-~-]return)(?:[\000-\011\013\014\01'
- r'6-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*((?:/(?![\r\n/*])[^/'
- r'\\\[\r\n]*(?:(?:\\[^\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\01'
- r'4\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))*)+(?=[^\000-\040"#'
- r'%-\047)*,./:-@\\-^`|-~])|(?<=[^\000-#%-,./:-@\[-^`{-~-])((?:[\000-'
- r'\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=[^'
- r'\000-#%-,./:-@\[-^`{-~-])|(?<=\+)((?:[\000-\011\013\014\016-\040]|'
- r'(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=\+)|(?<=-)((?:[\000-\011\0'
- r'13\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/)))+(?=-)|(?:[\0'
- r'00-\011\013\014\016-\040]|(?:/\*[^*]*\*+(?:[^/*][^*]*\*+)*/))+|(?:'
- r'(?:(?://[^\r\n]*)?[\r\n])(?:[\000-\011\013\014\016-\040]|(?:/\*[^*'
- r']*\*+(?:[^/*][^*]*\*+)*/))*)+', subber, '\n%s\n' % script
+ 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()
diff --git a/compressor/filters/yuglify.py b/compressor/filters/yuglify.py
new file mode 100644
index 0000000..07066cc
--- /dev/null
+++ b/compressor/filters/yuglify.py
@@ -0,0 +1,26 @@
+from compressor.conf import settings
+from compressor.filters import CompilerFilter
+
+
+class YUglifyFilter(CompilerFilter):
+ command = "{binary} {args}"
+
+ def __init__(self, *args, **kwargs):
+ super(YUglifyFilter, self).__init__(*args, **kwargs)
+ self.command += ' --type=%s' % self.type
+
+
+class YUglifyCSSFilter(YUglifyFilter):
+ type = 'css'
+ options = (
+ ("binary", settings.COMPRESS_YUGLIFY_BINARY),
+ ("args", settings.COMPRESS_YUGLIFY_CSS_ARGUMENTS),
+ )
+
+
+class YUglifyJSFilter(YUglifyFilter):
+ type = 'js'
+ options = (
+ ("binary", settings.COMPRESS_YUGLIFY_BINARY),
+ ("args", settings.COMPRESS_YUGLIFY_JS_ARGUMENTS),
+ )
diff --git a/compressor/management/commands/compress.py b/compressor/management/commands/compress.py
index 1ae0778..6be215e 100644
--- a/compressor/management/commands/compress.py
+++ b/compressor/management/commands/compress.py
@@ -1,105 +1,33 @@
# flake8: noqa
import os
import sys
-from types import MethodType
+
from fnmatch import fnmatch
from optparse import make_option
-try:
- from cStringIO import StringIO
-except ImportError:
- from StringIO import StringIO # noqa
-
-from django.core.management.base import NoArgsCommand, CommandError
-from django.template import (Context, Template,
- TemplateDoesNotExist, TemplateSyntaxError)
+from django.core.management.base import NoArgsCommand, 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.defaulttags import IfNode
-from django.template.loader_tags import (ExtendsNode, BlockNode,
- BLOCK_CONTEXT_KEY)
-
-try:
- from django.template.loaders.cached import Loader as CachedLoader
-except ImportError:
- CachedLoader = None # noqa
from compressor.cache import get_offline_hexdigest, write_offline_manifest
from compressor.conf import settings
-from compressor.exceptions import OfflineGenerationError
+from compressor.exceptions import (OfflineGenerationError, TemplateSyntaxError,
+ TemplateDoesNotExist)
from compressor.templatetags.compress import CompressorNode
-
-def patched_render(self, context):
- # 'Fake' _render method that just returns the context instead of
- # rendering. It also checks whether the first node is an extend node or
- # not, to be able to handle complex inheritance chain.
- self._render_firstnode = MethodType(patched_render_firstnode, self)
- self._render_firstnode(context)
-
- # Cleanup, uninstall our _render monkeypatch now that it has been called
- self._render = self._old_render
- return context
-
-
-def patched_render_firstnode(self, context):
- # If this template has a ExtendsNode, we want to find out what
- # should be put in render_context to make the {% block ... %}
- # tags work.
- #
- # We can't fully render the base template(s) (we don't have the
- # full context vars - only what's necessary to render the compress
- # nodes!), therefore we hack the ExtendsNode we found, patching
- # its get_parent method so that rendering the ExtendsNode only
- # gives us the blocks content without doing any actual rendering.
- extra_context = {}
+if six.PY3:
+ # there is an 'io' module in python 2.6+, but io.StringIO does not
+ # accept regular strings, just unicode objects
+ from io import StringIO
+else:
try:
- firstnode = self.nodelist[0]
- except IndexError:
- firstnode = None
- if isinstance(firstnode, ExtendsNode):
- firstnode._log = self._log
- firstnode._log_verbosity = self._log_verbosity
- firstnode._old_get_parent = firstnode.get_parent
- firstnode.get_parent = MethodType(patched_get_parent, firstnode)
- try:
- extra_context = firstnode.render(context)
- context.render_context = extra_context.render_context
- # We aren't rendering {% block %} tags, but we want
- # {{ block.super }} inside {% compress %} inside {% block %}s to
- # work. Therefore, we need to pop() the last block context for
- # each block name, to emulate what would have been done if the
- # {% block %} had been fully rendered.
- for blockname in firstnode.blocks.keys():
- context.render_context[BLOCK_CONTEXT_KEY].pop(blockname)
- except (IOError, TemplateSyntaxError, TemplateDoesNotExist):
- # That first node we are trying to render might cause more errors
- # that we didn't catch when simply creating a Template instance
- # above, so we need to catch that (and ignore it, just like above)
- # as well.
- if self._log_verbosity > 0:
- self._log.write("Caught error when rendering extend node from "
- "template %s\n" % getattr(self, 'name', self))
- return None
- finally:
- # Cleanup, uninstall our get_parent monkeypatch now that it has been called
- firstnode.get_parent = firstnode._old_get_parent
- return extra_context
-
-
-def patched_get_parent(self, context):
- # Patch template returned by extendsnode's get_parent to make sure their
- # _render method is just returning the context instead of actually
- # rendering stuff.
- # In addition, this follows the inheritance chain by looking if the first
- # node of the template is an extend node itself.
- compiled_template = self._old_get_parent(context)
- compiled_template._log = self._log
- compiled_template._log_verbosity = self._log_verbosity
- compiled_template._old_render = compiled_template._render
- compiled_template._render = MethodType(patched_render, compiled_template)
- return compiled_template
+ from cStringIO import StringIO
+ except ImportError:
+ from StringIO import StringIO
class Command(NoArgsCommand):
@@ -117,6 +45,9 @@ class Command(NoArgsCommand):
"(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.", dest='follow_links'),
+ make_option('--engine', default="django", action="store",
+ help="Specifies the templating engine. jinja2 or django",
+ dest="engine"),
)
requires_model_validation = False
@@ -134,7 +65,7 @@ class Command(NoArgsCommand):
# Force django to calculate template_source_loaders from
# TEMPLATE_LOADERS settings, by asking to find a dummy template
source, name = finder_func('test')
- except TemplateDoesNotExist:
+ 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
@@ -151,13 +82,28 @@ class Command(NoArgsCommand):
# )
# The loaders will return django.template.loaders.filesystem.Loader
# and django.template.loaders.app_directories.Loader
+ # The cached Loader and similar ones include a 'loaders' attribute
+ # so we look for that.
for loader in template_source_loaders:
- if CachedLoader is not None and isinstance(loader, CachedLoader):
+ if hasattr(loader, 'loaders'):
loaders.extend(loader.loaders)
else:
loaders.append(loader)
return loaders
+ def __get_parser(self, engine):
+ if engine == "jinja2":
+ from compressor.offline.jinja2 import Jinja2Parser
+ env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT()
+ parser = Jinja2Parser(charset=settings.FILE_CHARSET, env=env)
+ elif engine == "django":
+ from compressor.offline.django import DjangoParser
+ parser = DjangoParser(charset=settings.FILE_CHARSET)
+ else:
+ raise OfflineGenerationError("Invalid templating engine specified.")
+
+ return parser
+
def compress(self, log=None, **options):
"""
Searches templates containing 'compress' nodes and compresses them
@@ -210,20 +156,18 @@ 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()
for template_name in templates:
try:
- template_file = open(template_name)
- try:
- template = Template(template_file.read().decode(
- settings.FILE_CHARSET))
- finally:
- template_file.close()
+ template = parser.parse(template_name)
except IOError: # unreadable file -> ignore
if verbosity > 0:
log.write("Unreadable template at: %s\n" % template_name)
continue
- except TemplateSyntaxError, e: # broken template -> ignore
+ except TemplateSyntaxError as e: # broken template -> ignore
if verbosity > 0:
log.write("Invalid template %s: %s\n" % (template_name, e))
continue
@@ -235,7 +179,13 @@ class Command(NoArgsCommand):
if verbosity > 0:
log.write("UnicodeDecodeError while trying to read "
"template %s\n" % template_name)
- nodes = list(self.walk_nodes(template))
+ try:
+ nodes = list(parser.walk_nodes(template))
+ except (TemplateDoesNotExist, TemplateSyntaxError) as e:
+ # Could be an error in some base template
+ if verbosity > 0:
+ log.write("Error parsing template %s: %s\n" % (template_name, e))
+ continue
if nodes:
template.template_name = template_name
compressor_nodes.setdefault(template, []).extend(nodes)
@@ -255,27 +205,28 @@ class Command(NoArgsCommand):
count = 0
results = []
offline_manifest = SortedDict()
- for template, nodes in compressor_nodes.iteritems():
- context = Context(settings.COMPRESS_OFFLINE_CONTEXT)
+ 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
- template._render_firstnode = MethodType(patched_render_firstnode, template)
- extra_context = template._render_firstnode(context)
- if extra_context is None:
- # Something is wrong - ignore this template
+
+ if not parser.process_template(template, context):
continue
+
for node in nodes:
context.push()
- if extra_context and node._block_name:
- # Give a block context to the node if it was found inside
- # a {% block %}.
- context['block'] = context.render_context[BLOCK_CONTEXT_KEY].get_block(node._block_name)
- if context['block']:
- context['block'].context = context
- key = get_offline_hexdigest(node.nodelist.render(context))
+ 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 = node.render(context, forced=True)
- except Exception, e:
+ 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
@@ -289,23 +240,6 @@ class Command(NoArgsCommand):
(count, len(compressor_nodes)))
return count, results
- def get_nodelist(self, node):
- # Check if node is an ```if``` switch with true and false branches
- if hasattr(node, 'nodelist_true') and hasattr(node, 'nodelist_false'):
- return node.nodelist_true + node.nodelist_false
- return getattr(node, "nodelist", [])
-
- def walk_nodes(self, node, block_name=None):
- for node in self.get_nodelist(node):
- if isinstance(node, BlockNode):
- block_name = node.name
- if isinstance(node, CompressorNode) and node.is_offline_compression_enabled(forced=True):
- node._block_name = block_name
- yield node
- else:
- for node in self.walk_nodes(node, block_name=block_name):
- yield node
-
def handle_extensions(self, extensions=('html',)):
"""
organizes multiple extensions that are separated with commas or
@@ -331,7 +265,7 @@ class Command(NoArgsCommand):
if not settings.COMPRESS_ENABLED and not options.get("force"):
raise CommandError(
"Compressor is disabled. Set the COMPRESS_ENABLED "
- "settting or use --force to override.")
+ "setting or use --force to override.")
if not settings.COMPRESS_OFFLINE:
if not options.get("force"):
raise CommandError(
diff --git a/compressor/management/commands/mtime_cache.py b/compressor/management/commands/mtime_cache.py
index bfea571..e96f004 100644
--- a/compressor/management/commands/mtime_cache.py
+++ b/compressor/management/commands/mtime_cache.py
@@ -74,9 +74,9 @@ 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)
+ print("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)
+ print("Added mtimes of %d files to cache." % len(files_to_add))
diff --git a/compressor/offline/__init__.py b/compressor/offline/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/compressor/offline/__init__.py
diff --git a/compressor/offline/django.py b/compressor/offline/django.py
new file mode 100644
index 0000000..6541471
--- /dev/null
+++ b/compressor/offline/django.py
@@ -0,0 +1,143 @@
+from __future__ import absolute_import
+import io
+from copy import copy
+
+from django import template
+from django.conf import settings
+from django.template import Template
+from django.template import Context
+from django.template.base import Node, VariableNode, TextNode, NodeList
+from django.template.defaulttags import IfNode
+from django.template.loader_tags import ExtendsNode, BlockNode, BlockContext
+
+
+from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist
+from compressor.templatetags.compress import CompressorNode
+
+
+def handle_extendsnode(extendsnode, block_context=None):
+ """Create a copy of Node tree of a derived template replacing
+ all blocks tags with the nodes of appropriate blocks.
+ Also handles {{ block.super }} tags.
+ """
+ if block_context is None:
+ block_context = BlockContext()
+ blocks = dict((n.name, n) for n in
+ extendsnode.nodelist.get_nodes_by_type(BlockNode))
+ block_context.add_blocks(blocks)
+
+ context = Context(settings.COMPRESS_OFFLINE_CONTEXT)
+ compiled_parent = extendsnode.get_parent(context)
+ parent_nodelist = compiled_parent.nodelist
+ # If the parent template has an ExtendsNode it is not the root.
+ for node in parent_nodelist:
+ # The ExtendsNode has to be the first non-text node.
+ if not isinstance(node, TextNode):
+ if isinstance(node, ExtendsNode):
+ return handle_extendsnode(node, block_context)
+ break
+ # Add blocks of the root template to block context.
+ blocks = dict((n.name, n) for n in
+ parent_nodelist.get_nodes_by_type(BlockNode))
+ block_context.add_blocks(blocks)
+
+ block_stack = []
+ new_nodelist = remove_block_nodes(parent_nodelist, block_stack, block_context)
+ return new_nodelist
+
+
+def remove_block_nodes(nodelist, block_stack, block_context):
+ new_nodelist = NodeList()
+ for node in nodelist:
+ if isinstance(node, VariableNode):
+ var_name = node.filter_expression.token.strip()
+ if var_name == 'block.super':
+ if not block_stack:
+ continue
+ node = block_context.get_block(block_stack[-1].name)
+ if isinstance(node, BlockNode):
+ expanded_block = expand_blocknode(node, block_stack, block_context)
+ new_nodelist.extend(expanded_block)
+ else:
+ # IfNode has nodelist as a @property so we can not modify it
+ if isinstance(node, IfNode):
+ node = copy(node)
+ for i, (condition, sub_nodelist) in enumerate(node.conditions_nodelists):
+ sub_nodelist = remove_block_nodes(sub_nodelist, block_stack, block_context)
+ node.conditions_nodelists[i] = (condition, sub_nodelist)
+ else:
+ for attr in node.child_nodelists:
+ sub_nodelist = getattr(node, attr, None)
+ if sub_nodelist:
+ sub_nodelist = remove_block_nodes(sub_nodelist, block_stack, block_context)
+ node = copy(node)
+ setattr(node, attr, sub_nodelist)
+ new_nodelist.append(node)
+ return new_nodelist
+
+
+def expand_blocknode(node, block_stack, block_context):
+ popped_block = block = block_context.pop(node.name)
+ if block is None:
+ block = node
+ block_stack.append(block)
+ expanded_nodelist = remove_block_nodes(block.nodelist, block_stack, block_context)
+ block_stack.pop()
+ if popped_block is not None:
+ block_context.push(node.name, popped_block)
+ return expanded_nodelist
+
+
+class DjangoParser(object):
+ def __init__(self, charset):
+ self.charset = charset
+
+ def parse(self, template_name):
+ with io.open(template_name, mode='rb') as file:
+ try:
+ return Template(file.read().decode(self.charset))
+ except template.TemplateSyntaxError as e:
+ raise TemplateSyntaxError(str(e))
+ except template.TemplateDoesNotExist as e:
+ raise TemplateDoesNotExist(str(e))
+
+ def process_template(self, template, context):
+ return True
+
+ def get_init_context(self, offline_context):
+ return offline_context
+
+ def process_node(self, template, context, node):
+ pass
+
+ def render_nodelist(self, template, context, node):
+ return node.nodelist.render(context)
+
+ def render_node(self, template, context, node):
+ return node.render(context, forced=True)
+
+ def get_nodelist(self, node):
+ if isinstance(node, ExtendsNode):
+ try:
+ return handle_extendsnode(node)
+ except template.TemplateSyntaxError as e:
+ raise TemplateSyntaxError(str(e))
+ except template.TemplateDoesNotExist as e:
+ raise TemplateDoesNotExist(str(e))
+
+ # Check if node is an ```if``` switch with true and false branches
+ nodelist = []
+ if isinstance(node, Node):
+ for attr in node.child_nodelists:
+ nodelist += getattr(node, attr, [])
+ else:
+ nodelist = getattr(node, 'nodelist', [])
+ return nodelist
+
+ def walk_nodes(self, node):
+ for node in self.get_nodelist(node):
+ if isinstance(node, CompressorNode) and node.is_offline_compression_enabled(forced=True):
+ yield node
+ else:
+ for node in self.walk_nodes(node):
+ yield node
diff --git a/compressor/offline/jinja2.py b/compressor/offline/jinja2.py
new file mode 100644
index 0000000..feee818
--- /dev/null
+++ b/compressor/offline/jinja2.py
@@ -0,0 +1,125 @@
+from __future__ import absolute_import
+import io
+
+import jinja2
+import jinja2.ext
+from jinja2 import nodes
+from jinja2.ext import Extension
+from jinja2.nodes import CallBlock, Call, ExtensionAttribute
+
+from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist
+
+
+def flatten_context(context):
+ if hasattr(context, 'dicts'):
+ context_dict = {}
+
+ for d in context.dicts:
+ context_dict.update(d)
+
+ return context_dict
+
+ return context
+
+
+class SpacelessExtension(Extension):
+ """
+ Functional "spaceless" extension equivalent to Django's.
+
+ See: https://github.com/django/django/blob/master/django/template/defaulttags.py
+ """
+
+ tags = set(['spaceless'])
+
+ def parse(self, parser):
+ lineno = next(parser.stream).lineno
+ body = parser.parse_statements(['name:endspaceless'], drop_needle=True)
+
+ return nodes.CallBlock(self.call_method('_spaceless', []),
+ [], [], body).set_lineno(lineno)
+
+ def _spaceless(self, caller):
+ from django.utils.html import strip_spaces_between_tags
+
+ return strip_spaces_between_tags(caller().strip())
+
+
+def url_for(mod, filename):
+ """
+ Incomplete emulation of Flask's url_for.
+ """
+ from django.contrib.staticfiles.templatetags import staticfiles
+
+ if mod == "static":
+ return staticfiles.static(filename)
+
+ return ""
+
+
+class Jinja2Parser(object):
+ COMPRESSOR_ID = 'compressor.contrib.jinja2ext.CompressorExtension'
+
+ def __init__(self, charset, env):
+ self.charset = charset
+ self.env = env
+
+ def parse(self, template_name):
+ with io.open(template_name, mode='rb') as file:
+ try:
+ template = self.env.parse(file.read().decode(self.charset))
+ except jinja2.TemplateSyntaxError as e:
+ raise TemplateSyntaxError(str(e))
+ except jinja2.TemplateNotFound as e:
+ raise TemplateDoesNotExist(str(e))
+
+ return template
+
+ def process_template(self, template, context):
+ return True
+
+ def get_init_context(self, offline_context):
+ # Don't need to add filters and tests to the context, as Jinja2 will
+ # automatically look for them in self.env.filters and self.env.tests.
+ # This is tested by test_complex and test_templatetag.
+
+ # Allow offline context to override the globals.
+ context = self.env.globals.copy()
+ context.update(offline_context)
+
+ return context
+
+ def process_node(self, template, context, node):
+ pass
+
+ def _render_nodes(self, template, context, nodes):
+ compiled_node = self.env.compile(jinja2.nodes.Template(nodes))
+ template = jinja2.Template.from_code(self.env, compiled_node, {})
+ flat_context = flatten_context(context)
+
+ return template.render(flat_context)
+
+ def render_nodelist(self, template, context, node):
+ return self._render_nodes(template, context, node.body)
+
+ def render_node(self, template, context, node):
+ return self._render_nodes(template, context, [node])
+
+ def get_nodelist(self, node):
+ body = getattr(node, "body", getattr(node, "nodes", []))
+
+ if isinstance(node, jinja2.nodes.If):
+ return body + node.else_
+
+ return body
+
+ def walk_nodes(self, node, block_name=None):
+ for node in self.get_nodelist(node):
+ if (isinstance(node, CallBlock) and
+ isinstance(node.call, Call) and
+ isinstance(node.call.node, ExtensionAttribute) and
+ node.call.node.identifier == self.COMPRESSOR_ID):
+ node.call.node.name = '_compress_forced'
+ yield node
+ else:
+ for node in self.walk_nodes(node, block_name=block_name):
+ yield node
diff --git a/compressor/parser/__init__.py b/compressor/parser/__init__.py
index bc8c18c..a3fe78f 100644
--- a/compressor/parser/__init__.py
+++ b/compressor/parser/__init__.py
@@ -1,3 +1,4 @@
+from django.utils import six
from django.utils.functional import LazyObject
from django.utils.importlib import import_module
@@ -11,8 +12,9 @@ from compressor.parser.html5lib import Html5LibParser # noqa
class AutoSelectParser(LazyObject):
options = (
- ('lxml.html', LxmlParser), # lxml, extremely fast
- ('HTMLParser', HtmlParser), # fast and part of the Python stdlib
+ # TODO: make lxml.html parser first again
+ (six.moves.html_parser.__name__, HtmlParser), # fast and part of the Python stdlib
+ ('lxml.html', LxmlParser), # lxml, extremely fast
)
def __init__(self, content):
diff --git a/compressor/parser/beautifulsoup.py b/compressor/parser/beautifulsoup.py
index 498cde8..d143df4 100644
--- a/compressor/parser/beautifulsoup.py
+++ b/compressor/parser/beautifulsoup.py
@@ -1,6 +1,7 @@
from __future__ import absolute_import
from django.core.exceptions import ImproperlyConfigured
-from django.utils.encoding import smart_unicode
+from django.utils import six
+from django.utils.encoding import smart_text
from compressor.exceptions import ParserError
from compressor.parser import ParserBase
@@ -12,18 +13,27 @@ class BeautifulSoupParser(ParserBase):
@cached_property
def soup(self):
try:
- from BeautifulSoup import BeautifulSoup
+ if six.PY3:
+ from bs4 import BeautifulSoup
+ else:
+ from BeautifulSoup import BeautifulSoup
return BeautifulSoup(self.content)
- except ImportError, err:
+ except ImportError as err:
raise ImproperlyConfigured("Error while importing BeautifulSoup: %s" % err)
- except Exception, err:
+ except Exception as err:
raise ParserError("Error while initializing Parser: %s" % err)
def css_elems(self):
- return self.soup.findAll({'link': True, 'style': True})
+ if six.PY3:
+ return self.soup.find_all({'link': True, 'style': True})
+ else:
+ return self.soup.findAll({'link': True, 'style': True})
def js_elems(self):
- return self.soup.findAll('script')
+ if six.PY3:
+ return self.soup.find_all('script')
+ else:
+ return self.soup.findAll('script')
def elem_attribs(self, elem):
return dict(elem.attrs)
@@ -35,4 +45,4 @@ class BeautifulSoupParser(ParserBase):
return elem.name
def elem_str(self, elem):
- return smart_unicode(elem)
+ return smart_text(elem)
diff --git a/compressor/parser/default_htmlparser.py b/compressor/parser/default_htmlparser.py
index 8425d77..80272cb 100644
--- a/compressor/parser/default_htmlparser.py
+++ b/compressor/parser/default_htmlparser.py
@@ -1,13 +1,13 @@
-from HTMLParser import HTMLParser
-from django.utils.encoding import smart_unicode
+from django.utils import six
+from django.utils.encoding import smart_text
+
from compressor.exceptions import ParserError
from compressor.parser import ParserBase
-class DefaultHtmlParser(ParserBase, HTMLParser):
-
+class DefaultHtmlParser(ParserBase, six.moves.html_parser.HTMLParser):
def __init__(self, content):
- HTMLParser.__init__(self)
+ six.moves.html_parser.HTMLParser.__init__(self)
self.content = content
self._css_elems = []
self._js_elems = []
@@ -15,7 +15,7 @@ class DefaultHtmlParser(ParserBase, HTMLParser):
try:
self.feed(self.content)
self.close()
- except Exception, err:
+ except Exception as err:
lineno = err.lineno
line = self.content.splitlines()[lineno]
raise ParserError("Error while initializing HtmlParser: %s (line: %s)" % (err, repr(line)))
@@ -65,7 +65,7 @@ class DefaultHtmlParser(ParserBase, HTMLParser):
return elem['attrs_dict']
def elem_content(self, elem):
- return smart_unicode(elem['text'])
+ return smart_text(elem['text'])
def elem_str(self, elem):
tag = {}
diff --git a/compressor/parser/html5lib.py b/compressor/parser/html5lib.py
index 7fee590..b1d0948 100644
--- a/compressor/parser/html5lib.py
+++ b/compressor/parser/html5lib.py
@@ -1,6 +1,6 @@
from __future__ import absolute_import
-from django.utils.encoding import smart_unicode
from django.core.exceptions import ImproperlyConfigured
+from django.utils.encoding import smart_text
from compressor.exceptions import ParserError
from compressor.parser import ParserBase
@@ -15,42 +15,45 @@ class Html5LibParser(ParserBase):
self.html5lib = html5lib
def _serialize(self, elem):
- fragment = self.html5lib.treebuilders.simpletree.DocumentFragment()
- fragment.appendChild(elem)
- return self.html5lib.serialize(fragment,
- quote_attr_values=True, omit_optional_tags=False)
+ return self.html5lib.serialize(
+ elem, tree="etree", quote_attr_values=True,
+ omit_optional_tags=False, use_trailing_solidus=True,
+ )
def _find(self, *names):
- for node in self.html.childNodes:
- if node.type == 5 and node.name in names:
- yield node
+ for elem in self.html:
+ if elem.tag in names:
+ yield elem
@cached_property
def html(self):
try:
- return self.html5lib.parseFragment(self.content)
- except ImportError, err:
+ return self.html5lib.parseFragment(self.content, treebuilder="etree")
+ except ImportError as err:
raise ImproperlyConfigured("Error while importing html5lib: %s" % err)
- except Exception, err:
+ except Exception as err:
raise ParserError("Error while initializing Parser: %s" % err)
def css_elems(self):
- return self._find('style', 'link')
+ return self._find('{http://www.w3.org/1999/xhtml}link',
+ '{http://www.w3.org/1999/xhtml}style')
def js_elems(self):
- return self._find('script')
+ return self._find('{http://www.w3.org/1999/xhtml}script')
def elem_attribs(self, elem):
- return elem.attributes
+ return elem.attrib
def elem_content(self, elem):
- return elem.childNodes[0].value
+ return smart_text(elem.text)
def elem_name(self, elem):
- return elem.name
+ if '}' in elem.tag:
+ return elem.tag.split('}')[1]
+ return elem.tag
def elem_str(self, elem):
# This method serializes HTML in a way that does not pass all tests.
# However, this method is only called in tests anyway, so it doesn't
# really matter.
- return smart_unicode(self._serialize(elem))
+ return smart_text(self._serialize(elem))
diff --git a/compressor/parser/lxml.py b/compressor/parser/lxml.py
index 7bbb561..64a8fcb 100644
--- a/compressor/parser/lxml.py
+++ b/compressor/parser/lxml.py
@@ -1,6 +1,8 @@
-from __future__ import absolute_import
+from __future__ import absolute_import, unicode_literals
+
from django.core.exceptions import ImproperlyConfigured
-from django.utils.encoding import smart_unicode
+from django.utils import six
+from django.utils.encoding import smart_text
from compressor.exceptions import ParserError
from compressor.parser import ParserBase
@@ -8,28 +10,50 @@ from compressor.utils.decorators import cached_property
class LxmlParser(ParserBase):
-
+ """
+ LxmlParser will use `lxml.html` parser to parse rendered contents of
+ {% compress %} tag. Under python 2 it will also try to use beautiful
+ soup parser in case of any problems with encoding.
+ """
def __init__(self, content):
try:
- from lxml.html import fromstring, soupparser
+ from lxml.html import fromstring
from lxml.etree import tostring
- self.fromstring = fromstring
- self.soupparser = soupparser
- self.tostring = tostring
- except ImportError, err:
+ except ImportError as err:
raise ImproperlyConfigured("Error while importing lxml: %s" % err)
- except Exception, err:
- raise ParserError("Error while initializing Parser: %s" % err)
+ except Exception as err:
+ raise ParserError("Error while initializing parser: %s" % err)
+
+ if not six.PY3:
+ # soupparser uses Beautiful Soup 3 which does not run on python 3.x
+ try:
+ from lxml.html import soupparser
+ except ImportError as err:
+ soupparser = None
+ except Exception as err:
+ raise ParserError("Error while initializing parser: %s" % err)
+ else:
+ soupparser = None
+
+ self.soupparser = soupparser
+ self.fromstring = fromstring
+ self.tostring = tostring
super(LxmlParser, self).__init__(content)
@cached_property
def tree(self):
+ """
+ Document tree.
+ """
content = '<root>%s</root>' % self.content
tree = self.fromstring(content)
try:
- self.tostring(tree, encoding=unicode)
+ self.tostring(tree, encoding=six.text_type)
except UnicodeDecodeError:
- tree = self.soupparser.fromstring(content)
+ if self.soupparser: # use soup parser on python 2
+ tree = self.soupparser.fromstring(content)
+ else: # raise an error on python 3
+ raise
return tree
def css_elems(self):
@@ -43,14 +67,14 @@ class LxmlParser(ParserBase):
return elem.attrib
def elem_content(self, elem):
- return smart_unicode(elem.text)
+ return smart_text(elem.text)
def elem_name(self, elem):
return elem.tag
def elem_str(self, elem):
- elem_as_string = smart_unicode(
- self.tostring(elem, method='html', encoding=unicode))
+ elem_as_string = smart_text(
+ self.tostring(elem, method='html', encoding=six.text_type))
if elem.tag == 'link':
# This makes testcases happy
return elem_as_string.replace('>', ' />')
diff --git a/compressor/storage.py b/compressor/storage.py
index be9b066..16419a8 100644
--- a/compressor/storage.py
+++ b/compressor/storage.py
@@ -1,7 +1,9 @@
+from __future__ import unicode_literals
import errno
import gzip
-from os import path
+import os
from datetime import datetime
+import time
from django.core.files.storage import FileSystemStorage, get_storage_class
from django.utils.functional import LazyObject, SimpleLazyObject
@@ -26,13 +28,13 @@ class CompressorFileStorage(FileSystemStorage):
*args, **kwargs)
def accessed_time(self, name):
- return datetime.fromtimestamp(path.getatime(self.path(name)))
+ return datetime.fromtimestamp(os.path.getatime(self.path(name)))
def created_time(self, name):
- return datetime.fromtimestamp(path.getctime(self.path(name)))
+ return datetime.fromtimestamp(os.path.getctime(self.path(name)))
def modified_time(self, name):
- return datetime.fromtimestamp(path.getmtime(self.path(name)))
+ return datetime.fromtimestamp(os.path.getmtime(self.path(name)))
def get_available_name(self, name):
"""
@@ -49,7 +51,7 @@ class CompressorFileStorage(FileSystemStorage):
"""
try:
super(CompressorFileStorage, self).delete(name)
- except OSError, e:
+ except OSError as e:
if e.errno != errno.ENOENT:
raise
@@ -65,9 +67,25 @@ class GzipCompressorFileStorage(CompressorFileStorage):
"""
def save(self, filename, content):
filename = super(GzipCompressorFileStorage, self).save(filename, content)
- out = gzip.open(u'%s.gz' % self.path(filename), 'wb')
- out.writelines(open(self.path(filename), 'rb'))
- out.close()
+ orig_path = self.path(filename)
+ compressed_path = '%s.gz' % orig_path
+
+ f_in = open(orig_path, 'rb')
+ f_out = open(compressed_path, 'wb')
+ try:
+ f_out = gzip.GzipFile(fileobj=f_out)
+ f_out.write(f_in.read())
+ finally:
+ f_out.close()
+ f_in.close()
+ # Ensure the file timestamps match.
+ # os.stat() returns nanosecond resolution on Linux, but os.utime()
+ # only sets microsecond resolution. Set times on both files to
+ # ensure they are equal.
+ stamp = time.time()
+ os.utime(orig_path, (stamp, stamp))
+ os.utime(compressed_path, (stamp, stamp))
+
return filename
diff --git a/compressor/templatetags/compress.py b/compressor/templatetags/compress.py
index 870668a..a45f454 100644
--- a/compressor/templatetags/compress.py
+++ b/compressor/templatetags/compress.py
@@ -1,5 +1,6 @@
from django import template
from django.core.exceptions import ImproperlyConfigured
+from django.utils import six
from compressor.cache import (cache_get, cache_set, get_offline_hexdigest,
get_offline_manifest, get_templatetag_cachekey)
@@ -50,7 +51,7 @@ class CompressorMixin(object):
Check if offline compression is enabled or forced
Defaults to just checking the settings and forced argument,
- but can be overriden to completely disable compression for
+ but can be overridden to completely disable compression for
a subclass, for instance.
"""
return (settings.COMPRESS_ENABLED and
@@ -107,6 +108,7 @@ class CompressorMixin(object):
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:
@@ -199,7 +201,7 @@ def compress(parser, token):
if len(args) >= 3:
mode = args[2]
- if not mode in OUTPUT_MODES:
+ if mode not in OUTPUT_MODES:
raise template.TemplateSyntaxError(
"%r's second argument must be '%s' or '%s'." %
(args[0], OUTPUT_FILE, OUTPUT_INLINE))
diff --git a/compressor/test_settings.py b/compressor/test_settings.py
index 0e8a768..a5abf92 100644
--- a/compressor/test_settings.py
+++ b/compressor/test_settings.py
@@ -5,18 +5,17 @@ TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'tests')
COMPRESS_CACHE_BACKEND = 'locmem://'
-if django.VERSION[:2] >= (1, 3):
- DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': ':memory:',
- }
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': ':memory:',
}
-else:
- DATABASE_ENGINE = 'sqlite3'
+}
INSTALLED_APPS = [
'compressor',
+ 'coffin',
+ 'jingo',
]
STATIC_URL = '/static/'
@@ -31,6 +30,11 @@ TEMPLATE_DIRS = (
os.path.join(TEST_DIR, 'test_templates'),
)
-TEST_RUNNER = 'discover_runner.DiscoverRunner'
+if django.VERSION[:2] < (1, 6):
+ TEST_RUNNER = 'discover_runner.DiscoverRunner'
SECRET_KEY = "iufoj=mibkpdz*%bob952x(%49rqgv8gg45k36kjcg76&-y5=!"
+
+PASSWORD_HASHERS = (
+ 'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
+)
diff --git a/compressor/tests/precompiler.py b/compressor/tests/precompiler.py
index 4c01964..059a322 100644
--- a/compressor/tests/precompiler.py
+++ b/compressor/tests/precompiler.py
@@ -28,7 +28,7 @@ def main():
with open(options.outfile, 'w') as f:
f.write(content)
else:
- print content
+ print(content)
if __name__ == '__main__':
diff --git a/compressor/tests/test_base.py b/compressor/tests/test_base.py
index 8678e32..46b1d91 100644
--- a/compressor/tests/test_base.py
+++ b/compressor/tests/test_base.py
@@ -1,11 +1,16 @@
-from __future__ import with_statement
+from __future__ import with_statement, unicode_literals
import os
import re
-from BeautifulSoup import BeautifulSoup
+try:
+ from bs4 import BeautifulSoup
+except ImportError:
+ from BeautifulSoup import BeautifulSoup
+from django.utils import six
from django.core.cache.backends import locmem
-from django.test import TestCase
+from django.test import SimpleTestCase
+from django.test.utils import override_settings
from compressor.base import SOURCE_HUNK, SOURCE_FILE
from compressor.conf import settings
@@ -14,15 +19,24 @@ from compressor.js import JsCompressor
from compressor.exceptions import FilterDoesNotExist
+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)
+
+
def css_tag(href, **kwargs):
rendered_attrs = ''.join(['%s="%s" ' % (k, v) for k, v in kwargs.items()])
- template = u'<link rel="stylesheet" href="%s" type="text/css" %s/>'
+ template = '<link rel="stylesheet" href="%s" type="text/css" %s/>'
return template % (href, rendered_attrs)
class TestPrecompiler(object):
"""A filter whose output is always the string 'OUTPUT' """
- def __init__(self, content, attrs, filter_type=None, filename=None):
+ def __init__(self, content, attrs, filter_type=None, filename=None,
+ charset=None):
pass
def input(self, **kwargs):
@@ -32,11 +46,11 @@ class TestPrecompiler(object):
test_dir = os.path.abspath(os.path.join(os.path.dirname(__file__)))
-class CompressorTestCase(TestCase):
+class CompressorTestCase(SimpleTestCase):
def setUp(self):
settings.COMPRESS_ENABLED = True
- settings.COMPRESS_PRECOMPILERS = {}
+ settings.COMPRESS_PRECOMPILERS = ()
settings.COMPRESS_DEBUG_TOGGLE = 'nocompress'
self.css = """\
<link rel="stylesheet" href="/static/css/one.css" type="text/css" />
@@ -49,22 +63,52 @@ class CompressorTestCase(TestCase):
<script type="text/javascript">obj.value = "value";</script>"""
self.js_node = JsCompressor(self.js)
+ def assertEqualCollapsed(self, a, b):
+ """
+ assertEqual with internal newlines collapsed to single, and
+ trailing whitespace removed.
+ """
+ collapse = lambda x: re.sub(r'\n+', '\n', x).rstrip()
+ self.assertEqual(collapse(a), collapse(b))
+
+ def assertEqualSplits(self, a, b):
+ """
+ assertEqual for splits, particularly ignoring the presence of
+ a trailing newline on the content.
+ """
+ mangle = lambda split: [(x[0], x[1], x[2], x[3].rstrip()) for x in split]
+ self.assertEqual(mangle(a), mangle(b))
+
def test_css_split(self):
out = [
- (SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css', u'one.css'), u'css/one.css', u'<link rel="stylesheet" href="/static/css/one.css" type="text/css" />'),
- (SOURCE_HUNK, u'p { border:5px solid green;}', None, u'<style type="text/css">p { border:5px solid green;}</style>'),
- (SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css', u'two.css'), u'css/two.css', u'<link rel="stylesheet" href="/static/css/two.css" type="text/css" />'),
+ (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'css', 'one.css'),
+ 'css/one.css', '<link rel="stylesheet" href="/static/css/one.css" type="text/css" />',
+ ),
+ (
+ SOURCE_HUNK,
+ 'p { border:5px solid green;}',
+ None,
+ '<style type="text/css">p { border:5px solid green;}</style>',
+ ),
+ (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'css', 'two.css'),
+ 'css/two.css',
+ '<link rel="stylesheet" href="/static/css/two.css" type="text/css" />',
+ ),
]
split = self.css_node.split_contents()
split = [(x[0], x[1], x[2], self.css_node.parser.elem_str(x[3])) for x in split]
- self.assertEqual(out, split)
+ self.assertEqualSplits(split, out)
def test_css_hunks(self):
- out = ['body { background:#990; }', u'p { border:5px solid green;}', 'body { color:#fff; }']
+ out = ['body { background:#990; }', 'p { border:5px solid green;}', 'body { color:#fff; }']
self.assertEqual(out, list(self.css_node.hunks()))
def test_css_output(self):
- out = u'body { background:#990; }\np { border:5px solid green;}\nbody { color:#fff; }'
+ out = 'body { background:#990; }\np { border:5px solid green;}\nbody { color:#fff; }'
hunks = '\n'.join([h for h in self.css_node.hunks()])
self.assertEqual(out, hunks)
@@ -76,7 +120,7 @@ class CompressorTestCase(TestCase):
def test_css_return_if_off(self):
settings.COMPRESS_ENABLED = False
- self.assertEqual(self.css, self.css_node.output())
+ self.assertEqualCollapsed(self.css, self.css_node.output())
def test_cachekey(self):
is_cachekey = re.compile(r'\w{12}')
@@ -89,90 +133,83 @@ class CompressorTestCase(TestCase):
def test_js_split(self):
out = [
- (SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'js', u'one.js'), u'js/one.js', '<script src="/static/js/one.js" type="text/javascript"></script>'),
- (SOURCE_HUNK, u'obj.value = "value";', None, '<script type="text/javascript">obj.value = "value";</script>'),
+ (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'js', 'one.js'),
+ 'js/one.js',
+ '<script src="/static/js/one.js" type="text/javascript"></script>',
+ ),
+ (
+ SOURCE_HUNK,
+ 'obj.value = "value";',
+ None,
+ '<script type="text/javascript">obj.value = "value";</script>',
+ ),
]
split = self.js_node.split_contents()
split = [(x[0], x[1], x[2], self.js_node.parser.elem_str(x[3])) for x in split]
- self.assertEqual(out, split)
+ self.assertEqualSplits(split, out)
def test_js_hunks(self):
- out = ['obj = {};', u'obj.value = "value";']
+ out = ['obj = {};', 'obj.value = "value";']
self.assertEqual(out, list(self.js_node.hunks()))
def test_js_output(self):
- out = u'<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
+ out = '<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
self.assertEqual(out, self.js_node.output())
def test_js_override_url(self):
- self.js_node.context.update({'url': u'This is not a url, just a text'})
- out = u'<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
+ self.js_node.context.update({'url': 'This is not a url, just a text'})
+ out = '<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
self.assertEqual(out, self.js_node.output())
def test_css_override_url(self):
- self.css_node.context.update({'url': u'This is not a url, just a text'})
+ self.css_node.context.update({'url': 'This is not a url, just a text'})
output = css_tag('/static/CACHE/css/e41ba2cc6982.css')
self.assertEqual(output, self.css_node.output().strip())
+ @override_settings(COMPRESS_PRECOMPILERS=(), COMPRESS_ENABLED=False)
def test_js_return_if_off(self):
- try:
- enabled = settings.COMPRESS_ENABLED
- precompilers = settings.COMPRESS_PRECOMPILERS
- settings.COMPRESS_ENABLED = False
- settings.COMPRESS_PRECOMPILERS = {}
- self.assertEqual(self.js, self.js_node.output())
- finally:
- settings.COMPRESS_ENABLED = enabled
- settings.COMPRESS_PRECOMPILERS = precompilers
+ self.assertEqualCollapsed(self.js, self.js_node.output())
def test_js_return_if_on(self):
- output = u'<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
+ output = '<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
self.assertEqual(output, self.js_node.output())
- def test_custom_output_dir(self):
- try:
- old_output_dir = settings.COMPRESS_OUTPUT_DIR
- settings.COMPRESS_OUTPUT_DIR = 'custom'
- output = u'<script type="text/javascript" src="/static/custom/js/066cd253eada.js"></script>'
- self.assertEqual(output, JsCompressor(self.js).output())
- settings.COMPRESS_OUTPUT_DIR = ''
- output = u'<script type="text/javascript" src="/static/js/066cd253eada.js"></script>'
- self.assertEqual(output, JsCompressor(self.js).output())
- settings.COMPRESS_OUTPUT_DIR = '/custom/nested/'
- output = u'<script type="text/javascript" src="/static/custom/nested/js/066cd253eada.js"></script>'
- self.assertEqual(output, JsCompressor(self.js).output())
- finally:
- settings.COMPRESS_OUTPUT_DIR = old_output_dir
+ @override_settings(COMPRESS_OUTPUT_DIR='custom')
+ def test_custom_output_dir1(self):
+ output = '<script type="text/javascript" src="/static/custom/js/066cd253eada.js"></script>'
+ self.assertEqual(output, JsCompressor(self.js).output())
+
+ @override_settings(COMPRESS_OUTPUT_DIR='')
+ def test_custom_output_dir2(self):
+ output = '<script type="text/javascript" src="/static/js/066cd253eada.js"></script>'
+ self.assertEqual(output, JsCompressor(self.js).output())
+
+ @override_settings(COMPRESS_OUTPUT_DIR='/custom/nested/')
+ def test_custom_output_dir3(self):
+ output = '<script type="text/javascript" src="/static/custom/nested/js/066cd253eada.js"></script>'
+ self.assertEqual(output, JsCompressor(self.js).output())
+ @override_settings(COMPRESS_PRECOMPILERS=(
+ ('text/foobar', 'compressor.tests.test_base.TestPrecompiler'),
+ ), COMPRESS_ENABLED=True)
def test_precompiler_class_used(self):
- try:
- original_precompilers = settings.COMPRESS_PRECOMPILERS
- settings.COMPRESS_ENABLED = True
- settings.COMPRESS_PRECOMPILERS = (
- ('text/foobar', 'compressor.tests.test_base.TestPrecompiler'),
- )
- css = '<style type="text/foobar">p { border:10px solid red;}</style>'
- css_node = CssCompressor(css)
- output = BeautifulSoup(css_node.output('inline'))
- self.assertEqual(output.text, 'OUTPUT')
- finally:
- settings.COMPRESS_PRECOMPILERS = original_precompilers
+ css = '<style type="text/foobar">p { border:10px solid red;}</style>'
+ css_node = CssCompressor(css)
+ output = make_soup(css_node.output('inline'))
+ self.assertEqual(output.text, 'OUTPUT')
+ @override_settings(COMPRESS_PRECOMPILERS=(
+ ('text/foobar', 'compressor.tests.test_base.NonexistentFilter'),
+ ), COMPRESS_ENABLED=True)
def test_nonexistent_precompiler_class_error(self):
- try:
- original_precompilers = settings.COMPRESS_PRECOMPILERS
- settings.COMPRESS_ENABLED = True
- settings.COMPRESS_PRECOMPILERS = (
- ('text/foobar', 'compressor.tests.test_base.NonexistentFilter'),
- )
- css = '<style type="text/foobar">p { border:10px solid red;}</style>'
- css_node = CssCompressor(css)
- self.assertRaises(FilterDoesNotExist, css_node.output, 'inline')
- finally:
- settings.COMPRESS_PRECOMPILERS = original_precompilers
-
-
-class CssMediaTestCase(TestCase):
+ css = '<style type="text/foobar">p { border:10px solid red;}</style>'
+ css_node = CssCompressor(css)
+ self.assertRaises(FilterDoesNotExist, css_node.output, 'inline')
+
+
+class CssMediaTestCase(SimpleTestCase):
def setUp(self):
self.css = """\
<link rel="stylesheet" href="/static/css/one.css" type="text/css" media="screen">
@@ -182,35 +219,41 @@ class CssMediaTestCase(TestCase):
def test_css_output(self):
css_node = CssCompressor(self.css)
- links = BeautifulSoup(css_node.output()).findAll('link')
- media = [u'screen', u'print', u'all', None]
+ if six.PY3:
+ links = make_soup(css_node.output()).find_all('link')
+ else:
+ links = make_soup(css_node.output()).findAll('link')
+ media = ['screen', 'print', 'all', None]
self.assertEqual(len(links), 4)
self.assertEqual(media, [l.get('media', None) for l in links])
def test_avoid_reordering_css(self):
css = self.css + '<style type="text/css" media="print">p { border:10px solid red;}</style>'
css_node = CssCompressor(css)
- media = [u'screen', u'print', u'all', None, u'print']
- links = BeautifulSoup(css_node.output()).findAll('link')
+ 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')
self.assertEqual(media, [l.get('media', None) for l in links])
+ @override_settings(COMPRESS_PRECOMPILERS=(
+ ('text/foobar', 'python %s {infile} {outfile}' % os.path.join(test_dir, 'precompiler.py')),
+ ), COMPRESS_ENABLED=False)
def test_passthough_when_compress_disabled(self):
- original_precompilers = settings.COMPRESS_PRECOMPILERS
- settings.COMPRESS_ENABLED = False
- settings.COMPRESS_PRECOMPILERS = (
- ('text/foobar', 'python %s {infile} {outfile}' % os.path.join(test_dir, 'precompiler.py')),
- )
css = """\
<link rel="stylesheet" href="/static/css/one.css" type="text/css" media="screen">
<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)
- output = BeautifulSoup(css_node.output()).findAll(['link', 'style'])
- self.assertEqual([u'/static/css/one.css', u'/static/css/two.css', None],
+ if six.PY3:
+ output = make_soup(css_node.output()).find_all(['link', 'style'])
+ else:
+ output = make_soup(css_node.output()).findAll(['link', 'style'])
+ self.assertEqual(['/static/css/one.css', '/static/css/two.css', None],
[l.get('href', None) for l in output])
- self.assertEqual([u'screen', u'screen', u'screen'],
+ self.assertEqual(['screen', 'screen', 'screen'],
[l.get('media', None) for l in output])
- settings.COMPRESS_PRECOMPILERS = original_precompilers
class VerboseTestCase(CompressorTestCase):
diff --git a/compressor/tests/test_filters.py b/compressor/tests/test_filters.py
index 90c4036..b656a65 100644
--- a/compressor/tests/test_filters.py
+++ b/compressor/tests/test_filters.py
@@ -1,9 +1,13 @@
-from __future__ import with_statement
+from __future__ import with_statement, unicode_literals
+import io
import os
import sys
-from unittest2 import skipIf
+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.conf import settings
@@ -16,56 +20,66 @@ from compressor.filters.template import TemplateFilter
from compressor.tests.test_base import test_dir
+@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 = """
-/* Some comment */
-font,th,td,p{
-color: black;
-}
-"""
+ content = textwrap.dedent("""\
+ /* Some comment */
+ font,th,td,p{
+ color: black;
+ }
+ """)
from compressor.filters.csstidy import CSSTidyFilter
+ ret = CSSTidyFilter(content).input()
+ self.assertIsInstance(ret, six.text_type)
self.assertEqual(
"font,th,td,p{color:#000;}", CSSTidyFilter(content).input())
-CssTidyTestCase = skipIf(
- find_command(settings.COMPRESS_CSSTIDY_BINARY) is None,
- 'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY,
-)(CssTidyTestCase)
-
class PrecompilerTestCase(TestCase):
-
def setUp(self):
self.filename = os.path.join(test_dir, 'static/css/one.css')
- with open(self.filename) as f:
- self.content = f.read()
+ with io.open(self.filename, encoding=settings.FILE_CHARSET) as file:
+ self.content = file.read()
self.test_precompiler = os.path.join(test_dir, 'precompiler.py')
def test_precompiler_infile_outfile(self):
command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
- compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
- self.assertEqual(u"body { color:#990; }", compiler.input())
+ compiler = CompilerFilter(
+ content=self.content, filename=self.filename,
+ charset=settings.FILE_CHARSET, command=command)
+ self.assertEqual("body { color:#990; }", compiler.input())
def test_precompiler_infile_stdout(self):
command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler)
- compiler = CompilerFilter(content=self.content, filename=None, command=command)
- self.assertEqual(u"body { color:#990; }%s" % os.linesep, compiler.input())
+ compiler = CompilerFilter(
+ content=self.content, filename=None, charset=None, command=command)
+ self.assertEqual("body { color:#990; }%s" % os.linesep, compiler.input())
def test_precompiler_stdin_outfile(self):
command = '%s %s -o {outfile}' % (sys.executable, self.test_precompiler)
- compiler = CompilerFilter(content=self.content, filename=None, command=command)
- self.assertEqual(u"body { color:#990; }", compiler.input())
+ compiler = CompilerFilter(
+ content=self.content, filename=None, charset=None, command=command)
+ self.assertEqual("body { color:#990; }", compiler.input())
def test_precompiler_stdin_stdout(self):
command = '%s %s' % (sys.executable, self.test_precompiler)
- compiler = CompilerFilter(content=self.content, filename=None, command=command)
- self.assertEqual(u"body { color:#990; }%s" % os.linesep, compiler.input())
+ compiler = CompilerFilter(
+ content=self.content, filename=None, charset=None, command=command)
+ self.assertEqual("body { color:#990; }%s" % os.linesep, compiler.input())
def test_precompiler_stdin_stdout_filename(self):
command = '%s %s' % (sys.executable, self.test_precompiler)
+ compiler = CompilerFilter(
+ content=self.content, filename=self.filename,
+ charset=settings.FILE_CHARSET, command=command)
+ self.assertEqual("body { color:#990; }%s" % os.linesep, compiler.input())
+
+ def test_precompiler_output_unicode(self):
+ command = '%s %s' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
- self.assertEqual(u"body { color:#990; }%s" % os.linesep, compiler.input())
+ self.assertEqual(type(compiler.input()), six.text_type)
class CssMinTestCase(TestCase):
@@ -77,7 +91,7 @@ class CssMinTestCase(TestCase):
}
-"""
+ """
output = "p{background:#369 url('../../images/image.gif')}"
self.assertEqual(output, CSSMinFilter(content).output())
@@ -210,14 +224,14 @@ class CssAbsolutizingTestCase(TestCase):
'hash1': self.hashing_func(os.path.join(settings.COMPRESS_ROOT, 'img/python.png')),
'hash2': self.hashing_func(os.path.join(settings.COMPRESS_ROOT, 'img/add.png')),
}
- self.assertEqual([u"""\
+ self.assertEqual(["""\
p { background: url('/static/img/python.png?%(hash1)s'); }
p { background: url('/static/img/python.png?%(hash1)s'); }
p { background: url('/static/img/python.png?%(hash1)s'); }
p { background: url('/static/img/python.png?%(hash1)s'); }
p { filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/static/img/python.png?%(hash1)s'); }
""" % hash_dict,
- u"""\
+ """\
p { background: url('/static/img/add.png?%(hash2)s'); }
p { background: url('/static/img/add.png?%(hash2)s'); }
p { background: url('/static/img/add.png?%(hash2)s'); }
@@ -264,7 +278,7 @@ class CssDataUriTestCase(TestCase):
def test_data_uris(self):
datauri_hash = get_hashed_mtime(os.path.join(settings.COMPRESS_ROOT, 'img/python.png'))
- out = [u'''.add { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); }
+ out = ['''.add { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); }
.add-with-hash { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); }
.python { background-image: url("/static/img/python.png?%s"); }
.datauri { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0 vr4MkhoXe0rZigAAAABJRU5ErkJggg=="); }
@@ -273,17 +287,11 @@ class CssDataUriTestCase(TestCase):
class TemplateTestCase(TestCase):
- def setUp(self):
- self.old_context = settings.COMPRESS_TEMPLATE_FILTER_CONTEXT
-
- def tearDown(self):
- settings.COMPRESS_TEMPLATE_FILTER_CONTEXT = self.old_context
-
+ @override_settings(COMPRESS_TEMPLATE_FILTER_CONTEXT={
+ 'stuff': 'thing',
+ 'gimmick': 'bold'
+ })
def test_template_filter(self):
- settings.COMPRESS_TEMPLATE_FILTER_CONTEXT = {
- 'stuff': 'thing',
- 'gimmick': 'bold'
- }
content = """
#content {background-image: url("{{ STATIC_URL|default:stuff }}/images/bg.png");}
#footer {font-weight: {{ gimmick }};}
diff --git a/compressor/tests/test_jinja2ext.py b/compressor/tests/test_jinja2ext.py
index cb2e012..5adc8ee 100644
--- a/compressor/tests/test_jinja2ext.py
+++ b/compressor/tests/test_jinja2ext.py
@@ -1,14 +1,18 @@
# -*- coding: utf-8 -*-
-from __future__ import with_statement
+from __future__ import with_statement, unicode_literals
-from django.test import TestCase
+import sys
-import jinja2
+from django.test import TestCase
+from django.utils import unittest, 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),
+ 'Jinja can only run on Python < 3 and >= 3.3')
class TestJinja2CompressorExtension(TestCase):
"""
Test case for jinja2 extension.
@@ -18,30 +22,34 @@ class TestJinja2CompressorExtension(TestCase):
that we use jinja2 specific controls (*minus* character at block's
beginning or end). For more information see jinja2 documentation.
"""
-
def assertStrippedEqual(self, result, expected):
self.assertEqual(result.strip(), expected.strip(), "%r != %r" % (
result.strip(), expected.strip()))
def setUp(self):
+ import jinja2
+ self.jinja2 = jinja2
from compressor.contrib.jinja2ext import CompressorExtension
- self.env = jinja2.Environment(extensions=[CompressorExtension])
+ self.env = self.jinja2.Environment(extensions=[CompressorExtension])
def test_error_raised_if_no_arguments_given(self):
- self.assertRaises(jinja2.exceptions.TemplateSyntaxError,
+ self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError,
self.env.from_string, '{% compress %}Foobar{% endcompress %}')
def test_error_raised_if_wrong_kind_given(self):
- self.assertRaises(jinja2.exceptions.TemplateSyntaxError,
+ self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError,
self.env.from_string, '{% compress foo %}Foobar{% endcompress %}')
+ def test_error_raised_if_wrong_closing_kind_given(self):
+ self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError,
+ self.env.from_string, '{% compress js %}Foobar{% endcompress css %}')
+
def test_error_raised_if_wrong_mode_given(self):
- self.assertRaises(jinja2.exceptions.TemplateSyntaxError,
+ self.assertRaises(self.jinja2.exceptions.TemplateSyntaxError,
self.env.from_string, '{% compress css foo %}Foobar{% endcompress %}')
+ @override_settings(COMPRESS_ENABLED=False)
def test_compress_is_disabled(self):
- org_COMPRESS_ENABLED = settings.COMPRESS_ENABLED
- settings.COMPRESS_ENABLED = False
tag_body = '\n'.join([
'<link rel="stylesheet" href="css/one.css" type="text/css" charset="utf-8">',
'<style type="text/css">p { border:5px solid green;}</style>',
@@ -50,16 +58,26 @@ class TestJinja2CompressorExtension(TestCase):
template_string = '{% compress css %}' + tag_body + '{% endcompress %}'
template = self.env.from_string(template_string)
self.assertEqual(tag_body, template.render())
- settings.COMPRESS_ENABLED = org_COMPRESS_ENABLED
+
+ # Test with explicit kind
+ template_string = '{% compress css %}' + tag_body + '{% endcompress css %}'
+ template = self.env.from_string(template_string)
+ self.assertEqual(tag_body, template.render())
def test_empty_tag(self):
- template = self.env.from_string(u"""{% compress js %}{% block js %}
+ template = self.env.from_string("""{% compress js %}{% block js %}
{% endblock %}{% endcompress %}""")
context = {'STATIC_URL': settings.COMPRESS_URL}
- self.assertEqual(u'', template.render(context))
+ self.assertEqual('', template.render(context))
+
+ def test_empty_tag_with_kind(self):
+ template = self.env.from_string("""{% compress js %}{% block js %}
+ {% endblock %}{% endcompress js %}""")
+ context = {'STATIC_URL': settings.COMPRESS_URL}
+ self.assertEqual('', template.render(context))
def test_css_tag(self):
- template = self.env.from_string(u"""{% compress css -%}
+ template = self.env.from_string("""{% compress css -%}
<link rel="stylesheet" href="{{ STATIC_URL }}css/one.css" type="text/css" charset="utf-8">
<style type="text/css">p { border:5px solid green;}</style>
<link rel="stylesheet" href="{{ STATIC_URL }}css/two.css" type="text/css" charset="utf-8">
@@ -69,7 +87,7 @@ class TestJinja2CompressorExtension(TestCase):
self.assertEqual(out, template.render(context))
def test_nonascii_css_tag(self):
- template = self.env.from_string(u"""{% compress css -%}
+ template = self.env.from_string("""{% compress css -%}
<link rel="stylesheet" href="{{ STATIC_URL }}css/nonasc.css" type="text/css" charset="utf-8">
<style type="text/css">p { border:5px solid green;}</style>
{% endcompress %}""")
@@ -78,34 +96,34 @@ class TestJinja2CompressorExtension(TestCase):
self.assertEqual(out, template.render(context))
def test_js_tag(self):
- template = self.env.from_string(u"""{% compress js -%}
+ template = self.env.from_string("""{% compress js -%}
<script src="{{ STATIC_URL }}js/one.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">obj.value = "value";</script>
{% endcompress %}""")
context = {'STATIC_URL': settings.COMPRESS_URL}
- out = u'<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
+ out = '<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
self.assertEqual(out, template.render(context))
def test_nonascii_js_tag(self):
- template = self.env.from_string(u"""{% compress js -%}
+ template = self.env.from_string("""{% compress js -%}
<script src="{{ STATIC_URL }}js/nonasc.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">var test_value = "\u2014";</script>
{% endcompress %}""")
context = {'STATIC_URL': settings.COMPRESS_URL}
- out = u'<script type="text/javascript" src="/static/CACHE/js/e214fe629b28.js"></script>'
+ out = '<script type="text/javascript" src="/static/CACHE/js/e214fe629b28.js"></script>'
self.assertEqual(out, template.render(context))
def test_nonascii_latin1_js_tag(self):
- template = self.env.from_string(u"""{% compress js -%}
+ template = self.env.from_string("""{% compress js -%}
<script src="{{ STATIC_URL }}js/nonasc-latin1.js" type="text/javascript" charset="latin-1"></script>
<script type="text/javascript">var test_value = "\u2014";</script>
{% endcompress %}""")
context = {'STATIC_URL': settings.COMPRESS_URL}
- out = u'<script type="text/javascript" src="/static/CACHE/js/be9e078b5ca7.js"></script>'
+ out = '<script type="text/javascript" src="/static/CACHE/js/be9e078b5ca7.js"></script>'
self.assertEqual(out, template.render(context))
def test_css_inline(self):
- template = self.env.from_string(u"""{% compress css, inline -%}
+ template = self.env.from_string("""{% compress css, inline -%}
<link rel="stylesheet" href="{{ STATIC_URL }}css/one.css" type="text/css" charset="utf-8">
<style type="text/css">p { border:5px solid green;}</style>
{% endcompress %}""")
@@ -117,7 +135,7 @@ class TestJinja2CompressorExtension(TestCase):
self.assertEqual(out, template.render(context))
def test_js_inline(self):
- template = self.env.from_string(u"""{% compress js, inline -%}
+ template = self.env.from_string("""{% compress js, inline -%}
<script src="{{ STATIC_URL }}js/one.js" type="text/css" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">obj.value = "value";</script>
{% endcompress %}""")
@@ -128,11 +146,11 @@ class TestJinja2CompressorExtension(TestCase):
def test_nonascii_inline_css(self):
org_COMPRESS_ENABLED = settings.COMPRESS_ENABLED
settings.COMPRESS_ENABLED = False
- template = self.env.from_string(u'{% compress css %}'
- u'<style type="text/css">'
- u'/* русский текст */'
- u'</style>{% endcompress %}')
- out = u'<link rel="stylesheet" href="/static/CACHE/css/b2cec0f8cb24.css" type="text/css" />'
+ 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/compressor/tests/test_offline.py b/compressor/tests/test_offline.py
index a988afd..327b901 100644
--- a/compressor/tests/test_offline.py
+++ b/compressor/tests/test_offline.py
@@ -1,12 +1,12 @@
-from __future__ import with_statement
+from __future__ import with_statement, unicode_literals
+import io
import os
-from StringIO import StringIO
-from unittest2 import skipIf
+import sys
-import django
+from django.core.management.base import CommandError
from django.template import Template, Context
from django.test import TestCase
-from django.core.management.base import CommandError
+from django.utils import six, unittest
from compressor.cache import flush_offline_manifest, get_offline_manifest
from compressor.conf import settings
@@ -14,6 +14,22 @@ from compressor.exceptions import OfflineGenerationError
from compressor.management.commands.compress import Command as CompressCommand
from compressor.storage import default_storage
+if six.PY3:
+ # there is an 'io' module in python 2.6+, but io.StringIO does not
+ # accept regular strings, just unicode objects
+ from io import StringIO
+else:
+ try:
+ from cStringIO import StringIO
+ except ImportError:
+ from StringIO import StringIO
+
+# 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'"
+_TEST_JINJA2 = not(sys.version_info[0] == 3 and sys.version_info[1] == 2)
+
class OfflineTestCaseMixin(object):
template_name = "test_compressor_offline.html"
@@ -21,6 +37,11 @@ class OfflineTestCaseMixin(object):
# Change this for each test class
templates_dir = ""
expected_hash = ""
+ # Engines to test
+ if _TEST_JINJA2:
+ engines = ("django", "jinja2")
+ else:
+ engines = ("django",)
def setUp(self):
self._old_compress = settings.COMPRESS_ENABLED
@@ -32,48 +53,128 @@ class OfflineTestCaseMixin(object):
# Reset template dirs, because it enables us to force compress to
# consider only a specific directory (helps us make true,
# independant unit tests).
- settings.TEMPLATE_DIRS = (
- os.path.join(settings.TEST_DIR, 'test_templates', self.templates_dir),
- )
+ # 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)
+ settings.TEMPLATE_DIRS = (django_template_dir, jinja2_template_dir)
+
# Enable offline compress
settings.COMPRESS_ENABLED = True
settings.COMPRESS_OFFLINE = True
- self.template_path = os.path.join(settings.TEMPLATE_DIRS[0], self.template_name)
- self.template_file = open(self.template_path)
- self.template = Template(self.template_file.read().decode(settings.FILE_CHARSET))
+
+ 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())
+
+ self._old_jinja2_get_environment = settings.COMPRESS_JINJA2_GET_ENVIRONMENT
+
+ if "jinja2" in self.engines:
+ # Setup Jinja2 settings.
+ settings.COMPRESS_JINJA2_GET_ENVIRONMENT = lambda: self._get_jinja2_env()
+ jinja2_env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT()
+ self.template_path_jinja2 = os.path.join(jinja2_template_dir, self.template_name)
+
+ with io.open(self.template_path_jinja2, encoding=settings.FILE_CHARSET) as file:
+ self.template_jinja2 = jinja2_env.from_string(file.read())
def tearDown(self):
+ settings.COMPRESS_JINJA2_GET_ENVIRONMENT = self._old_jinja2_get_environment
settings.COMPRESS_ENABLED = self._old_compress
settings.COMPRESS_OFFLINE = self._old_compress_offline
settings.TEMPLATE_DIRS = self._old_template_dirs
- self.template_file.close()
manifest_path = os.path.join('CACHE', 'manifest.json')
if default_storage.exists(manifest_path):
default_storage.delete(manifest_path)
- def test_offline(self):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity)
+ 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
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
self.assertEqual(1, count)
self.assertEqual([
- u'<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (self.expected_hash, ),
+ '<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (self.expected_hash, ),
], result)
- rendered_template = self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
+ rendered_template = self._render_template(engine)
self.assertEqual(rendered_template, "".join(result) + "\n")
+ def test_offline(self):
+ for engine in self.engines:
+ self._test_offline(engine=engine)
+
+ def _get_jinja2_env(self):
+ import jinja2
+ import jinja2.ext
+ from compressor.offline.jinja2 import url_for, SpacelessExtension
+ from compressor.contrib.jinja2ext import CompressorExtension
+
+ # Extensions needed for the test cases only.
+ extensions = [
+ CompressorExtension,
+ SpacelessExtension,
+ jinja2.ext.with_,
+ jinja2.ext.do,
+ ]
+ loader = self._get_jinja2_loader()
+ env = jinja2.Environment(extensions=extensions, loader=loader)
+ env.globals['url_for'] = url_for
+
+ return env
+
+ def _get_jinja2_loader(self):
+ import jinja2
+
+ loader = jinja2.FileSystemLoader(settings.TEMPLATE_DIRS, encoding=settings.FILE_CHARSET)
+ return loader
+
+
+class OfflineGenerationSkipDuplicatesTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_duplicate"
+
+ # We don't need to test multiples engines here.
+ engines = ("django",)
+
+ def _test_offline(self, 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>',
+ ], result)
+ rendered_template = self._render_template(engine)
+ # But rendering the template returns both (identical) scripts.
+ self.assertEqual(rendered_template, "".join(result * 2) + "\n")
+
class OfflineGenerationBlockSuperTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_block_super"
expected_hash = "7c02d201f69d"
+ # Block.super not supported for Jinja2 yet.
+ engines = ("django",)
class OfflineGenerationBlockSuperMultipleTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_block_super_multiple"
- expected_hash = "2f6ef61c488e"
+ expected_hash = "f8891c416981"
+ # Block.super not supported for Jinja2 yet.
+ engines = ("django",)
class OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase(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
@@ -92,15 +193,17 @@ class OfflineGenerationBlockSuperMultipleWithCachedLoaderTestCase(OfflineTestCas
class OfflineGenerationBlockSuperTestCaseWithExtraContent(OfflineTestCaseMixin, TestCase):
templates_dir = "test_block_super_extra"
+ # Block.super not supported for Jinja2 yet.
+ engines = ("django",)
- def test_offline(self):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity)
+ def _test_offline(self, engine):
+ count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
self.assertEqual(2, count)
self.assertEqual([
- u'<script type="text/javascript" src="/static/CACHE/js/ced14aec5856.js"></script>',
- u'<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.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
+ rendered_template = self._render_template(engine)
self.assertEqual(rendered_template, "".join(result) + "\n")
@@ -128,10 +231,6 @@ class OfflineGenerationTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
class OfflineGenerationStaticTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = "test_static_templatetag"
expected_hash = "dfa2bb387fa8"
-# This test uses {% static %} which was introduced in django 1.4
-OfflineGenerationStaticTemplateTagTestCase = skipIf(
- django.VERSION[1] < 4, 'Django 1.4 not found'
-)(OfflineGenerationStaticTemplateTagTestCase)
class OfflineGenerationTestCaseWithContext(OfflineTestCaseMixin, TestCase):
@@ -153,11 +252,23 @@ class OfflineGenerationTestCaseWithContext(OfflineTestCaseMixin, TestCase):
class OfflineGenerationTestCaseErrors(OfflineTestCaseMixin, TestCase):
templates_dir = "test_error_handling"
- def test_offline(self):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity)
- self.assertEqual(2, count)
- self.assertIn(u'<script type="text/javascript" src="/static/CACHE/js/3872c9ae3f42.js"></script>', result)
- self.assertIn(u'<script type="text/javascript" src="/static/CACHE/js/cd8870829421.js"></script>', result)
+ def _test_offline(self, engine):
+ count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity, engine=engine)
+
+ 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.
+ 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):
@@ -168,7 +279,7 @@ class OfflineGenerationTestCaseWithError(OfflineTestCaseMixin, TestCase):
settings.COMPRESS_PRECOMPILERS = (('text/coffeescript', 'non-existing-binary'),)
super(OfflineGenerationTestCaseWithError, self).setUp()
- def test_offline(self):
+ def _test_offline(self, engine):
"""
Test that a CommandError is raised with DEBUG being False as well as
True, as otherwise errors in configuration will never show in
@@ -178,10 +289,10 @@ class OfflineGenerationTestCaseWithError(OfflineTestCaseMixin, TestCase):
try:
settings.DEBUG = True
- self.assertRaises(CommandError, CompressCommand().compress)
+ self.assertRaises(CommandError, CompressCommand().compress, engine=engine)
settings.DEBUG = False
- self.assertRaises(CommandError, CompressCommand().compress)
+ self.assertRaises(CommandError, CompressCommand().compress, engine=engine)
finally:
settings.DEBUG = self._old_debug
@@ -201,19 +312,30 @@ class OfflineGenerationTestCase(OfflineTestCaseMixin, TestCase):
self.assertRaises(OfflineGenerationError,
self.template.render, Context({}))
- def test_deleting_manifest_does_not_affect_rendering(self):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity)
+ @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([
- u'<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (self.expected_hash, ),
+ '<script type="text/javascript" src="/static/CACHE/js/%s.js"></script>' % (self.expected_hash, ),
], result)
- rendered_template = self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
+ 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_requires_model_validation(self):
self.assertFalse(CompressCommand.requires_model_validation)
@@ -238,13 +360,50 @@ class OfflineGenerationTestCase(OfflineTestCaseMixin, TestCase):
settings.TEMPLATE_LOADERS = old_loaders
+class OfflineGenerationBlockSuperBaseCompressed(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",)
+
+ def setUp(self):
+ super(OfflineGenerationBlockSuperBaseCompressed, 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)
+ self.template_paths.append(template_path)
+ 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":
+ return template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
+ 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)
+ 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)
+ rendered_template = self._render_template(template, engine)
+ self.assertEqual(rendered_template, expected_output + '\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 = {
- 'test_non_ascii_value': u'\u2014',
+ 'test_non_ascii_value': '\u2014',
}
super(OfflineGenerationInlineNonAsciiTestCase, self).setUp()
@@ -252,7 +411,89 @@ class OfflineGenerationInlineNonAsciiTestCase(OfflineTestCaseMixin, TestCase):
self.COMPRESS_OFFLINE_CONTEXT = self.old_offline_context
super(OfflineGenerationInlineNonAsciiTestCase, self).tearDown()
- def test_offline(self):
- count, result = CompressCommand().compress(log=self.log, verbosity=self.verbosity)
- rendered_template = self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT))
+ def _test_offline(self, 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")
+
+
+class OfflineGenerationComplexTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_complex"
+
+ def setUp(self):
+ self.old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
+ 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"),
+ }
+ 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)
+ 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>',
+ ], 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+")
+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")
+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
diff --git a/compressor/tests/test_parsers.py b/compressor/tests/test_parsers.py
index 04ec924..d9b4dd6 100644
--- a/compressor/tests/test_parsers.py
+++ b/compressor/tests/test_parsers.py
@@ -1,6 +1,5 @@
from __future__ import with_statement
import os
-from unittest2 import skipIf
try:
import lxml
@@ -17,15 +16,15 @@ try:
except ImportError:
BeautifulSoup = None
+from django.utils import unittest
+from django.test.utils import override_settings
from compressor.base import SOURCE_HUNK, SOURCE_FILE
from compressor.conf import settings
-from compressor.css import CssCompressor
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
@@ -35,52 +34,92 @@ class ParserTestCase(object):
settings.COMPRESS_PARSER = self.old_parser
+@unittest.skipIf(lxml is None, 'lxml not found')
class LxmlParserTests(ParserTestCase, CompressorTestCase):
parser_cls = 'compressor.parser.LxmlParser'
-LxmlParserTests = skipIf(lxml is None, 'lxml not found')(LxmlParserTests)
+@unittest.skipIf(html5lib is None, 'html5lib not found')
class Html5LibParserTests(ParserTestCase, CompressorTestCase):
parser_cls = 'compressor.parser.Html5LibParser'
-
- def setUp(self):
- super(Html5LibParserTests, self).setUp()
- # special version of the css since the parser sucks
- self.css = """\
-<link href="/static/css/one.css" rel="stylesheet" type="text/css">
-<style type="text/css">p { border:5px solid green;}</style>
-<link href="/static/css/two.css" rel="stylesheet" type="text/css">"""
- self.css_node = CssCompressor(self.css)
+ # Special test variants required since xml.etree holds attributes
+ # as a plain dictionary, e.g. key order is unpredictable.
def test_css_split(self):
- out = [
- (SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css', u'one.css'), u'css/one.css', u'<link href="/static/css/one.css" rel="stylesheet" type="text/css">'),
- (SOURCE_HUNK, u'p { border:5px solid green;}', None, u'<style type="text/css">p { border:5px solid green;}</style>'),
- (SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css', u'two.css'), u'css/two.css', u'<link href="/static/css/two.css" rel="stylesheet" type="text/css">'),
- ]
split = self.css_node.split_contents()
- split = [(x[0], x[1], x[2], self.css_node.parser.elem_str(x[3])) for x in split]
- self.assertEqual(out, split)
+ out0 = (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'css', 'one.css'),
+ 'css/one.css',
+ '{http://www.w3.org/1999/xhtml}link',
+ {'rel': 'stylesheet', 'href': '/static/css/one.css',
+ 'type': 'text/css'},
+ )
+ 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',
+ '{http://www.w3.org/1999/xhtml}link',
+ {'rel': 'stylesheet', 'href': '/static/css/two.css',
+ 'type': 'text/css'},
+ )
+ self.assertEqual(out2, split[2][:3] + (split[2][3].tag,
+ split[2][3].attrib))
def test_js_split(self):
- out = [
- (SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'js', u'one.js'), u'js/one.js', u'<script src="/static/js/one.js" type="text/javascript"></script>'),
- (SOURCE_HUNK, u'obj.value = "value";', None, u'<script type="text/javascript">obj.value = "value";</script>'),
- ]
split = self.js_node.split_contents()
- split = [(x[0], x[1], x[2], self.js_node.parser.elem_str(x[3])) for x in split]
- self.assertEqual(out, split)
-
-Html5LibParserTests = skipIf(
- html5lib is None, 'html5lib not found')(Html5LibParserTests)
-
-
+ out0 = (
+ SOURCE_FILE,
+ os.path.join(settings.COMPRESS_ROOT, 'js', 'one.js'),
+ 'js/one.js',
+ '{http://www.w3.org/1999/xhtml}script',
+ {'src': '/static/js/one.js', 'type': 'text/javascript'},
+ None,
+ )
+ self.assertEqual(out0, split[0][:3] + (split[0][3].tag,
+ split[0][3].attrib,
+ split[0][3].text))
+ out1 = (
+ SOURCE_HUNK,
+ 'obj.value = "value";',
+ None,
+ '{http://www.w3.org/1999/xhtml}script',
+ {'type': 'text/javascript'},
+ 'obj.value = "value";',
+ )
+ self.assertEqual(out1, split[1][:3] + (split[1][3].tag,
+ split[1][3].attrib,
+ split[1][3].text))
+
+ 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
+ # and then evaluating the result, which no longer is
+ # a meaningful unit test.
+ self.assertEqual(len(self.css), len(self.css_node.output()))
+
+ @override_settings(COMPRESS_PRECOMPILERS=(), COMPRESS_ENABLED=False)
+ def test_js_return_if_off(self):
+ # As above.
+ 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'
-BeautifulSoupParserTests = skipIf(
- BeautifulSoup is None, 'BeautifulSoup not found')(BeautifulSoupParserTests)
-
class HtmlParserTests(ParserTestCase, CompressorTestCase):
parser_cls = 'compressor.parser.HtmlParser'
diff --git a/compressor/tests/test_signals.py b/compressor/tests/test_signals.py
index b4ece9c..13d5eed 100644
--- a/compressor/tests/test_signals.py
+++ b/compressor/tests/test_signals.py
@@ -11,7 +11,7 @@ from compressor.signals import post_compress
class PostCompressSignalTestCase(TestCase):
def setUp(self):
settings.COMPRESS_ENABLED = True
- settings.COMPRESS_PRECOMPILERS = {}
+ settings.COMPRESS_PRECOMPILERS = ()
settings.COMPRESS_DEBUG_TOGGLE = 'nocompress'
self.css = """\
<link rel="stylesheet" href="/static/css/one.css" type="text/css" />
@@ -34,9 +34,9 @@ class PostCompressSignalTestCase(TestCase):
post_compress.connect(callback)
self.js_node.output()
args, kwargs = callback.call_args
- self.assertEquals(JsCompressor, kwargs['sender'])
- self.assertEquals('js', kwargs['type'])
- self.assertEquals('file', kwargs['mode'])
+ self.assertEqual(JsCompressor, kwargs['sender'])
+ self.assertEqual('js', kwargs['type'])
+ self.assertEqual('file', kwargs['mode'])
context = kwargs['context']
assert 'url' in context['compressed']
@@ -47,9 +47,9 @@ class PostCompressSignalTestCase(TestCase):
post_compress.connect(callback)
self.css_node.output()
args, kwargs = callback.call_args
- self.assertEquals(CssCompressor, kwargs['sender'])
- self.assertEquals('css', kwargs['type'])
- self.assertEquals('file', kwargs['mode'])
+ self.assertEqual(CssCompressor, kwargs['sender'])
+ self.assertEqual('css', kwargs['type'])
+ self.assertEqual('file', kwargs['mode'])
context = kwargs['context']
assert 'url' in context['compressed']
@@ -65,4 +65,4 @@ class PostCompressSignalTestCase(TestCase):
callback = Mock(wraps=listener)
post_compress.connect(callback)
css_node.output()
- self.assertEquals(3, callback.call_count)
+ self.assertEqual(3, callback.call_count)
diff --git a/compressor/tests/test_storages.py b/compressor/tests/test_storages.py
index 713002e..91a36f2 100644
--- a/compressor/tests/test_storages.py
+++ b/compressor/tests/test_storages.py
@@ -1,29 +1,41 @@
-from __future__ import with_statement
+from __future__ import with_statement, unicode_literals
import errno
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.utils.functional import LazyObject
-from compressor import base
+from compressor import storage
from compressor.conf import settings
from compressor.tests.test_base import css_tag
from compressor.tests.test_templatetags import render
+class GzipStorage(LazyObject):
+ def _setup(self):
+ self._wrapped = get_storage_class('compressor.storage.GzipCompressorFileStorage')()
+
+
class StorageTestCase(TestCase):
def setUp(self):
- self._storage = base.default_storage
- base.default_storage = get_storage_class(
- 'compressor.storage.GzipCompressorFileStorage')()
+ self.old_enabled = settings.COMPRESS_ENABLED
settings.COMPRESS_ENABLED = True
+ self.default_storage = storage.default_storage
+ storage.default_storage = GzipStorage()
def tearDown(self):
- base.default_storage = self._storage
+ 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'))
+ self.assertTrue(os.path.exists(os.path.join(settings.COMPRESS_ROOT, 'test.txt')))
+ self.assertTrue(os.path.exists(os.path.join(settings.COMPRESS_ROOT, 'test.txt.gz')))
def test_css_tag_with_storage(self):
- template = u"""{% load compress %}{% compress css %}
+ template = """{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ STATIC_URL }}css/one.css" type="text/css">
<style type="text/css">p { border:5px solid white;}</style>
<link rel="stylesheet" href="{{ STATIC_URL }}css/two.css" type="text/css">
@@ -40,13 +52,13 @@ class StorageTestCase(TestCase):
def race_remove(path):
"Patched os.remove to raise ENOENT (No such file or directory)"
original_remove(path)
- raise OSError(errno.ENOENT, u'Fake ENOENT')
+ raise OSError(errno.ENOENT, 'Fake ENOENT')
try:
os.remove = race_remove
- self._storage.save('race.file', ContentFile('Fake ENOENT'))
- self._storage.delete('race.file')
- self.assertFalse(self._storage.exists('race.file'))
+ self.default_storage.save('race.file', ContentFile('Fake ENOENT'))
+ self.default_storage.delete('race.file')
+ self.assertFalse(self.default_storage.exists('race.file'))
finally:
# Restore os.remove
os.remove = original_remove
diff --git a/compressor/tests/test_templates/test_block_super_base_compressed/base.html b/compressor/tests/test_templates/test_block_super_base_compressed/base.html
new file mode 100644
index 0000000..481ff40
--- /dev/null
+++ b/compressor/tests/test_templates/test_block_super_base_compressed/base.html
@@ -0,0 +1,10 @@
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using multiple inheritance and block.super");
+ </script>
+{% endblock %}
+{% endcompress %}
+{% endspaceless %}
diff --git a/compressor/tests/test_templates/test_block_super_base_compressed/base2.html b/compressor/tests/test_templates/test_block_super_base_compressed/base2.html
new file mode 100644
index 0000000..abd074d
--- /dev/null
+++ b/compressor/tests/test_templates/test_block_super_base_compressed/base2.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+
+{% block js %}{% spaceless %}
+ {{ block.super }}
+ <script type="text/javascript">
+ alert("this alert should be included");
+ </script>
+{% endspaceless %}{% endblock %}
diff --git a/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html b/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html
new file mode 100644
index 0000000..01382ec
--- /dev/null
+++ b/compressor/tests/test_templates/test_block_super_base_compressed/test_compressor_offline.html
@@ -0,0 +1,8 @@
+{% extends "base2.html" %}
+
+{% block js %}{% spaceless %}
+ {{ block.super }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+{% endspaceless %}{% endblock %}
diff --git a/compressor/tests/test_templates/test_block_super_multiple/base2.html b/compressor/tests/test_templates/test_block_super_multiple/base2.html
index b0b2fef..c781fb5 100644
--- a/compressor/tests/test_templates/test_block_super_multiple/base2.html
+++ b/compressor/tests/test_templates/test_block_super_multiple/base2.html
@@ -1,3 +1,10 @@
{% extends "base.html" %}
+{% block js %}{% spaceless %}
+ {{ block.super }}
+ <script type="text/javascript">
+ alert("this alert should be included");
+ </script>
+{% endspaceless %}{% endblock %}
+
{% block css %}{% endblock %}
diff --git a/compressor/tests/test_templates/test_complex/test_compressor_offline.html b/compressor/tests/test_templates/test_complex/test_compressor_offline.html
new file mode 100644
index 0000000..6eea06e
--- /dev/null
+++ b/compressor/tests/test_templates/test_complex/test_compressor_offline.html
@@ -0,0 +1,20 @@
+{% load compress static %}{% spaceless %}
+
+{% if condition %}
+ {% compress js%}
+ <script type="text/javascript">alert("{{ condition|default:"yellow" }}");</script>
+ {% with names=my_names %}{% spaceless %}
+ {% for name in names %}
+ <script type="text/javascript" src="{% static name %}"></script>
+ {% endfor %}
+ {% endspaceless %}{% endwith %}
+ {% endcompress %}
+{% endif %}{% if not condition %}
+ {% compress js %}
+ <script type="text/javascript">var not_ok;</script>
+ {% endcompress %}
+{% else %}
+ {% compress js %}
+ <script type="text/javascript">var ok = "ok";</script>
+ {% endcompress %}
+{% endif %}{% endspaceless %}
diff --git a/compressor/tests/test_templates/test_duplicate/test_compressor_offline.html b/compressor/tests/test_templates/test_duplicate/test_compressor_offline.html
new file mode 100644
index 0000000..6050c8b
--- /dev/null
+++ b/compressor/tests/test_templates/test_duplicate/test_compressor_offline.html
@@ -0,0 +1,13 @@
+{% load compress %}{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("Basic test");
+ </script>
+{% endcompress %}
+{% compress js %}
+ <script type="text/javascript">
+ alert("Basic test");
+ </script>
+{% endcompress %}
+{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html
new file mode 100644
index 0000000..6e89ed2
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/basic/test_compressor_offline.html
@@ -0,0 +1,8 @@
+{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("Basic test");
+ </script>
+{% endcompress %}
+{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_block_super/base.html b/compressor/tests/test_templates_jinja2/test_block_super/base.html
new file mode 100644
index 0000000..e9ca3ad
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_block_super/base.html
@@ -0,0 +1,15 @@
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html
new file mode 100644
index 0000000..e1fabd8
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_block_super/test_compressor_offline.html
@@ -0,0 +1,12 @@
+{% extends "base.html" %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ {{ super() }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
+
+{% block css %}{% endblock %}
diff --git a/compressor/tests/test_templates_jinja2/test_block_super_extra/base.html b/compressor/tests/test_templates_jinja2/test_block_super_extra/base.html
new file mode 100644
index 0000000..e9ca3ad
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_block_super_extra/base.html
@@ -0,0 +1,15 @@
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html
new file mode 100644
index 0000000..328ccb9
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_block_super_extra/test_compressor_offline.html
@@ -0,0 +1,18 @@
+{% extends "base.html" %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ <script type="text/javascript">
+ alert("this alert should be alone.");
+ </script>
+ {% endcompress %}
+
+ {% compress js %}
+ {{ super() }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
+
+{% block css %}{% endblock %}
diff --git a/compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html b/compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html
new file mode 100644
index 0000000..c9ee6cc
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_block_super_multiple/base.html
@@ -0,0 +1,15 @@
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using multiple inheritance and block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html b/compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html
new file mode 100644
index 0000000..b0b2fef
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_block_super_multiple/base2.html
@@ -0,0 +1,3 @@
+{% extends "base.html" %}
+
+{% block css %}{% endblock %}
diff --git a/compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html
new file mode 100644
index 0000000..accd76d
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_block_super_multiple/test_compressor_offline.html
@@ -0,0 +1,10 @@
+{% extends "base2.html" %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ {{ super() }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
diff --git a/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html b/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html
new file mode 100644
index 0000000..c9ee6cc
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base.html
@@ -0,0 +1,15 @@
+{% spaceless %}
+{% block js %}
+ <script type="text/javascript">
+ alert("test using multiple inheritance and block.super");
+ </script>
+{% endblock %}
+
+{% block css %}
+ <style type="text/css">
+ body {
+ background: red;
+ }
+ </style>
+{% endblock %}
+{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html b/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html
new file mode 100644
index 0000000..b0b2fef
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/base2.html
@@ -0,0 +1,3 @@
+{% extends "base.html" %}
+
+{% block css %}{% endblock %}
diff --git a/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html
new file mode 100644
index 0000000..accd76d
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_block_super_multiple_cached/test_compressor_offline.html
@@ -0,0 +1,10 @@
+{% extends "base2.html" %}
+
+{% block js %}{% spaceless %}
+ {% compress js %}
+ {{ super() }}
+ <script type="text/javascript">
+ alert("this alert shouldn't be alone!");
+ </script>
+ {% endcompress %}
+{% endspaceless %}{% endblock %}
diff --git a/compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html
new file mode 100644
index 0000000..511ddd0
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_coffin/test_compressor_offline.html
@@ -0,0 +1,11 @@
+{%- load compress -%}
+{% spaceless %}
+ {% compress js%}
+ <script type="text/javascript">alert("{{ condition|default("yellow") }}");
+ var ok = "{% if (25*4) is divisibleby 50 %}ok{% endif %}";
+ </script>
+ {% with "js/one.js" as name -%}
+ <script type="text/javascript" src="{% static name %}"></script>
+ {%- endwith %}
+ {% endcompress %}
+{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html
new file mode 100644
index 0000000..4707182
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_complex/test_compressor_offline.html
@@ -0,0 +1,24 @@
+{% spaceless %}
+
+{% if condition %}
+ {% compress js%}
+ <script type="text/javascript">alert("{{ condition|default("yellow") }}");</script>
+ {% with names=[] -%}
+ {%- do names.append("js/one.js") -%}
+ {%- do names.append("js/nonasc.js") -%}
+ {% for name in names -%}
+ <script type="text/javascript" src="{{url_for('static', filename=name)}}"></script>
+ {%- endfor %}
+ {%- endwith %}
+ {% endcompress %}
+{% endif %}
+{% if not condition -%}
+ {% compress js %}
+ <script type="text/javascript">var not_ok;</script>
+ {% endcompress %}
+{%- else -%}
+ {% compress js %}
+ <script type="text/javascript">var ok = "{% if (25*4) is divisibleby 50 %}ok{% endif %}";</script>
+ {% endcompress %}
+{%- endif %}
+{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html
new file mode 100644
index 0000000..bd1adb8
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_condition/test_compressor_offline.html
@@ -0,0 +1,7 @@
+{% spaceless %}
+
+{% if condition %}
+ {% compress js%}
+ <script type="text/javascript">alert("{{ condition|default("yellow") }}");</script>
+ {% endcompress %}
+{% endif %}{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html b/compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html
new file mode 100644
index 0000000..72513f7
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_error_handling/buggy_extends.html
@@ -0,0 +1,9 @@
+{% extends "buggy_template.html" %}
+
+{% compress css %}
+ <style type="text/css">
+ body {
+ background: orange;
+ }
+ </style>
+{% endcompress %}
diff --git a/compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html b/compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html
new file mode 100644
index 0000000..a01b899
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_error_handling/buggy_template.html
@@ -0,0 +1,10 @@
+{% compress css %}
+ <style type="text/css">
+ body {
+ background: pink;
+ }
+ </style>
+{% endcompress %}
+
+
+{% fail %}
diff --git a/compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html b/compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html
new file mode 100644
index 0000000..dc76034
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_error_handling/missing_extends.html
@@ -0,0 +1,9 @@
+{% extends "missing.html" %}
+
+{% compress css %}
+ <style type="text/css">
+ body {
+ background: purple;
+ }
+ </style>
+{% endcompress %}
diff --git a/compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html
new file mode 100644
index 0000000..3ecffa5
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_error_handling/test_compressor_offline.html
@@ -0,0 +1,8 @@
+{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("Basic test, should pass in spite of errors in other templates");
+ </script>
+{% endcompress %}
+{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html b/compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html
new file mode 100644
index 0000000..8a53e44
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_error_handling/with_coffeescript.html
@@ -0,0 +1,5 @@
+{% compress js %}
+ <script type="text/coffeescript" charset="utf-8">
+ a = 1
+ </script>
+{% endcompress %}
diff --git a/compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html
new file mode 100644
index 0000000..c03b191
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_inline_non_ascii/test_compressor_offline.html
@@ -0,0 +1,7 @@
+{% spaceless %}
+
+{% compress js, inline %}
+ <script type="text/javascript">
+ var value = '{{ test_non_ascii_value }}';
+ </script>
+{% endcompress %}{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html
new file mode 100644
index 0000000..d79c797
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_jingo/test_compressor_offline.html
@@ -0,0 +1,11 @@
+{% spaceless %}
+ {% compress js%}
+ <script type="text/javascript">alert("{{ condition|default("yellow") }}");
+ var ok = "{% if (25*4) is divisibleby 50 %}ok{% endif %}";
+ var text = "{{"hello\nworld"|nl2br}}";
+ </script>
+ {% with name="js/one.js" -%}
+ <script type="text/javascript" src="{{ 8|ifeq(2*4, url_for('static', name)) }}"></script>
+ {%- endwith %}
+ {% endcompress %}
+{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html
new file mode 100644
index 0000000..ed7238c
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_static_templatetag/test_compressor_offline.html
@@ -0,0 +1,6 @@
+{% spaceless %}
+
+{% compress js %}
+ <script>alert('amazing');</script>
+ <script type="text/javascript" src="{{ url_for('static', filename="js/one.js") }}"></script>
+{% endcompress %}{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html
new file mode 100644
index 0000000..31c5d17
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_templatetag/test_compressor_offline.html
@@ -0,0 +1,7 @@
+{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("{{ "testtemplateTAG"|lower }}");
+ </script>
+{% endcompress %}{% endspaceless %}
diff --git a/compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html b/compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html
new file mode 100644
index 0000000..2289a5f
--- /dev/null
+++ b/compressor/tests/test_templates_jinja2/test_with_context/test_compressor_offline.html
@@ -0,0 +1,7 @@
+{% spaceless %}
+
+{% compress js %}
+ <script type="text/javascript">
+ alert("{{ content|default("Ooops!") }}");
+ </script>
+{% endcompress %}{% endspaceless %}
diff --git a/compressor/tests/test_templatetags.py b/compressor/tests/test_templatetags.py
index 151b785..db0d1b7 100644
--- a/compressor/tests/test_templatetags.py
+++ b/compressor/tests/test_templatetags.py
@@ -1,4 +1,4 @@
-from __future__ import with_statement
+from __future__ import with_statement, unicode_literals
import os
import sys
@@ -7,6 +7,7 @@ from mock import Mock
from django.template import Template, Context, TemplateSyntaxError
from django.test import TestCase
+from django.test.utils import override_settings
from compressor.conf import settings
from compressor.signals import post_compress
@@ -34,12 +35,12 @@ class TemplatetagTestCase(TestCase):
settings.COMPRESS_ENABLED = self.old_enabled
def test_empty_tag(self):
- template = u"""{% load compress %}{% compress js %}{% block js %}
+ template = """{% load compress %}{% compress js %}{% block js %}
{% endblock %}{% endcompress %}"""
- self.assertEqual(u'', render(template, self.context))
+ self.assertEqual('', render(template, self.context))
def test_css_tag(self):
- template = u"""{% load compress %}{% compress css %}
+ template = """{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ STATIC_URL }}css/one.css" type="text/css">
<style type="text/css">p { border:5px solid green;}</style>
<link rel="stylesheet" href="{{ STATIC_URL }}css/two.css" type="text/css">
@@ -47,10 +48,8 @@ class TemplatetagTestCase(TestCase):
out = css_tag("/static/CACHE/css/e41ba2cc6982.css")
self.assertEqual(out, render(template, self.context))
- maxDiff = None
-
def test_uppercase_rel(self):
- template = u"""{% load compress %}{% compress css %}
+ template = """{% load compress %}{% compress css %}
<link rel="StyleSheet" href="{{ STATIC_URL }}css/one.css" type="text/css">
<style type="text/css">p { border:5px solid green;}</style>
<link rel="StyleSheet" href="{{ STATIC_URL }}css/two.css" type="text/css">
@@ -59,7 +58,7 @@ class TemplatetagTestCase(TestCase):
self.assertEqual(out, render(template, self.context))
def test_nonascii_css_tag(self):
- template = u"""{% load compress %}{% compress css %}
+ template = """{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ STATIC_URL }}css/nonasc.css" type="text/css">
<style type="text/css">p { border:5px solid green;}</style>
{% endcompress %}
@@ -68,40 +67,41 @@ class TemplatetagTestCase(TestCase):
self.assertEqual(out, render(template, self.context))
def test_js_tag(self):
- template = u"""{% load compress %}{% compress js %}
+ template = """{% load compress %}{% compress js %}
<script src="{{ STATIC_URL }}js/one.js" type="text/javascript"></script>
<script type="text/javascript">obj.value = "value";</script>
{% endcompress %}
"""
- out = u'<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
+ out = '<script type="text/javascript" src="/static/CACHE/js/066cd253eada.js"></script>'
self.assertEqual(out, render(template, self.context))
def test_nonascii_js_tag(self):
- template = u"""{% load compress %}{% compress js %}
+ template = """{% load compress %}{% compress js %}
<script src="{{ STATIC_URL }}js/nonasc.js" type="text/javascript"></script>
<script type="text/javascript">var test_value = "\u2014";</script>
{% endcompress %}
"""
- out = u'<script type="text/javascript" src="/static/CACHE/js/e214fe629b28.js"></script>'
+ out = '<script type="text/javascript" src="/static/CACHE/js/e214fe629b28.js"></script>'
self.assertEqual(out, render(template, self.context))
def test_nonascii_latin1_js_tag(self):
- template = u"""{% load compress %}{% compress js %}
+ template = """{% load compress %}{% compress js %}
<script src="{{ STATIC_URL }}js/nonasc-latin1.js" type="text/javascript" charset="latin-1"></script>
<script type="text/javascript">var test_value = "\u2014";</script>
{% endcompress %}
"""
- out = u'<script type="text/javascript" src="/static/CACHE/js/be9e078b5ca7.js"></script>'
+ out = '<script type="text/javascript" src="/static/CACHE/js/be9e078b5ca7.js"></script>'
self.assertEqual(out, render(template, self.context))
def test_compress_tag_with_illegal_arguments(self):
- template = u"""{% load compress %}{% compress pony %}
+ template = """{% load compress %}{% compress pony %}
<script type="pony/application">unicorn</script>
{% endcompress %}"""
self.assertRaises(TemplateSyntaxError, render, template, {})
+ @override_settings(COMPRESS_DEBUG_TOGGLE='togglecompress')
def test_debug_toggle(self):
- template = u"""{% load compress %}{% compress js %}
+ template = """{% load compress %}{% compress js %}
<script src="{{ STATIC_URL }}js/one.js" type="text/javascript"></script>
<script type="text/javascript">obj.value = "value";</script>
{% endcompress %}
@@ -111,12 +111,12 @@ class TemplatetagTestCase(TestCase):
GET = {settings.COMPRESS_DEBUG_TOGGLE: 'true'}
context = dict(self.context, request=MockDebugRequest())
- out = u"""<script src="/static/js/one.js" type="text/javascript"></script>
+ out = """<script src="/static/js/one.js" type="text/javascript"></script>
<script type="text/javascript">obj.value = "value";</script>"""
self.assertEqual(out, render(template, context))
def test_named_compress_tag(self):
- template = u"""{% load compress %}{% compress js inline foo %}
+ template = """{% load compress %}{% compress js inline foo %}
<script type="text/javascript">obj.value = "value";</script>
{% endcompress %}
"""
@@ -151,118 +151,94 @@ class PrecompilerTemplatetagTestCase(TestCase):
settings.COMPRESS_PRECOMPILERS = self.old_precompilers
def test_compress_coffeescript_tag(self):
- template = u"""{% load compress %}{% compress js %}
+ template = """{% load compress %}{% compress js %}
<script type="text/coffeescript"># this is a comment.</script>
{% endcompress %}"""
out = script(src="/static/CACHE/js/e920d58f166d.js")
self.assertEqual(out, render(template, self.context))
def test_compress_coffeescript_tag_and_javascript_tag(self):
- template = u"""{% load compress %}{% compress js %}
+ template = """{% load compress %}{% compress js %}
<script type="text/coffeescript"># this is a comment.</script>
<script type="text/javascript"># this too is a comment.</script>
{% endcompress %}"""
out = script(src="/static/CACHE/js/ef6b32a54575.js")
self.assertEqual(out, render(template, self.context))
+ @override_settings(COMPRESS_ENABLED=False)
def test_coffeescript_and_js_tag_with_compress_enabled_equals_false(self):
- self.old_enabled = settings.COMPRESS_ENABLED
- settings.COMPRESS_ENABLED = False
- try:
- template = u"""{% load compress %}{% compress js %}
- <script type="text/coffeescript"># this is a comment.</script>
- <script type="text/javascript"># this too is a comment.</script>
- {% endcompress %}"""
- out = (script('# this is a comment.\n') + '\n' +
- script('# this too is a comment.'))
- self.assertEqual(out, render(template, self.context))
- finally:
- settings.COMPRESS_ENABLED = self.old_enabled
+ template = """{% load compress %}{% compress js %}
+ <script type="text/coffeescript"># this is a comment.</script>
+ <script type="text/javascript"># this too is a comment.</script>
+ {% endcompress %}"""
+ out = (script('# this is a comment.\n') + '\n' +
+ script('# this too is a comment.'))
+ self.assertEqual(out, render(template, self.context))
+ @override_settings(COMPRESS_ENABLED=False)
def test_compress_coffeescript_tag_compress_enabled_is_false(self):
- self.old_enabled = settings.COMPRESS_ENABLED
- settings.COMPRESS_ENABLED = False
- try:
- template = u"""{% load compress %}{% compress js %}
- <script type="text/coffeescript"># this is a comment.</script>
- {% endcompress %}"""
- out = script("# this is a comment.\n")
- self.assertEqual(out, render(template, self.context))
- finally:
- settings.COMPRESS_ENABLED = self.old_enabled
+ template = """{% load compress %}{% compress js %}
+ <script type="text/coffeescript"># this is a comment.</script>
+ {% endcompress %}"""
+ out = script("# this is a comment.\n")
+ self.assertEqual(out, render(template, self.context))
+ @override_settings(COMPRESS_ENABLED=False)
def test_compress_coffeescript_file_tag_compress_enabled_is_false(self):
- self.old_enabled = settings.COMPRESS_ENABLED
- settings.COMPRESS_ENABLED = False
- try:
- template = u"""
- {% load compress %}{% compress js %}
- <script type="text/coffeescript" src="{{ STATIC_URL }}js/one.coffee">
- </script>
- {% endcompress %}"""
+ template = """
+ {% load compress %}{% compress js %}
+ <script type="text/coffeescript" src="{{ STATIC_URL }}js/one.coffee">
+ </script>
+ {% endcompress %}"""
- out = script(src="/static/CACHE/js/one.95cfb869eead.js")
- self.assertEqual(out, render(template, self.context))
- finally:
- settings.COMPRESS_ENABLED = self.old_enabled
+ out = script(src="/static/CACHE/js/one.95cfb869eead.js")
+ self.assertEqual(out, render(template, self.context))
+ @override_settings(COMPRESS_ENABLED=False)
def test_multiple_file_order_conserved(self):
- self.old_enabled = settings.COMPRESS_ENABLED
- settings.COMPRESS_ENABLED = False
- try:
- template = u"""
- {% load compress %}{% compress js %}
- <script type="text/coffeescript" src="{{ STATIC_URL }}js/one.coffee">
- </script>
- <script src="{{ STATIC_URL }}js/one.js"></script>
- <script type="text/coffeescript" src="{{ STATIC_URL }}js/one.js">
- </script>
- {% endcompress %}"""
+ template = """
+ {% load compress %}{% compress js %}
+ <script type="text/coffeescript" src="{{ STATIC_URL }}js/one.coffee">
+ </script>
+ <script src="{{ STATIC_URL }}js/one.js"></script>
+ <script type="text/coffeescript" src="{{ STATIC_URL }}js/one.js">
+ </script>
+ {% endcompress %}"""
- out = '\n'.join([script(src="/static/CACHE/js/one.95cfb869eead.js"),
- script(scripttype="", src="/static/js/one.js"),
- script(src="/static/CACHE/js/one.81a2cd965815.js")])
+ out = '\n'.join([script(src="/static/CACHE/js/one.95cfb869eead.js"),
+ script(scripttype="", src="/static/js/one.js"),
+ script(src="/static/CACHE/js/one.81a2cd965815.js")])
- self.assertEqual(out, render(template, self.context))
- finally:
- settings.COMPRESS_ENABLED = self.old_enabled
+ self.assertEqual(out, render(template, self.context))
+ @override_settings(COMPRESS_ENABLED=False)
def test_css_multiple_files_disabled_compression(self):
- self.old_enabled = settings.COMPRESS_ENABLED
- settings.COMPRESS_ENABLED = False
assert(settings.COMPRESS_PRECOMPILERS)
- try:
- template = u"""
- {% load compress %}{% compress css %}
- <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/one.css"></link>
- <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/two.css"></link>
- {% endcompress %}"""
+ template = """
+ {% load compress %}{% compress css %}
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/one.css"></link>
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/two.css"></link>
+ {% endcompress %}"""
- out = ''.join(['<link rel="stylesheet" type="text/css" href="/static/css/one.css" />',
- '<link rel="stylesheet" type="text/css" href="/static/css/two.css" />'])
+ out = ''.join(['<link rel="stylesheet" type="text/css" href="/static/css/one.css" />',
+ '<link rel="stylesheet" type="text/css" href="/static/css/two.css" />'])
- self.assertEqual(out, render(template, self.context))
- finally:
- settings.COMPRESS_ENABLED = self.old_enabled
+ self.assertEqual(out, render(template, self.context))
+ @override_settings(COMPRESS_ENABLED=False)
def test_css_multiple_files_mixed_precompile_disabled_compression(self):
- self.old_enabled = settings.COMPRESS_ENABLED
- settings.COMPRESS_ENABLED = False
assert(settings.COMPRESS_PRECOMPILERS)
- try:
- template = u"""
- {% load compress %}{% compress css %}
- <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/one.css"/>
- <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/two.css"/>
- <link rel="stylesheet" type="text/less" href="{{ STATIC_URL }}css/url/test.css"/>
- {% endcompress %}"""
+ template = """
+ {% load compress %}{% compress css %}
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/one.css"/>
+ <link rel="stylesheet" type="text/css" href="{{ STATIC_URL }}css/two.css"/>
+ <link rel="stylesheet" type="text/less" href="{{ STATIC_URL }}css/url/test.css"/>
+ {% endcompress %}"""
- out = ''.join(['<link rel="stylesheet" type="text/css" href="/static/css/one.css" />',
- '<link rel="stylesheet" type="text/css" href="/static/css/two.css" />',
- '<link rel="stylesheet" href="/static/CACHE/css/test.5dddc6c2fb5a.css" type="text/css" />'])
- self.assertEqual(out, render(template, self.context))
- finally:
- settings.COMPRESS_ENABLED = self.old_enabled
+ out = ''.join(['<link rel="stylesheet" type="text/css" href="/static/css/one.css" />',
+ '<link rel="stylesheet" type="text/css" href="/static/css/two.css" />',
+ '<link rel="stylesheet" href="/static/CACHE/css/test.5dddc6c2fb5a.css" type="text/css" />'])
+ self.assertEqual(out, render(template, self.context))
def script(content="", src="", scripttype="text/javascript"):
@@ -272,9 +248,9 @@ def script(content="", src="", scripttype="text/javascript"):
>>> script('#this is a comment', scripttype="text/applescript")
'<script type="text/applescript">#this is a comment</script>'
"""
- out_script = u'<script '
+ out_script = '<script '
if scripttype:
- out_script += u'type="%s" ' % scripttype
+ out_script += 'type="%s" ' % scripttype
if src:
- out_script += u'src="%s" ' % src
- return out_script[:-1] + u'>%s</script>' % content
+ out_script += 'src="%s" ' % src
+ return out_script[:-1] + '>%s</script>' % content
diff --git a/compressor/utils/__init__.py b/compressor/utils/__init__.py
index 83a1a2a..1c3479b 100644
--- a/compressor/utils/__init__.py
+++ b/compressor/utils/__init__.py
@@ -1,6 +1,9 @@
# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
import os
+from django.utils import six
+
from compressor.exceptions import FilterError
@@ -10,15 +13,14 @@ def get_class(class_string, exception=FilterError):
"""
if not hasattr(class_string, '__bases__'):
try:
- class_string = class_string.encode('ascii')
+ class_string = str(class_string)
mod_name, class_name = get_mod_func(class_string)
- if class_name != '':
- cls = getattr(__import__(mod_name, {}, {}, ['']), class_name)
+ if class_name:
+ return getattr(__import__(mod_name, {}, {}, [str('')]), class_name)
except (ImportError, AttributeError):
- pass
- else:
- return cls
- raise exception('Failed to import %s' % class_string)
+ raise exception('Failed to import %s' % class_string)
+
+ raise exception("Invalid class path '%s'" % class_string)
def get_mod_func(callback):
@@ -48,7 +50,7 @@ def find_command(cmd, paths=None, pathext=None):
"""
if paths is None:
paths = os.environ.get('PATH', '').split(os.pathsep)
- if isinstance(paths, basestring):
+ if isinstance(paths, six.string_types):
paths = [paths]
# check if there are funny path extensions for executables, e.g. Windows
if pathext is None:
diff --git a/compressor/utils/staticfiles.py b/compressor/utils/staticfiles.py
index 169d427..28026f2 100644
--- a/compressor/utils/staticfiles.py
+++ b/compressor/utils/staticfiles.py
@@ -1,4 +1,4 @@
-from __future__ import absolute_import
+from __future__ import absolute_import, unicode_literals
from django.core.exceptions import ImproperlyConfigured
diff --git a/compressor/utils/stringformat.py b/compressor/utils/stringformat.py
index 0cfba86..9311e78 100644
--- a/compressor/utils/stringformat.py
+++ b/compressor/utils/stringformat.py
@@ -6,8 +6,12 @@ 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'|(?:}})+(?!})' # '}}
@@ -128,7 +132,7 @@ def _format_field(value, parts, conv, spec, want_bytes=False):
value = value.strftime(str(spec))
else:
value = _strformat(value, spec)
- if want_bytes and isinstance(value, unicode):
+ if want_bytes and isinstance(value, six.text_type):
return str(value)
return value
@@ -138,9 +142,9 @@ class FormattableString(object):
The method format() behaves like str.format() in python 2.6+.
- >>> FormattableString(u'{a:5}').format(a=42)
- ... # Same as u'{a:5}'.format(a=42)
- u' 42'
+ >>> FormattableString('{a:5}').format(a=42)
+ ... # Same as '{a:5}'.format(a=42)
+ ' 42'
"""
@@ -244,13 +248,13 @@ def selftest():
import datetime
F = FormattableString
- assert F(u"{0:{width}.{precision}s}").format('hello world',
- width=8, precision=5) == u'hello '
+ assert F("{0:{width}.{precision}s}").format('hello world',
+ width=8, precision=5) == 'hello '
d = datetime.date(2010, 9, 7)
- assert F(u"The year is {0.year}").format(d) == u"The year is 2010"
- assert F(u"Tested on {0:%Y-%m-%d}").format(d) == u"Tested on 2010-09-07"
- print 'Test successful'
+ 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()