summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--compressor/base.py11
-rw-r--r--compressor/base.py.orig384
-rw-r--r--compressor/base_BACKUP_20651.py384
-rw-r--r--compressor/base_BASE_20651.py365
-rw-r--r--compressor/base_LOCAL_20651.py374
-rw-r--r--compressor/base_REMOTE_20651.py381
-rw-r--r--compressor/cache.py2
-rw-r--r--compressor/contrib/jinja2ext.py2
-rw-r--r--compressor/contrib/jinja2ext.py.orig90
-rw-r--r--compressor/filters/base.py2
-rw-r--r--compressor/js.py2
-rw-r--r--compressor/management/commands/compress.py2
-rw-r--r--compressor/parser/__init__.py2
-rw-r--r--compressor/parser/beautifulsoup.py6
-rw-r--r--compressor/parser/default_htmlparser.py4
-rw-r--r--compressor/parser/html5lib.py2
-rw-r--r--compressor/parser/html5lib.py.orig63
-rw-r--r--compressor/parser/lxml.py9
-rw-r--r--compressor/templates/compressor/css_file.html2
-rw-r--r--compressor/templates/compressor/css_preload.html1
-rw-r--r--compressor/templates/compressor/js_file.html2
-rw-r--r--compressor/templates/compressor/js_inline.html2
-rw-r--r--compressor/templates/compressor/js_preload.html1
-rw-r--r--compressor/templatetags/compress.py5
-rw-r--r--compressor/tests/test_base.py52
-rw-r--r--compressor/tests/test_base.py.orig477
-rw-r--r--compressor/tests/test_filters.py2
-rw-r--r--compressor/tests/test_jinja2ext.py10
-rw-r--r--compressor/tests/test_mtime_cache.py2
-rw-r--r--compressor/tests/test_offline.py72
-rw-r--r--compressor/tests/test_offline.py.orig868
-rw-r--r--compressor/tests/test_parsers.py5
-rw-r--r--compressor/tests/test_sekizai.py6
-rw-r--r--compressor/tests/test_sekizai.py.orig51
-rw-r--r--compressor/tests/test_signals.py6
-rw-r--r--compressor/tests/test_signals.py.orig78
-rw-r--r--compressor/tests/test_templatetags.py28
-rw-r--r--docs/changelog.txt2
-rw-r--r--docs/changelog.txt.orig542
-rw-r--r--docs/usage.txt17
-rw-r--r--requirements/tests.txt2
-rw-r--r--requirements/tests.txt.orig16
-rw-r--r--setup.py1
-rw-r--r--setup.py.orig152
44 files changed, 4373 insertions, 114 deletions
diff --git a/compressor/base.py b/compressor/base.py
index 7200479..9d79dab 100644
--- a/compressor/base.py
+++ b/compressor/base.py
@@ -3,12 +3,12 @@ import os
import codecs
from importlib import import_module
+import six
from django.core.files.base import ContentFile
-from django.utils import six
from django.utils.safestring import mark_safe
-from django.utils.six.moves.urllib.request import url2pathname
from django.template.loader import render_to_string
from django.utils.functional import cached_property
+from six.moves.urllib.request import url2pathname
from compressor.cache import get_hexdigest, get_mtime
from compressor.conf import settings
@@ -349,6 +349,13 @@ class Compressor(object):
"""
return self.render_output(mode, {"content": content})
+ def output_preload(self, mode, content, forced=False, basename=None):
+ """
+ The output method that returns <link> with rel="preload" and
+ proper href attribute for given file.
+ """
+ return self.output_file(mode, content, forced, basename)
+
def render_output(self, mode, context=None):
"""
Renders the compressor output with the appropriate template for
diff --git a/compressor/base.py.orig b/compressor/base.py.orig
new file mode 100644
index 0000000..c04fd22
--- /dev/null
+++ b/compressor/base.py.orig
@@ -0,0 +1,384 @@
+from __future__ import with_statement, unicode_literals
+import os
+import codecs
+from importlib import import_module
+
+<<<<<<< HEAD
+=======
+import six
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+from django.core.files.base import ContentFile
+from django.utils.safestring import mark_safe
+from django.template.loader import render_to_string
+from django.utils.functional import cached_property
+from six.moves.urllib.request 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 CachedCompilerFilter
+from compressor.storage import compressor_file_storage
+from compressor.signals import post_compress
+from compressor.utils import get_class, get_mod_func, staticfiles
+
+# Some constants for nicer handling.
+SOURCE_HUNK, SOURCE_FILE = 'inline', 'file'
+METHOD_INPUT, METHOD_OUTPUT = 'input', 'output'
+
+
+class Compressor(object):
+ """
+ Base compressor object to be subclassed for content type
+ depending implementations details.
+ """
+
+ output_mimetypes = {}
+
+ def __init__(self, resource_kind, content=None, output_prefix=None,
+ context=None, filters=None, *args, **kwargs):
+ if filters is None:
+ self.filters = settings.COMPRESS_FILTERS[resource_kind]
+ else:
+ self.filters = filters
+ if output_prefix is None:
+ self.output_prefix = resource_kind
+ else:
+ self.output_prefix = output_prefix
+ self.content = content or "" # rendered contents of {% compress %} tag
+ self.output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
+ self.charset = settings.DEFAULT_CHARSET
+ self.split_content = []
+ self.context = context or {}
+ self.resource_kind = resource_kind
+ self.extra_context = {}
+ self.precompiler_mimetypes = dict(settings.COMPRESS_PRECOMPILERS)
+ self.finders = staticfiles.finders
+ self._storage = None
+
+ def copy(self, **kwargs):
+ keywords = dict(
+ content=self.content,
+ context=self.context,
+ output_prefix=self.output_prefix,
+ filters=self.filters)
+ keywords.update(kwargs)
+ return self.__class__(self.resource_kind, **keywords)
+
+ @cached_property
+ def storage(self):
+ from compressor.storage import default_storage
+ return default_storage
+
+ def split_contents(self):
+ """
+ To be implemented in a subclass, should return an
+ iterable with four values: kind, value, basename, element
+ """
+ raise NotImplementedError
+
+ def get_template_name(self, mode):
+ """
+ Returns the template path for the given mode.
+ """
+ try:
+ template = getattr(self, "template_name_%s" % mode)
+ if template:
+ return template
+ except AttributeError:
+ pass
+ return "compressor/%s_%s.html" % (self.resource_kind, 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:
+ base_url = settings.COMPRESS_URL
+
+ # Cast ``base_url`` to a string to allow it to be
+ # a string-alike object to e.g. add ``SCRIPT_NAME``
+ # WSGI param as a *path prefix* to the output URL.
+ # See https://code.djangoproject.com/ticket/25598.
+ base_url = six.text_type(base_url)
+
+ if not url.startswith(base_url):
+ raise UncompressableFileError("'%s' isn't accessible via "
+ "COMPRESS_URL ('%s') and can't be "
+ "compressed" % (url, base_url))
+ basename = url.replace(base_url, "", 1)
+ # drop the querystring, which is used for non-compressed cache-busting.
+ 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/58a8c0714e59.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.58a8c0714e59.css'
+ """
+ parts = []
+ if basename:
+ filename = os.path.split(basename)[1]
+ parts.append(os.path.splitext(filename)[0])
+ parts.extend([get_hexdigest(content, 12), self.resource_kind])
+ 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 using the storage class.
+ # This is skipped in DEBUG mode as files might be outdated in
+ # compressor's final destination (COMPRESS_ROOT) during development
+ if not settings.DEBUG:
+ try:
+ # call path first so remote storages don't make it to exists,
+ # which would cause network I/O
+ filename = self.storage.path(basename)
+ if not self.storage.exists(basename):
+ filename = None
+ except NotImplementedError:
+ # remote storages don't implement path, access the file locally
+ if compressor_file_storage.exists(basename):
+ filename = compressor_file_storage.path(basename)
+ # secondly try to find it with staticfiles
+ if not filename and self.finders:
+ filename = self.finders.find(url2pathname(basename))
+ if filename:
+ return filename
+ # or just raise an exception as the last resort
+ raise UncompressableFileError(
+ "'%s' could not be found in the COMPRESS_ROOT '%s'%s" %
+ (basename, settings.COMPRESS_ROOT,
+ self.finders and " or with staticfiles." or "."))
+
+ def get_filecontent(self, filename, charset):
+ """
+ Reads file contents using given `charset` and returns it as text.
+ """
+ if charset == 'utf-8':
+ # Removes BOM
+ charset = 'utf-8-sig'
+ with codecs.open(filename, 'r', charset) as fd:
+ try:
+ return fd.read()
+ except IOError as e:
+ raise UncompressableFileError("IOError while processing "
+ "'%s': %s" % (filename, e))
+ except UnicodeDecodeError as e:
+ raise UncompressableFileError("UnicodeDecodeError while "
+ "processing '%s' with "
+ "charset %s: %s" %
+ (filename, charset, e))
+
+ @cached_property
+ def parser(self):
+ return get_class(settings.COMPRESS_PARSER)(self.content)
+
+ @cached_property
+ def cached_filters(self):
+ return [get_class(filter_cls) for filter_cls in self.filters]
+
+ @cached_property
+ def mtimes(self):
+ return [str(get_mtime(value))
+ for kind, value, basename, elem in self.split_contents()
+ if kind == SOURCE_FILE]
+
+ @cached_property
+ def cachekey(self):
+ return get_hexdigest(''.join(
+ [self.content] + self.mtimes).encode(self.charset), 12)
+
+ def hunks(self, forced=False):
+ """
+ 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.
+ """
+ enabled = settings.COMPRESS_ENABLED or forced
+
+ for kind, value, basename, elem in self.split_contents():
+ precompiled = False
+ attribs = self.parser.elem_attribs(elem)
+ charset = attribs.get("charset", self.charset)
+ options = {
+ 'method': METHOD_INPUT,
+ 'elem': elem,
+ 'kind': kind,
+ 'basename': basename,
+ 'charset': charset,
+ }
+
+ if kind == SOURCE_FILE:
+ options = dict(options, filename=value)
+ value = self.get_filecontent(value, charset)
+
+ if self.precompiler_mimetypes:
+ precompiled, value = self.precompile(value, **options)
+
+ if enabled:
+ yield self.filter(value, self.cached_filters, **options)
+ elif precompiled:
+ for filter_cls in self.cached_filters:
+ if filter_cls.run_with_compression_disabled:
+ value = self.filter(value, [filter_cls], **options)
+ yield self.handle_output(kind, value, forced=True,
+ basename=basename)
+ else:
+ yield self.parser.elem_str(elem)
+
+ def filter_output(self, content):
+ """
+ Passes the concatenated content to the 'output' methods
+ of the compressor filters.
+ """
+ return self.filter(content, self.cached_filters, method=METHOD_OUTPUT)
+
+ def filter_input(self, forced=False):
+ """
+ Passes each hunk (file or code) to the 'input' methods
+ of the compressor filters.
+ """
+ content = []
+ for hunk in self.hunks(forced):
+ content.append(hunk)
+ return content
+
+ 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)
+ mimetype = attrs.get("type", None)
+ if mimetype is None:
+ return False, content
+
+ filter_or_command = self.precompiler_mimetypes.get(mimetype)
+ if filter_or_command is None:
+ if mimetype in self.output_mimetypes:
+ return False, content
+ raise CompressorError("Couldn't find any precompiler in "
+ "COMPRESS_PRECOMPILERS setting for "
+ "mimetype '%s'." % mimetype)
+
+ mod_name, cls_name = get_mod_func(filter_or_command)
+ try:
+ mod = import_module(mod_name)
+ except (ImportError, TypeError):
+ filter = CachedCompilerFilter(
+ content=content, filter_type=self.resource_kind, filename=filename,
+ charset=charset, command=filter_or_command, mimetype=mimetype)
+ return True, filter.input(**kwargs)
+ try:
+ precompiler_class = getattr(mod, cls_name)
+ except AttributeError:
+ raise FilterDoesNotExist('Could not find "%s".' % filter_or_command)
+ filter = precompiler_class(
+ content, attrs=attrs, filter_type=self.resource_kind, charset=charset,
+ filename=filename)
+ return True, filter.input(**kwargs)
+
+ def filter(self, content, filters, method, **kwargs):
+ for filter_cls in filters:
+ filter_func = getattr(
+ filter_cls(content, filter_type=self.resource_kind), method)
+ try:
+ if callable(filter_func):
+ content = filter_func(**kwargs)
+ except NotImplementedError:
+ pass
+ return content
+
+ def output(self, mode='file', forced=False, basename=None):
+ """
+ The general output method, override in subclass if you need to do
+ any custom modification. Calls other mode specific methods or simply
+ returns the content directly.
+ """
+ output = '\n'.join(self.filter_input(forced))
+
+ if not output:
+ return ''
+
+ if settings.COMPRESS_ENABLED or forced:
+ filtered_output = self.filter_output(output)
+ return self.handle_output(mode, filtered_output, forced, basename)
+
+ return output
+
+ def handle_output(self, mode, content, forced, basename=None):
+ # Then check for the appropriate output method and call it
+ output_func = getattr(self, "output_%s" % mode, None)
+ if callable(output_func):
+ return output_func(mode, content, forced, basename)
+ # Total failure, raise a general exception
+ raise CompressorError(
+ "Couldn't find output method for mode '%s'" % mode)
+
+ def output_file(self, mode, content, forced=False, basename=None):
+ """
+ The output method that saves the content to a file and renders
+ the appropriate template with the file's URL.
+ """
+ new_filepath = self.get_filepath(content, basename=basename)
+ if not self.storage.exists(new_filepath) or forced:
+ 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})
+
+ def output_inline(self, mode, content, forced=False, basename=None):
+ """
+ The output method that directly returns the content for inline
+ display.
+ """
+ return self.render_output(mode, {"content": content})
+
+ def output_preload(self, mode, content, forced=False, basename=None):
+ """
+ The output method that returns <link> with rel="preload" and
+ proper href attribute for given file.
+ """
+ return self.output_file(mode, content, forced, basename)
+
+ def render_output(self, mode, context=None):
+ """
+ Renders the compressor output with the appropriate template for
+ the given mode and template context.
+ """
+ # Just in case someone renders the compressor outside
+ # the usual template rendering cycle
+ if 'compressed' not in self.context:
+ self.context['compressed'] = {}
+
+ self.context['compressed'].update(context or {})
+ self.context['compressed'].update(self.extra_context)
+
+ if hasattr(self.context, 'flatten'):
+ # Passing Contexts to Template.render is deprecated since Django 1.8.
+ final_context = self.context.flatten()
+ else:
+ final_context = self.context
+
+ post_compress.send(sender=self.__class__, type=self.resource_kind,
+ mode=mode, context=final_context)
+ template_name = self.get_template_name(mode)
+ return render_to_string(template_name, context=final_context)
diff --git a/compressor/base_BACKUP_20651.py b/compressor/base_BACKUP_20651.py
new file mode 100644
index 0000000..c04fd22
--- /dev/null
+++ b/compressor/base_BACKUP_20651.py
@@ -0,0 +1,384 @@
+from __future__ import with_statement, unicode_literals
+import os
+import codecs
+from importlib import import_module
+
+<<<<<<< HEAD
+=======
+import six
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+from django.core.files.base import ContentFile
+from django.utils.safestring import mark_safe
+from django.template.loader import render_to_string
+from django.utils.functional import cached_property
+from six.moves.urllib.request 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 CachedCompilerFilter
+from compressor.storage import compressor_file_storage
+from compressor.signals import post_compress
+from compressor.utils import get_class, get_mod_func, staticfiles
+
+# Some constants for nicer handling.
+SOURCE_HUNK, SOURCE_FILE = 'inline', 'file'
+METHOD_INPUT, METHOD_OUTPUT = 'input', 'output'
+
+
+class Compressor(object):
+ """
+ Base compressor object to be subclassed for content type
+ depending implementations details.
+ """
+
+ output_mimetypes = {}
+
+ def __init__(self, resource_kind, content=None, output_prefix=None,
+ context=None, filters=None, *args, **kwargs):
+ if filters is None:
+ self.filters = settings.COMPRESS_FILTERS[resource_kind]
+ else:
+ self.filters = filters
+ if output_prefix is None:
+ self.output_prefix = resource_kind
+ else:
+ self.output_prefix = output_prefix
+ self.content = content or "" # rendered contents of {% compress %} tag
+ self.output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
+ self.charset = settings.DEFAULT_CHARSET
+ self.split_content = []
+ self.context = context or {}
+ self.resource_kind = resource_kind
+ self.extra_context = {}
+ self.precompiler_mimetypes = dict(settings.COMPRESS_PRECOMPILERS)
+ self.finders = staticfiles.finders
+ self._storage = None
+
+ def copy(self, **kwargs):
+ keywords = dict(
+ content=self.content,
+ context=self.context,
+ output_prefix=self.output_prefix,
+ filters=self.filters)
+ keywords.update(kwargs)
+ return self.__class__(self.resource_kind, **keywords)
+
+ @cached_property
+ def storage(self):
+ from compressor.storage import default_storage
+ return default_storage
+
+ def split_contents(self):
+ """
+ To be implemented in a subclass, should return an
+ iterable with four values: kind, value, basename, element
+ """
+ raise NotImplementedError
+
+ def get_template_name(self, mode):
+ """
+ Returns the template path for the given mode.
+ """
+ try:
+ template = getattr(self, "template_name_%s" % mode)
+ if template:
+ return template
+ except AttributeError:
+ pass
+ return "compressor/%s_%s.html" % (self.resource_kind, 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:
+ base_url = settings.COMPRESS_URL
+
+ # Cast ``base_url`` to a string to allow it to be
+ # a string-alike object to e.g. add ``SCRIPT_NAME``
+ # WSGI param as a *path prefix* to the output URL.
+ # See https://code.djangoproject.com/ticket/25598.
+ base_url = six.text_type(base_url)
+
+ if not url.startswith(base_url):
+ raise UncompressableFileError("'%s' isn't accessible via "
+ "COMPRESS_URL ('%s') and can't be "
+ "compressed" % (url, base_url))
+ basename = url.replace(base_url, "", 1)
+ # drop the querystring, which is used for non-compressed cache-busting.
+ 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/58a8c0714e59.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.58a8c0714e59.css'
+ """
+ parts = []
+ if basename:
+ filename = os.path.split(basename)[1]
+ parts.append(os.path.splitext(filename)[0])
+ parts.extend([get_hexdigest(content, 12), self.resource_kind])
+ 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 using the storage class.
+ # This is skipped in DEBUG mode as files might be outdated in
+ # compressor's final destination (COMPRESS_ROOT) during development
+ if not settings.DEBUG:
+ try:
+ # call path first so remote storages don't make it to exists,
+ # which would cause network I/O
+ filename = self.storage.path(basename)
+ if not self.storage.exists(basename):
+ filename = None
+ except NotImplementedError:
+ # remote storages don't implement path, access the file locally
+ if compressor_file_storage.exists(basename):
+ filename = compressor_file_storage.path(basename)
+ # secondly try to find it with staticfiles
+ if not filename and self.finders:
+ filename = self.finders.find(url2pathname(basename))
+ if filename:
+ return filename
+ # or just raise an exception as the last resort
+ raise UncompressableFileError(
+ "'%s' could not be found in the COMPRESS_ROOT '%s'%s" %
+ (basename, settings.COMPRESS_ROOT,
+ self.finders and " or with staticfiles." or "."))
+
+ def get_filecontent(self, filename, charset):
+ """
+ Reads file contents using given `charset` and returns it as text.
+ """
+ if charset == 'utf-8':
+ # Removes BOM
+ charset = 'utf-8-sig'
+ with codecs.open(filename, 'r', charset) as fd:
+ try:
+ return fd.read()
+ except IOError as e:
+ raise UncompressableFileError("IOError while processing "
+ "'%s': %s" % (filename, e))
+ except UnicodeDecodeError as e:
+ raise UncompressableFileError("UnicodeDecodeError while "
+ "processing '%s' with "
+ "charset %s: %s" %
+ (filename, charset, e))
+
+ @cached_property
+ def parser(self):
+ return get_class(settings.COMPRESS_PARSER)(self.content)
+
+ @cached_property
+ def cached_filters(self):
+ return [get_class(filter_cls) for filter_cls in self.filters]
+
+ @cached_property
+ def mtimes(self):
+ return [str(get_mtime(value))
+ for kind, value, basename, elem in self.split_contents()
+ if kind == SOURCE_FILE]
+
+ @cached_property
+ def cachekey(self):
+ return get_hexdigest(''.join(
+ [self.content] + self.mtimes).encode(self.charset), 12)
+
+ def hunks(self, forced=False):
+ """
+ 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.
+ """
+ enabled = settings.COMPRESS_ENABLED or forced
+
+ for kind, value, basename, elem in self.split_contents():
+ precompiled = False
+ attribs = self.parser.elem_attribs(elem)
+ charset = attribs.get("charset", self.charset)
+ options = {
+ 'method': METHOD_INPUT,
+ 'elem': elem,
+ 'kind': kind,
+ 'basename': basename,
+ 'charset': charset,
+ }
+
+ if kind == SOURCE_FILE:
+ options = dict(options, filename=value)
+ value = self.get_filecontent(value, charset)
+
+ if self.precompiler_mimetypes:
+ precompiled, value = self.precompile(value, **options)
+
+ if enabled:
+ yield self.filter(value, self.cached_filters, **options)
+ elif precompiled:
+ for filter_cls in self.cached_filters:
+ if filter_cls.run_with_compression_disabled:
+ value = self.filter(value, [filter_cls], **options)
+ yield self.handle_output(kind, value, forced=True,
+ basename=basename)
+ else:
+ yield self.parser.elem_str(elem)
+
+ def filter_output(self, content):
+ """
+ Passes the concatenated content to the 'output' methods
+ of the compressor filters.
+ """
+ return self.filter(content, self.cached_filters, method=METHOD_OUTPUT)
+
+ def filter_input(self, forced=False):
+ """
+ Passes each hunk (file or code) to the 'input' methods
+ of the compressor filters.
+ """
+ content = []
+ for hunk in self.hunks(forced):
+ content.append(hunk)
+ return content
+
+ 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)
+ mimetype = attrs.get("type", None)
+ if mimetype is None:
+ return False, content
+
+ filter_or_command = self.precompiler_mimetypes.get(mimetype)
+ if filter_or_command is None:
+ if mimetype in self.output_mimetypes:
+ return False, content
+ raise CompressorError("Couldn't find any precompiler in "
+ "COMPRESS_PRECOMPILERS setting for "
+ "mimetype '%s'." % mimetype)
+
+ mod_name, cls_name = get_mod_func(filter_or_command)
+ try:
+ mod = import_module(mod_name)
+ except (ImportError, TypeError):
+ filter = CachedCompilerFilter(
+ content=content, filter_type=self.resource_kind, filename=filename,
+ charset=charset, command=filter_or_command, mimetype=mimetype)
+ return True, filter.input(**kwargs)
+ try:
+ precompiler_class = getattr(mod, cls_name)
+ except AttributeError:
+ raise FilterDoesNotExist('Could not find "%s".' % filter_or_command)
+ filter = precompiler_class(
+ content, attrs=attrs, filter_type=self.resource_kind, charset=charset,
+ filename=filename)
+ return True, filter.input(**kwargs)
+
+ def filter(self, content, filters, method, **kwargs):
+ for filter_cls in filters:
+ filter_func = getattr(
+ filter_cls(content, filter_type=self.resource_kind), method)
+ try:
+ if callable(filter_func):
+ content = filter_func(**kwargs)
+ except NotImplementedError:
+ pass
+ return content
+
+ def output(self, mode='file', forced=False, basename=None):
+ """
+ The general output method, override in subclass if you need to do
+ any custom modification. Calls other mode specific methods or simply
+ returns the content directly.
+ """
+ output = '\n'.join(self.filter_input(forced))
+
+ if not output:
+ return ''
+
+ if settings.COMPRESS_ENABLED or forced:
+ filtered_output = self.filter_output(output)
+ return self.handle_output(mode, filtered_output, forced, basename)
+
+ return output
+
+ def handle_output(self, mode, content, forced, basename=None):
+ # Then check for the appropriate output method and call it
+ output_func = getattr(self, "output_%s" % mode, None)
+ if callable(output_func):
+ return output_func(mode, content, forced, basename)
+ # Total failure, raise a general exception
+ raise CompressorError(
+ "Couldn't find output method for mode '%s'" % mode)
+
+ def output_file(self, mode, content, forced=False, basename=None):
+ """
+ The output method that saves the content to a file and renders
+ the appropriate template with the file's URL.
+ """
+ new_filepath = self.get_filepath(content, basename=basename)
+ if not self.storage.exists(new_filepath) or forced:
+ 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})
+
+ def output_inline(self, mode, content, forced=False, basename=None):
+ """
+ The output method that directly returns the content for inline
+ display.
+ """
+ return self.render_output(mode, {"content": content})
+
+ def output_preload(self, mode, content, forced=False, basename=None):
+ """
+ The output method that returns <link> with rel="preload" and
+ proper href attribute for given file.
+ """
+ return self.output_file(mode, content, forced, basename)
+
+ def render_output(self, mode, context=None):
+ """
+ Renders the compressor output with the appropriate template for
+ the given mode and template context.
+ """
+ # Just in case someone renders the compressor outside
+ # the usual template rendering cycle
+ if 'compressed' not in self.context:
+ self.context['compressed'] = {}
+
+ self.context['compressed'].update(context or {})
+ self.context['compressed'].update(self.extra_context)
+
+ if hasattr(self.context, 'flatten'):
+ # Passing Contexts to Template.render is deprecated since Django 1.8.
+ final_context = self.context.flatten()
+ else:
+ final_context = self.context
+
+ post_compress.send(sender=self.__class__, type=self.resource_kind,
+ mode=mode, context=final_context)
+ template_name = self.get_template_name(mode)
+ return render_to_string(template_name, context=final_context)
diff --git a/compressor/base_BASE_20651.py b/compressor/base_BASE_20651.py
new file mode 100644
index 0000000..a2529e6
--- /dev/null
+++ b/compressor/base_BASE_20651.py
@@ -0,0 +1,365 @@
+from __future__ import with_statement, unicode_literals
+import os
+import codecs
+from importlib import import_module
+
+from django import VERSION
+from django.core.files.base import ContentFile
+from django.utils import six
+from django.utils.safestring import mark_safe
+from django.utils.six.moves.urllib.request import url2pathname
+from django.template.loader import render_to_string
+from django.utils.functional import cached_property
+
+from compressor.cache import get_hexdigest, get_mtime
+from compressor.conf import settings
+from compressor.exceptions import (CompressorError, UncompressableFileError,
+ FilterDoesNotExist)
+from compressor.filters import CachedCompilerFilter
+from compressor.filters.css_default import CssAbsoluteFilter, CssRelativeFilter
+from compressor.storage import compressor_file_storage
+from compressor.signals import post_compress
+from compressor.utils import get_class, get_mod_func, staticfiles
+
+# Some constants for nicer handling.
+SOURCE_HUNK, SOURCE_FILE = 'inline', 'file'
+METHOD_INPUT, METHOD_OUTPUT = 'input', 'output'
+
+
+class Compressor(object):
+ """
+ Base compressor object to be subclassed for content type
+ depending implementations details.
+ """
+
+ def __init__(self, content=None, output_prefix=None,
+ context=None, filters=None, *args, **kwargs):
+ self.content = content or "" # rendered contents of {% compress %} tag
+ self.output_prefix = output_prefix or "compressed"
+ self.output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
+ self.charset = settings.DEFAULT_CHARSET
+ self.split_content = []
+ self.context = context or {}
+ self.type = output_prefix or ""
+ self.filters = filters or []
+ self.extra_context = {}
+ self.precompiler_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):
+ """
+ To be implemented in a subclass, should return an
+ iterable with four values: kind, value, basename, element
+ """
+ raise NotImplementedError
+
+ def get_template_name(self, mode):
+ """
+ Returns the template path for the given mode.
+ """
+ try:
+ template = getattr(self, "template_name_%s" % mode)
+ if template:
+ return template
+ except AttributeError:
+ pass
+ 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:
+ base_url = settings.COMPRESS_URL
+
+ # Cast ``base_url`` to a string to allow it to be
+ # a string-alike object to e.g. add ``SCRIPT_NAME``
+ # WSGI param as a *path prefix* to the output URL.
+ # See https://code.djangoproject.com/ticket/25598.
+ base_url = six.text_type(base_url)
+
+ if not url.startswith(base_url):
+ raise UncompressableFileError("'%s' isn't accessible via "
+ "COMPRESS_URL ('%s') and can't be "
+ "compressed" % (url, base_url))
+ basename = url.replace(base_url, "", 1)
+ # drop the querystring, which is used for non-compressed cache-busting.
+ 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/58a8c0714e59.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.58a8c0714e59.css'
+ """
+ parts = []
+ if basename:
+ filename = os.path.split(basename)[1]
+ parts.append(os.path.splitext(filename)[0])
+ parts.extend([get_hexdigest(content, 12), self.type])
+ 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 using the storage class.
+ # This is skipped in DEBUG mode as files might be outdated in
+ # compressor's final destination (COMPRESS_ROOT) during development
+ if not settings.DEBUG:
+ try:
+ # call path first so remote storages don't make it to exists,
+ # which would cause network I/O
+ filename = self.storage.path(basename)
+ if not self.storage.exists(basename):
+ filename = None
+ except NotImplementedError:
+ # remote storages don't implement path, access the file locally
+ if compressor_file_storage.exists(basename):
+ filename = compressor_file_storage.path(basename)
+ # secondly try to find it with staticfiles
+ if not filename and self.finders:
+ filename = self.finders.find(url2pathname(basename))
+ if filename:
+ return filename
+ # or just raise an exception as the last resort
+ raise UncompressableFileError(
+ "'%s' could not be found in the COMPRESS_ROOT '%s'%s" %
+ (basename, settings.COMPRESS_ROOT,
+ self.finders and " or with staticfiles." or "."))
+
+ def get_filecontent(self, filename, charset):
+ """
+ Reads file contents using given `charset` and returns it as text.
+ """
+ if charset == 'utf-8':
+ # Removes BOM
+ charset = 'utf-8-sig'
+ with codecs.open(filename, 'r', charset) as fd:
+ try:
+ return fd.read()
+ except IOError as e:
+ raise UncompressableFileError("IOError while processing "
+ "'%s': %s" % (filename, e))
+ except UnicodeDecodeError as e:
+ raise UncompressableFileError("UnicodeDecodeError while "
+ "processing '%s' with "
+ "charset %s: %s" %
+ (filename, charset, e))
+
+ @cached_property
+ def parser(self):
+ return get_class(settings.COMPRESS_PARSER)(self.content)
+
+ @cached_property
+ def cached_filters(self):
+ return [get_class(filter_cls) for filter_cls in self.filters]
+
+ @cached_property
+ def mtimes(self):
+ return [str(get_mtime(value))
+ for kind, value, basename, elem in self.split_contents()
+ if kind == SOURCE_FILE]
+
+ @cached_property
+ def cachekey(self):
+ return get_hexdigest(''.join(
+ [self.content] + self.mtimes).encode(self.charset), 12)
+
+ def hunks(self, forced=False):
+ """
+ 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.
+ """
+ enabled = settings.COMPRESS_ENABLED or forced
+
+ for kind, value, basename, elem in self.split_contents():
+ precompiled = False
+ attribs = self.parser.elem_attribs(elem)
+ charset = attribs.get("charset", self.charset)
+ options = {
+ 'method': METHOD_INPUT,
+ 'elem': elem,
+ 'kind': kind,
+ 'basename': basename,
+ 'charset': charset,
+ }
+
+ if kind == SOURCE_FILE:
+ options = dict(options, filename=value)
+ value = self.get_filecontent(value, charset)
+
+ if self.precompiler_mimetypes:
+ precompiled, value = self.precompile(value, **options)
+
+ if enabled:
+ yield self.filter(value, self.cached_filters, **options)
+ elif precompiled:
+ # since precompiling moves files around, it breaks url()
+ # statements in css files. therefore we run the absolute filter
+ # on precompiled css files even if compression is disabled.
+ if CssAbsoluteFilter in self.cached_filters:
+ value = self.filter(value, [CssAbsoluteFilter], **options)
+ elif CssRelativeFilter in self.cached_filters:
+ value = self.filter(value, [CssRelativeFilter], **options)
+ yield self.handle_output(kind, value, forced=True,
+ basename=basename)
+ else:
+ yield self.parser.elem_str(elem)
+
+ def filter_output(self, content):
+ """
+ Passes the concatenated content to the 'output' methods
+ of the compressor filters.
+ """
+ return self.filter(content, self.cached_filters, method=METHOD_OUTPUT)
+
+ def filter_input(self, forced=False):
+ """
+ Passes each hunk (file or code) to the 'input' methods
+ of the compressor filters.
+ """
+ content = []
+ for hunk in self.hunks(forced):
+ content.append(hunk)
+ return content
+
+ 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)
+ mimetype = attrs.get("type", None)
+ if mimetype is None:
+ return False, content
+
+ filter_or_command = self.precompiler_mimetypes.get(mimetype)
+ if filter_or_command is None:
+ if mimetype in ("text/css", "text/javascript"):
+ return False, content
+ raise CompressorError("Couldn't find any precompiler in "
+ "COMPRESS_PRECOMPILERS setting for "
+ "mimetype '%s'." % mimetype)
+
+ mod_name, cls_name = get_mod_func(filter_or_command)
+ try:
+ mod = import_module(mod_name)
+ except (ImportError, TypeError):
+ filter = CachedCompilerFilter(
+ content=content, filter_type=self.type, filename=filename,
+ charset=charset, command=filter_or_command, mimetype=mimetype)
+ return True, filter.input(**kwargs)
+ try:
+ precompiler_class = getattr(mod, cls_name)
+ except AttributeError:
+ raise FilterDoesNotExist('Could not find "%s".' % filter_or_command)
+ filter = precompiler_class(
+ content, attrs=attrs, filter_type=self.type, charset=charset,
+ filename=filename)
+ return True, filter.input(**kwargs)
+
+ def filter(self, content, filters, method, **kwargs):
+ for filter_cls in filters:
+ filter_func = getattr(
+ filter_cls(content, filter_type=self.type), method)
+ try:
+ if callable(filter_func):
+ content = filter_func(**kwargs)
+ except NotImplementedError:
+ pass
+ return content
+
+ def output(self, mode='file', forced=False, basename=None):
+ """
+ The general output method, override in subclass if you need to do
+ any custom modification. Calls other mode specific methods or simply
+ returns the content directly.
+ """
+ output = '\n'.join(self.filter_input(forced))
+
+ if not output:
+ return ''
+
+ if settings.COMPRESS_ENABLED or forced:
+ filtered_output = self.filter_output(output)
+ return self.handle_output(mode, filtered_output, forced, basename)
+
+ return output
+
+ def handle_output(self, mode, content, forced, basename=None):
+ # Then check for the appropriate output method and call it
+ output_func = getattr(self, "output_%s" % mode, None)
+ if callable(output_func):
+ return output_func(mode, content, forced, basename)
+ # Total failure, raise a general exception
+ raise CompressorError(
+ "Couldn't find output method for mode '%s'" % mode)
+
+ def output_file(self, mode, content, forced=False, basename=None):
+ """
+ The output method that saves the content to a file and renders
+ the appropriate template with the file's URL.
+ """
+ new_filepath = self.get_filepath(content, basename=basename)
+ if not self.storage.exists(new_filepath) or forced:
+ 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})
+
+ def output_inline(self, mode, content, forced=False, basename=None):
+ """
+ The output method that directly returns the content for inline
+ display.
+ """
+ return self.render_output(mode, {"content": content})
+
+ def render_output(self, mode, context=None):
+ """
+ Renders the compressor output with the appropriate template for
+ the given mode and template context.
+ """
+ # Just in case someone renders the compressor outside
+ # the usual template rendering cycle
+ if 'compressed' not in self.context:
+ self.context['compressed'] = {}
+
+ self.context['compressed'].update(context or {})
+ self.context['compressed'].update(self.extra_context)
+
+ if hasattr(self.context, 'flatten') and VERSION >= (1, 9):
+ # Passing Contexts to Template.render is deprecated since Django 1.8.
+ # However, we use the fix below only for django 1.9 and above, since
+ # the flatten method is buggy in 1.8, see https://code.djangoproject.com/ticket/24765
+ final_context = self.context.flatten()
+ else:
+ final_context = self.context
+
+ post_compress.send(sender=self.__class__, type=self.type,
+ mode=mode, context=final_context)
+ template_name = self.get_template_name(mode)
+ return render_to_string(template_name, context=final_context)
diff --git a/compressor/base_LOCAL_20651.py b/compressor/base_LOCAL_20651.py
new file mode 100644
index 0000000..7200479
--- /dev/null
+++ b/compressor/base_LOCAL_20651.py
@@ -0,0 +1,374 @@
+from __future__ import with_statement, unicode_literals
+import os
+import codecs
+from importlib import import_module
+
+from django.core.files.base import ContentFile
+from django.utils import six
+from django.utils.safestring import mark_safe
+from django.utils.six.moves.urllib.request import url2pathname
+from django.template.loader import render_to_string
+from django.utils.functional import cached_property
+
+from compressor.cache import get_hexdigest, get_mtime
+from compressor.conf import settings
+from compressor.exceptions import (CompressorError, UncompressableFileError,
+ FilterDoesNotExist)
+from compressor.filters import CachedCompilerFilter
+from compressor.storage import compressor_file_storage
+from compressor.signals import post_compress
+from compressor.utils import get_class, get_mod_func, staticfiles
+
+# Some constants for nicer handling.
+SOURCE_HUNK, SOURCE_FILE = 'inline', 'file'
+METHOD_INPUT, METHOD_OUTPUT = 'input', 'output'
+
+
+class Compressor(object):
+ """
+ Base compressor object to be subclassed for content type
+ depending implementations details.
+ """
+
+ output_mimetypes = {}
+
+ def __init__(self, resource_kind, content=None, output_prefix=None,
+ context=None, filters=None, *args, **kwargs):
+ if filters is None:
+ self.filters = settings.COMPRESS_FILTERS[resource_kind]
+ else:
+ self.filters = filters
+ if output_prefix is None:
+ self.output_prefix = resource_kind
+ else:
+ self.output_prefix = output_prefix
+ self.content = content or "" # rendered contents of {% compress %} tag
+ self.output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
+ self.charset = settings.DEFAULT_CHARSET
+ self.split_content = []
+ self.context = context or {}
+ self.resource_kind = resource_kind
+ self.extra_context = {}
+ self.precompiler_mimetypes = dict(settings.COMPRESS_PRECOMPILERS)
+ self.finders = staticfiles.finders
+ self._storage = None
+
+ def copy(self, **kwargs):
+ keywords = dict(
+ content=self.content,
+ context=self.context,
+ output_prefix=self.output_prefix,
+ filters=self.filters)
+ keywords.update(kwargs)
+ return self.__class__(self.resource_kind, **keywords)
+
+ @cached_property
+ def storage(self):
+ from compressor.storage import default_storage
+ return default_storage
+
+ def split_contents(self):
+ """
+ To be implemented in a subclass, should return an
+ iterable with four values: kind, value, basename, element
+ """
+ raise NotImplementedError
+
+ def get_template_name(self, mode):
+ """
+ Returns the template path for the given mode.
+ """
+ try:
+ template = getattr(self, "template_name_%s" % mode)
+ if template:
+ return template
+ except AttributeError:
+ pass
+ return "compressor/%s_%s.html" % (self.resource_kind, 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:
+ base_url = settings.COMPRESS_URL
+
+ # Cast ``base_url`` to a string to allow it to be
+ # a string-alike object to e.g. add ``SCRIPT_NAME``
+ # WSGI param as a *path prefix* to the output URL.
+ # See https://code.djangoproject.com/ticket/25598.
+ base_url = six.text_type(base_url)
+
+ if not url.startswith(base_url):
+ raise UncompressableFileError("'%s' isn't accessible via "
+ "COMPRESS_URL ('%s') and can't be "
+ "compressed" % (url, base_url))
+ basename = url.replace(base_url, "", 1)
+ # drop the querystring, which is used for non-compressed cache-busting.
+ 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/58a8c0714e59.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.58a8c0714e59.css'
+ """
+ parts = []
+ if basename:
+ filename = os.path.split(basename)[1]
+ parts.append(os.path.splitext(filename)[0])
+ parts.extend([get_hexdigest(content, 12), self.resource_kind])
+ 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 using the storage class.
+ # This is skipped in DEBUG mode as files might be outdated in
+ # compressor's final destination (COMPRESS_ROOT) during development
+ if not settings.DEBUG:
+ try:
+ # call path first so remote storages don't make it to exists,
+ # which would cause network I/O
+ filename = self.storage.path(basename)
+ if not self.storage.exists(basename):
+ filename = None
+ except NotImplementedError:
+ # remote storages don't implement path, access the file locally
+ if compressor_file_storage.exists(basename):
+ filename = compressor_file_storage.path(basename)
+ # secondly try to find it with staticfiles
+ if not filename and self.finders:
+ filename = self.finders.find(url2pathname(basename))
+ if filename:
+ return filename
+ # or just raise an exception as the last resort
+ raise UncompressableFileError(
+ "'%s' could not be found in the COMPRESS_ROOT '%s'%s" %
+ (basename, settings.COMPRESS_ROOT,
+ self.finders and " or with staticfiles." or "."))
+
+ def get_filecontent(self, filename, charset):
+ """
+ Reads file contents using given `charset` and returns it as text.
+ """
+ if charset == 'utf-8':
+ # Removes BOM
+ charset = 'utf-8-sig'
+ with codecs.open(filename, 'r', charset) as fd:
+ try:
+ return fd.read()
+ except IOError as e:
+ raise UncompressableFileError("IOError while processing "
+ "'%s': %s" % (filename, e))
+ except UnicodeDecodeError as e:
+ raise UncompressableFileError("UnicodeDecodeError while "
+ "processing '%s' with "
+ "charset %s: %s" %
+ (filename, charset, e))
+
+ @cached_property
+ def parser(self):
+ return get_class(settings.COMPRESS_PARSER)(self.content)
+
+ @cached_property
+ def cached_filters(self):
+ return [get_class(filter_cls) for filter_cls in self.filters]
+
+ @cached_property
+ def mtimes(self):
+ return [str(get_mtime(value))
+ for kind, value, basename, elem in self.split_contents()
+ if kind == SOURCE_FILE]
+
+ @cached_property
+ def cachekey(self):
+ return get_hexdigest(''.join(
+ [self.content] + self.mtimes).encode(self.charset), 12)
+
+ def hunks(self, forced=False):
+ """
+ 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.
+ """
+ enabled = settings.COMPRESS_ENABLED or forced
+
+ for kind, value, basename, elem in self.split_contents():
+ precompiled = False
+ attribs = self.parser.elem_attribs(elem)
+ charset = attribs.get("charset", self.charset)
+ options = {
+ 'method': METHOD_INPUT,
+ 'elem': elem,
+ 'kind': kind,
+ 'basename': basename,
+ 'charset': charset,
+ }
+
+ if kind == SOURCE_FILE:
+ options = dict(options, filename=value)
+ value = self.get_filecontent(value, charset)
+
+ if self.precompiler_mimetypes:
+ precompiled, value = self.precompile(value, **options)
+
+ if enabled:
+ yield self.filter(value, self.cached_filters, **options)
+ elif precompiled:
+ for filter_cls in self.cached_filters:
+ if filter_cls.run_with_compression_disabled:
+ value = self.filter(value, [filter_cls], **options)
+ yield self.handle_output(kind, value, forced=True,
+ basename=basename)
+ else:
+ yield self.parser.elem_str(elem)
+
+ def filter_output(self, content):
+ """
+ Passes the concatenated content to the 'output' methods
+ of the compressor filters.
+ """
+ return self.filter(content, self.cached_filters, method=METHOD_OUTPUT)
+
+ def filter_input(self, forced=False):
+ """
+ Passes each hunk (file or code) to the 'input' methods
+ of the compressor filters.
+ """
+ content = []
+ for hunk in self.hunks(forced):
+ content.append(hunk)
+ return content
+
+ 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)
+ mimetype = attrs.get("type", None)
+ if mimetype is None:
+ return False, content
+
+ filter_or_command = self.precompiler_mimetypes.get(mimetype)
+ if filter_or_command is None:
+ if mimetype in self.output_mimetypes:
+ return False, content
+ raise CompressorError("Couldn't find any precompiler in "
+ "COMPRESS_PRECOMPILERS setting for "
+ "mimetype '%s'." % mimetype)
+
+ mod_name, cls_name = get_mod_func(filter_or_command)
+ try:
+ mod = import_module(mod_name)
+ except (ImportError, TypeError):
+ filter = CachedCompilerFilter(
+ content=content, filter_type=self.resource_kind, filename=filename,
+ charset=charset, command=filter_or_command, mimetype=mimetype)
+ return True, filter.input(**kwargs)
+ try:
+ precompiler_class = getattr(mod, cls_name)
+ except AttributeError:
+ raise FilterDoesNotExist('Could not find "%s".' % filter_or_command)
+ filter = precompiler_class(
+ content, attrs=attrs, filter_type=self.resource_kind, charset=charset,
+ filename=filename)
+ return True, filter.input(**kwargs)
+
+ def filter(self, content, filters, method, **kwargs):
+ for filter_cls in filters:
+ filter_func = getattr(
+ filter_cls(content, filter_type=self.resource_kind), method)
+ try:
+ if callable(filter_func):
+ content = filter_func(**kwargs)
+ except NotImplementedError:
+ pass
+ return content
+
+ def output(self, mode='file', forced=False, basename=None):
+ """
+ The general output method, override in subclass if you need to do
+ any custom modification. Calls other mode specific methods or simply
+ returns the content directly.
+ """
+ output = '\n'.join(self.filter_input(forced))
+
+ if not output:
+ return ''
+
+ if settings.COMPRESS_ENABLED or forced:
+ filtered_output = self.filter_output(output)
+ return self.handle_output(mode, filtered_output, forced, basename)
+
+ return output
+
+ def handle_output(self, mode, content, forced, basename=None):
+ # Then check for the appropriate output method and call it
+ output_func = getattr(self, "output_%s" % mode, None)
+ if callable(output_func):
+ return output_func(mode, content, forced, basename)
+ # Total failure, raise a general exception
+ raise CompressorError(
+ "Couldn't find output method for mode '%s'" % mode)
+
+ def output_file(self, mode, content, forced=False, basename=None):
+ """
+ The output method that saves the content to a file and renders
+ the appropriate template with the file's URL.
+ """
+ new_filepath = self.get_filepath(content, basename=basename)
+ if not self.storage.exists(new_filepath) or forced:
+ 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})
+
+ def output_inline(self, mode, content, forced=False, basename=None):
+ """
+ The output method that directly returns the content for inline
+ display.
+ """
+ return self.render_output(mode, {"content": content})
+
+ def render_output(self, mode, context=None):
+ """
+ Renders the compressor output with the appropriate template for
+ the given mode and template context.
+ """
+ # Just in case someone renders the compressor outside
+ # the usual template rendering cycle
+ if 'compressed' not in self.context:
+ self.context['compressed'] = {}
+
+ self.context['compressed'].update(context or {})
+ self.context['compressed'].update(self.extra_context)
+
+ if hasattr(self.context, 'flatten'):
+ # Passing Contexts to Template.render is deprecated since Django 1.8.
+ final_context = self.context.flatten()
+ else:
+ final_context = self.context
+
+ post_compress.send(sender=self.__class__, type=self.resource_kind,
+ mode=mode, context=final_context)
+ template_name = self.get_template_name(mode)
+ return render_to_string(template_name, context=final_context)
diff --git a/compressor/base_REMOTE_20651.py b/compressor/base_REMOTE_20651.py
new file mode 100644
index 0000000..9d79dab
--- /dev/null
+++ b/compressor/base_REMOTE_20651.py
@@ -0,0 +1,381 @@
+from __future__ import with_statement, unicode_literals
+import os
+import codecs
+from importlib import import_module
+
+import six
+from django.core.files.base import ContentFile
+from django.utils.safestring import mark_safe
+from django.template.loader import render_to_string
+from django.utils.functional import cached_property
+from six.moves.urllib.request 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 CachedCompilerFilter
+from compressor.storage import compressor_file_storage
+from compressor.signals import post_compress
+from compressor.utils import get_class, get_mod_func, staticfiles
+
+# Some constants for nicer handling.
+SOURCE_HUNK, SOURCE_FILE = 'inline', 'file'
+METHOD_INPUT, METHOD_OUTPUT = 'input', 'output'
+
+
+class Compressor(object):
+ """
+ Base compressor object to be subclassed for content type
+ depending implementations details.
+ """
+
+ output_mimetypes = {}
+
+ def __init__(self, resource_kind, content=None, output_prefix=None,
+ context=None, filters=None, *args, **kwargs):
+ if filters is None:
+ self.filters = settings.COMPRESS_FILTERS[resource_kind]
+ else:
+ self.filters = filters
+ if output_prefix is None:
+ self.output_prefix = resource_kind
+ else:
+ self.output_prefix = output_prefix
+ self.content = content or "" # rendered contents of {% compress %} tag
+ self.output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
+ self.charset = settings.DEFAULT_CHARSET
+ self.split_content = []
+ self.context = context or {}
+ self.resource_kind = resource_kind
+ self.extra_context = {}
+ self.precompiler_mimetypes = dict(settings.COMPRESS_PRECOMPILERS)
+ self.finders = staticfiles.finders
+ self._storage = None
+
+ def copy(self, **kwargs):
+ keywords = dict(
+ content=self.content,
+ context=self.context,
+ output_prefix=self.output_prefix,
+ filters=self.filters)
+ keywords.update(kwargs)
+ return self.__class__(self.resource_kind, **keywords)
+
+ @cached_property
+ def storage(self):
+ from compressor.storage import default_storage
+ return default_storage
+
+ def split_contents(self):
+ """
+ To be implemented in a subclass, should return an
+ iterable with four values: kind, value, basename, element
+ """
+ raise NotImplementedError
+
+ def get_template_name(self, mode):
+ """
+ Returns the template path for the given mode.
+ """
+ try:
+ template = getattr(self, "template_name_%s" % mode)
+ if template:
+ return template
+ except AttributeError:
+ pass
+ return "compressor/%s_%s.html" % (self.resource_kind, 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:
+ base_url = settings.COMPRESS_URL
+
+ # Cast ``base_url`` to a string to allow it to be
+ # a string-alike object to e.g. add ``SCRIPT_NAME``
+ # WSGI param as a *path prefix* to the output URL.
+ # See https://code.djangoproject.com/ticket/25598.
+ base_url = six.text_type(base_url)
+
+ if not url.startswith(base_url):
+ raise UncompressableFileError("'%s' isn't accessible via "
+ "COMPRESS_URL ('%s') and can't be "
+ "compressed" % (url, base_url))
+ basename = url.replace(base_url, "", 1)
+ # drop the querystring, which is used for non-compressed cache-busting.
+ 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/58a8c0714e59.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.58a8c0714e59.css'
+ """
+ parts = []
+ if basename:
+ filename = os.path.split(basename)[1]
+ parts.append(os.path.splitext(filename)[0])
+ parts.extend([get_hexdigest(content, 12), self.resource_kind])
+ 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 using the storage class.
+ # This is skipped in DEBUG mode as files might be outdated in
+ # compressor's final destination (COMPRESS_ROOT) during development
+ if not settings.DEBUG:
+ try:
+ # call path first so remote storages don't make it to exists,
+ # which would cause network I/O
+ filename = self.storage.path(basename)
+ if not self.storage.exists(basename):
+ filename = None
+ except NotImplementedError:
+ # remote storages don't implement path, access the file locally
+ if compressor_file_storage.exists(basename):
+ filename = compressor_file_storage.path(basename)
+ # secondly try to find it with staticfiles
+ if not filename and self.finders:
+ filename = self.finders.find(url2pathname(basename))
+ if filename:
+ return filename
+ # or just raise an exception as the last resort
+ raise UncompressableFileError(
+ "'%s' could not be found in the COMPRESS_ROOT '%s'%s" %
+ (basename, settings.COMPRESS_ROOT,
+ self.finders and " or with staticfiles." or "."))
+
+ def get_filecontent(self, filename, charset):
+ """
+ Reads file contents using given `charset` and returns it as text.
+ """
+ if charset == 'utf-8':
+ # Removes BOM
+ charset = 'utf-8-sig'
+ with codecs.open(filename, 'r', charset) as fd:
+ try:
+ return fd.read()
+ except IOError as e:
+ raise UncompressableFileError("IOError while processing "
+ "'%s': %s" % (filename, e))
+ except UnicodeDecodeError as e:
+ raise UncompressableFileError("UnicodeDecodeError while "
+ "processing '%s' with "
+ "charset %s: %s" %
+ (filename, charset, e))
+
+ @cached_property
+ def parser(self):
+ return get_class(settings.COMPRESS_PARSER)(self.content)
+
+ @cached_property
+ def cached_filters(self):
+ return [get_class(filter_cls) for filter_cls in self.filters]
+
+ @cached_property
+ def mtimes(self):
+ return [str(get_mtime(value))
+ for kind, value, basename, elem in self.split_contents()
+ if kind == SOURCE_FILE]
+
+ @cached_property
+ def cachekey(self):
+ return get_hexdigest(''.join(
+ [self.content] + self.mtimes).encode(self.charset), 12)
+
+ def hunks(self, forced=False):
+ """
+ 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.
+ """
+ enabled = settings.COMPRESS_ENABLED or forced
+
+ for kind, value, basename, elem in self.split_contents():
+ precompiled = False
+ attribs = self.parser.elem_attribs(elem)
+ charset = attribs.get("charset", self.charset)
+ options = {
+ 'method': METHOD_INPUT,
+ 'elem': elem,
+ 'kind': kind,
+ 'basename': basename,
+ 'charset': charset,
+ }
+
+ if kind == SOURCE_FILE:
+ options = dict(options, filename=value)
+ value = self.get_filecontent(value, charset)
+
+ if self.precompiler_mimetypes:
+ precompiled, value = self.precompile(value, **options)
+
+ if enabled:
+ yield self.filter(value, self.cached_filters, **options)
+ elif precompiled:
+ for filter_cls in self.cached_filters:
+ if filter_cls.run_with_compression_disabled:
+ value = self.filter(value, [filter_cls], **options)
+ yield self.handle_output(kind, value, forced=True,
+ basename=basename)
+ else:
+ yield self.parser.elem_str(elem)
+
+ def filter_output(self, content):
+ """
+ Passes the concatenated content to the 'output' methods
+ of the compressor filters.
+ """
+ return self.filter(content, self.cached_filters, method=METHOD_OUTPUT)
+
+ def filter_input(self, forced=False):
+ """
+ Passes each hunk (file or code) to the 'input' methods
+ of the compressor filters.
+ """
+ content = []
+ for hunk in self.hunks(forced):
+ content.append(hunk)
+ return content
+
+ 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)
+ mimetype = attrs.get("type", None)
+ if mimetype is None:
+ return False, content
+
+ filter_or_command = self.precompiler_mimetypes.get(mimetype)
+ if filter_or_command is None:
+ if mimetype in self.output_mimetypes:
+ return False, content
+ raise CompressorError("Couldn't find any precompiler in "
+ "COMPRESS_PRECOMPILERS setting for "
+ "mimetype '%s'." % mimetype)
+
+ mod_name, cls_name = get_mod_func(filter_or_command)
+ try:
+ mod = import_module(mod_name)
+ except (ImportError, TypeError):
+ filter = CachedCompilerFilter(
+ content=content, filter_type=self.resource_kind, filename=filename,
+ charset=charset, command=filter_or_command, mimetype=mimetype)
+ return True, filter.input(**kwargs)
+ try:
+ precompiler_class = getattr(mod, cls_name)
+ except AttributeError:
+ raise FilterDoesNotExist('Could not find "%s".' % filter_or_command)
+ filter = precompiler_class(
+ content, attrs=attrs, filter_type=self.resource_kind, charset=charset,
+ filename=filename)
+ return True, filter.input(**kwargs)
+
+ def filter(self, content, filters, method, **kwargs):
+ for filter_cls in filters:
+ filter_func = getattr(
+ filter_cls(content, filter_type=self.resource_kind), method)
+ try:
+ if callable(filter_func):
+ content = filter_func(**kwargs)
+ except NotImplementedError:
+ pass
+ return content
+
+ def output(self, mode='file', forced=False, basename=None):
+ """
+ The general output method, override in subclass if you need to do
+ any custom modification. Calls other mode specific methods or simply
+ returns the content directly.
+ """
+ output = '\n'.join(self.filter_input(forced))
+
+ if not output:
+ return ''
+
+ if settings.COMPRESS_ENABLED or forced:
+ filtered_output = self.filter_output(output)
+ return self.handle_output(mode, filtered_output, forced, basename)
+
+ return output
+
+ def handle_output(self, mode, content, forced, basename=None):
+ # Then check for the appropriate output method and call it
+ output_func = getattr(self, "output_%s" % mode, None)
+ if callable(output_func):
+ return output_func(mode, content, forced, basename)
+ # Total failure, raise a general exception
+ raise CompressorError(
+ "Couldn't find output method for mode '%s'" % mode)
+
+ def output_file(self, mode, content, forced=False, basename=None):
+ """
+ The output method that saves the content to a file and renders
+ the appropriate template with the file's URL.
+ """
+ new_filepath = self.get_filepath(content, basename=basename)
+ if not self.storage.exists(new_filepath) or forced:
+ 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})
+
+ def output_inline(self, mode, content, forced=False, basename=None):
+ """
+ The output method that directly returns the content for inline
+ display.
+ """
+ return self.render_output(mode, {"content": content})
+
+ def output_preload(self, mode, content, forced=False, basename=None):
+ """
+ The output method that returns <link> with rel="preload" and
+ proper href attribute for given file.
+ """
+ return self.output_file(mode, content, forced, basename)
+
+ def render_output(self, mode, context=None):
+ """
+ Renders the compressor output with the appropriate template for
+ the given mode and template context.
+ """
+ # Just in case someone renders the compressor outside
+ # the usual template rendering cycle
+ if 'compressed' not in self.context:
+ self.context['compressed'] = {}
+
+ self.context['compressed'].update(context or {})
+ self.context['compressed'].update(self.extra_context)
+
+ if hasattr(self.context, 'flatten'):
+ # Passing Contexts to Template.render is deprecated since Django 1.8.
+ final_context = self.context.flatten()
+ else:
+ final_context = self.context
+
+ post_compress.send(sender=self.__class__, type=self.resource_kind,
+ mode=mode, context=final_context)
+ template_name = self.get_template_name(mode)
+ return render_to_string(template_name, context=final_context)
diff --git a/compressor/cache.py b/compressor/cache.py
index 4a05702..c3ee2ba 100644
--- a/compressor/cache.py
+++ b/compressor/cache.py
@@ -5,9 +5,9 @@ import socket
import time
from importlib import import_module
+import six
from django.core.cache import caches
from django.core.files.base import ContentFile
-from django.utils import six
from django.utils.encoding import force_text, smart_bytes
from django.utils.functional import SimpleLazyObject
diff --git a/compressor/contrib/jinja2ext.py b/compressor/contrib/jinja2ext.py
index 63c2495..c519ceb 100644
--- a/compressor/contrib/jinja2ext.py
+++ b/compressor/contrib/jinja2ext.py
@@ -49,7 +49,7 @@ class CompressorExtension(compress.CompressorMixin, Extension):
# The file mode optionally accepts a name
if parser.stream.current.type != 'block_end':
namearg = const(parser.parse_expression())
- elif modearg.value == compress.OUTPUT_INLINE:
+ elif modearg.value == compress.OUTPUT_INLINE or modearg.value == compress.OUTPUT_PRELOAD:
pass
else:
raise TemplateSyntaxError(
diff --git a/compressor/contrib/jinja2ext.py.orig b/compressor/contrib/jinja2ext.py.orig
new file mode 100644
index 0000000..2d2717e
--- /dev/null
+++ b/compressor/contrib/jinja2ext.py.orig
@@ -0,0 +1,90 @@
+from jinja2 import nodes
+from jinja2.ext import Extension
+from jinja2.exceptions import TemplateSyntaxError
+
+from compressor.templatetags import compress
+
+
+# Allow django like definitions which assume constants instead of variables
+def const(node):
+ if isinstance(node, nodes.Name):
+ return nodes.Const(node.name)
+ else:
+ return node
+
+
+class CompressorExtension(compress.CompressorMixin, Extension):
+
+ tags = set(['compress'])
+
+ def parse(self, parser):
+ # Store the first lineno for the actual function call
+ lineno = parser.stream.current.lineno
+ next(parser.stream)
+ args = []
+
+ kindarg = const(parser.parse_expression())
+
+ if kindarg.value in self.compressors:
+ args.append(kindarg)
+ else:
+ raise TemplateSyntaxError(
+ 'Compress kind may be one of: %r, got: %r' % (
+ self.compressors.keys(), kindarg.value),
+ parser.stream.current.lineno)
+
+ # For legacy support, allow for a commma but simply ignore it
+ parser.stream.skip_if('comma')
+
+ # Some sane defaults for file output
+ namearg = nodes.Const(None)
+ modearg = nodes.Const('file')
+
+ # If we're not at the "%}" part yet we must have a output mode argument
+ if parser.stream.current.type != 'block_end':
+ modearg = const(parser.parse_expression())
+ args.append(modearg)
+
+ if modearg.value == compress.OUTPUT_FILE:
+ # The file mode optionally accepts a name
+ if parser.stream.current.type != 'block_end':
+ namearg = const(parser.parse_expression())
+<<<<<<< HEAD
+ elif modearg.value == compress.OUTPUT_INLINE:
+=======
+ elif modearg.value == compress.OUTPUT_INLINE or modearg.value == compress.OUTPUT_PRELOAD:
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+ pass
+ else:
+ raise TemplateSyntaxError(
+ 'Compress mode may be one of: %r, got %r' % (
+ compress.OUTPUT_MODES, modearg.value),
+ parser.stream.current.lineno)
+
+ # Parse everything between the compress and endcompress tags
+ body = parser.parse_statements(['name:endcompress'], drop_needle=True)
+
+ # 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', [kindarg, modearg, namearg]),
+ [], [], body).set_lineno(lineno)
+
+ def _compress_forced(self, kind, mode, name, caller):
+ return self._compress(kind, mode, name, caller, True)
+
+ def _compress_normal(self, kind, mode, name, caller):
+ return self._compress(kind, mode, name, caller, False)
+
+ def _compress(self, kind, mode, name, caller, forced):
+ mode = mode or compress.OUTPUT_FILE
+ original_content = caller()
+ context = {
+ 'original_content': original_content
+ }
+ return self.render_compressed(context, kind, mode, name, forced=forced)
+
+ def get_original_content(self, context):
+ return context['original_content']
diff --git a/compressor/filters/base.py b/compressor/filters/base.py
index e894776..67fcce8 100644
--- a/compressor/filters/base.py
+++ b/compressor/filters/base.py
@@ -20,8 +20,8 @@ else:
from django.core.exceptions import ImproperlyConfigured
from django.core.files.temp import NamedTemporaryFile
+import six
from django.utils.encoding import smart_text
-from django.utils import six
from compressor.cache import cache, get_precompiler_cachekey
diff --git a/compressor/js.py b/compressor/js.py
index 66b9ca1..7fc18f6 100644
--- a/compressor/js.py
+++ b/compressor/js.py
@@ -61,6 +61,6 @@ class JsCompressor(Compressor):
# error like TypeError...
# Forcing a semicolon in between fixes it.
if settings.COMPRESS_ENABLED or forced:
- hunk = ";" + hunk
+ hunk += ";"
content.append(hunk)
return content
diff --git a/compressor/management/commands/compress.py b/compressor/management/commands/compress.py
index 92d63d4..a6039c5 100644
--- a/compressor/management/commands/compress.py
+++ b/compressor/management/commands/compress.py
@@ -8,10 +8,10 @@ from fnmatch import fnmatch
from importlib import import_module
import django
+import six
from django.core.management.base import BaseCommand, CommandError
import django.template
from django.template import Context
-from django.utils import six
from django.utils.encoding import smart_text
from django.template.loader import get_template # noqa Leave this in to preload template locations
from django.template import engines
diff --git a/compressor/parser/__init__.py b/compressor/parser/__init__.py
index a53f2a4..a8d8697 100644
--- a/compressor/parser/__init__.py
+++ b/compressor/parser/__init__.py
@@ -1,6 +1,6 @@
from importlib import import_module
-from django.utils import six
+import six
from django.utils.functional import LazyObject
# support legacy parser module usage
diff --git a/compressor/parser/beautifulsoup.py b/compressor/parser/beautifulsoup.py
index 1068abe..02d7a3d 100644
--- a/compressor/parser/beautifulsoup.py
+++ b/compressor/parser/beautifulsoup.py
@@ -37,4 +37,8 @@ class BeautifulSoupParser(ParserBase):
return elem.name
def elem_str(self, elem):
- return smart_text(elem)
+ elem_as_string = smart_text(elem)
+ if elem.name == 'link':
+ # This makes testcases happy
+ elem_as_string = elem_as_string.replace('/>', '>')
+ return elem_as_string
diff --git a/compressor/parser/default_htmlparser.py b/compressor/parser/default_htmlparser.py
index ac47343..baec0af 100644
--- a/compressor/parser/default_htmlparser.py
+++ b/compressor/parser/default_htmlparser.py
@@ -1,6 +1,6 @@
import sys
-from django.utils import six
+import six
from django.utils.encoding import smart_text
from compressor.exceptions import ParserError
@@ -83,6 +83,6 @@ class DefaultHtmlParser(ParserBase, six.moves.html_parser.HTMLParser):
if len(elem['attrs']):
tag['attrs'] = ' %s' % ' '.join(['%s="%s"' % (name, value) for name, value in elem['attrs']])
if elem['tag'] == 'link':
- return '<%(tag)s%(attrs)s />' % tag
+ return '<%(tag)s%(attrs)s>' % tag
else:
return '<%(tag)s%(attrs)s>%(text)s</%(tag)s>' % tag
diff --git a/compressor/parser/html5lib.py b/compressor/parser/html5lib.py
index 6d476f5..f479237 100644
--- a/compressor/parser/html5lib.py
+++ b/compressor/parser/html5lib.py
@@ -17,7 +17,7 @@ class Html5LibParser(ParserBase):
def _serialize(self, elem):
return self.html5lib.serialize(
elem, tree="etree", quote_attr_values="always",
- omit_optional_tags=False, use_trailing_solidus=True,
+ omit_optional_tags=False,
)
def _find(self, *names):
diff --git a/compressor/parser/html5lib.py.orig b/compressor/parser/html5lib.py.orig
new file mode 100644
index 0000000..48c497e
--- /dev/null
+++ b/compressor/parser/html5lib.py.orig
@@ -0,0 +1,63 @@
+from __future__ import absolute_import
+from django.core.exceptions import ImproperlyConfigured
+from django.utils.encoding import smart_text
+from django.utils.functional import cached_property
+
+from compressor.exceptions import ParserError
+from compressor.parser import ParserBase
+
+
+class Html5LibParser(ParserBase):
+
+ def __init__(self, content):
+ super(Html5LibParser, self).__init__(content)
+ import html5lib
+ self.html5lib = html5lib
+
+ def _serialize(self, elem):
+ return self.html5lib.serialize(
+ elem, tree="etree", quote_attr_values="always",
+<<<<<<< HEAD
+ omit_optional_tags=False, use_trailing_solidus=True,
+=======
+ omit_optional_tags=False,
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+ )
+
+ def _find(self, *names):
+ for elem in self.html:
+ if elem.tag in names:
+ yield elem
+
+ @cached_property
+ def html(self):
+ try:
+ return self.html5lib.parseFragment(self.content, treebuilder="etree")
+ except ImportError as err:
+ raise ImproperlyConfigured("Error while importing html5lib: %s" % err)
+ except Exception as err:
+ raise ParserError("Error while initializing Parser: %s" % err)
+
+ def css_elems(self):
+ return self._find('{http://www.w3.org/1999/xhtml}link',
+ '{http://www.w3.org/1999/xhtml}style')
+
+ def js_elems(self):
+ return self._find('{http://www.w3.org/1999/xhtml}script')
+
+ def elem_attribs(self, elem):
+ return elem.attrib
+
+ def elem_content(self, elem):
+ return smart_text(elem.text)
+
+ def elem_name(self, elem):
+ 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_text(self._serialize(elem))
diff --git a/compressor/parser/lxml.py b/compressor/parser/lxml.py
index 5436de0..3328563 100644
--- a/compressor/parser/lxml.py
+++ b/compressor/parser/lxml.py
@@ -1,7 +1,7 @@
from __future__ import absolute_import, unicode_literals
+import six
from django.core.exceptions import ImproperlyConfigured
-from django.utils import six
from django.utils.encoding import smart_text
from django.utils.functional import cached_property
@@ -73,9 +73,4 @@ class LxmlParser(ParserBase):
return elem.tag
def elem_str(self, elem):
- 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('>', ' />')
- return elem_as_string
+ return smart_text(self.tostring(elem, method='html', encoding=six.text_type))
diff --git a/compressor/templates/compressor/css_file.html b/compressor/templates/compressor/css_file.html
index 2b3a86f..8d629c1 100644
--- a/compressor/templates/compressor/css_file.html
+++ b/compressor/templates/compressor/css_file.html
@@ -1 +1 @@
-<link rel="stylesheet" href="{{ compressed.url }}" type="text/css"{% if compressed.media %} media="{{ compressed.media }}"{% endif %} /> \ No newline at end of file
+<link rel="stylesheet" href="{{ compressed.url }}" type="text/css"{% if compressed.media %} media="{{ compressed.media }}"{% endif %}> \ No newline at end of file
diff --git a/compressor/templates/compressor/css_preload.html b/compressor/templates/compressor/css_preload.html
new file mode 100644
index 0000000..7a6acf8
--- /dev/null
+++ b/compressor/templates/compressor/css_preload.html
@@ -0,0 +1 @@
+<link rel="preload" href="{{ compressed.url }}" as="style" /> \ No newline at end of file
diff --git a/compressor/templates/compressor/js_file.html b/compressor/templates/compressor/js_file.html
index e76d860..bda861e 100644
--- a/compressor/templates/compressor/js_file.html
+++ b/compressor/templates/compressor/js_file.html
@@ -1 +1 @@
-<script type="text/javascript" src="{{ compressed.url }}"{{ compressed.extra }}></script> \ No newline at end of file
+<script src="{{ compressed.url }}"{{ compressed.extra }}></script> \ No newline at end of file
diff --git a/compressor/templates/compressor/js_inline.html b/compressor/templates/compressor/js_inline.html
index 403bec5..b7085a7 100644
--- a/compressor/templates/compressor/js_inline.html
+++ b/compressor/templates/compressor/js_inline.html
@@ -1 +1 @@
-<script type="text/javascript">{{ compressed.content|safe }}</script> \ No newline at end of file
+<script>{{ compressed.content|safe }}</script> \ No newline at end of file
diff --git a/compressor/templates/compressor/js_preload.html b/compressor/templates/compressor/js_preload.html
new file mode 100644
index 0000000..6c78806
--- /dev/null
+++ b/compressor/templates/compressor/js_preload.html
@@ -0,0 +1 @@
+<link rel="preload" href="{{ compressed.url }}" as="script" /> \ No newline at end of file
diff --git a/compressor/templatetags/compress.py b/compressor/templatetags/compress.py
index 7472831..2f13fec 100644
--- a/compressor/templatetags/compress.py
+++ b/compressor/templatetags/compress.py
@@ -1,6 +1,6 @@
+import six
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)
@@ -12,7 +12,8 @@ register = template.Library()
OUTPUT_FILE = 'file'
OUTPUT_INLINE = 'inline'
-OUTPUT_MODES = (OUTPUT_FILE, OUTPUT_INLINE)
+OUTPUT_PRELOAD = 'preload'
+OUTPUT_MODES = (OUTPUT_FILE, OUTPUT_INLINE, OUTPUT_PRELOAD)
class CompressorMixin(object):
diff --git a/compressor/tests/test_base.py b/compressor/tests/test_base.py
index 32bfd28..fcbcf13 100644
--- a/compressor/tests/test_base.py
+++ b/compressor/tests/test_base.py
@@ -1,6 +1,7 @@
from __future__ import with_statement, unicode_literals
import os
import re
+import sys
from tempfile import mkdtemp
from shutil import rmtree, copytree
@@ -25,8 +26,8 @@ def make_soup(markup):
def css_tag(href, **kwargs):
- rendered_attrs = ''.join(['%s="%s" ' % (k, v) for k, v in kwargs.items()])
- template = '<link rel="stylesheet" href="%s" type="text/css" %s/>'
+ rendered_attrs = ''.join([' %s="%s"' % (k, v) for k, v in kwargs.items()])
+ template = '<link rel="stylesheet" href="%s" type="text/css"%s>'
return template % (href, rendered_attrs)
@@ -57,8 +58,9 @@ class PrecompilerAndAbsoluteFilterTestCase(SimpleTestCase):
def setUp(self):
self.html_orig = '<link rel="stylesheet" href="/static/css/relative_url.css" type="text/css" />'
- self.html_link_to_precompiled_css = '<link rel="stylesheet" href="/static/CACHE/css/relative_url.e8602322bfa6.css" type="text/css" />'
- self.html_link_to_absolutized_css = '<link rel="stylesheet" href="/static/CACHE/css/relative_url.376db5682982.css" type="text/css" />'
+ self.html_auto_close_removed = '<link rel="stylesheet" href="/static/css/relative_url.css" type="text/css">'
+ self.html_link_to_precompiled_css = '<link rel="stylesheet" href="/static/CACHE/css/relative_url.e8602322bfa6.css" type="text/css">'
+ self.html_link_to_absolutized_css = '<link rel="stylesheet" href="/static/CACHE/css/relative_url.376db5682982.css" type="text/css">'
self.css_orig = "p { background: url('../img/python.png'); }" # content of relative_url.css
self.css_absolutized = "p { background: url('/static/img/python.png?ccb38978f900'); }"
@@ -81,8 +83,8 @@ class PrecompilerAndAbsoluteFilterTestCase(SimpleTestCase):
in the filters setting.
While at it, ensure that everything runs as expected when compression is enabled.
"""
- self.helper(enabled=False, use_precompiler=False, use_absolute_filter=False, expected_output=self.html_orig)
- self.helper(enabled=False, use_precompiler=False, use_absolute_filter=True, expected_output=self.html_orig)
+ self.helper(enabled=False, use_precompiler=False, use_absolute_filter=False, expected_output=self.html_auto_close_removed)
+ self.helper(enabled=False, use_precompiler=False, use_absolute_filter=True, expected_output=self.html_auto_close_removed)
self.helper(enabled=False, use_precompiler=True, use_absolute_filter=False, expected_output=self.html_link_to_precompiled_css)
self.helper(enabled=False, use_precompiler=True, use_absolute_filter=True, expected_output=self.html_link_to_absolutized_css)
self.helper(enabled=True, use_precompiler=False, use_absolute_filter=False, expected_output=self.css_orig)
@@ -100,9 +102,9 @@ class CompressorTestCase(SimpleTestCase):
def setUp(self):
self.css = """\
-<link rel="stylesheet" href="/static/css/one.css" type="text/css" />
+<link rel="stylesheet" href="/static/css/one.css" type="text/css">
<style type="text/css">p { border:5px solid green;}</style>
-<link rel="stylesheet" href="/static/css/two.css" type="text/css" />"""
+<link rel="stylesheet" href="/static/css/two.css" type="text/css">"""
self.css_node = CssCompressor('css', self.css)
self.js = """\
@@ -133,7 +135,7 @@ class CompressorTestCase(SimpleTestCase):
(
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" />',
+ 'css/one.css', '<link rel="stylesheet" href="/static/css/one.css" type="text/css">',
),
(
SOURCE_HUNK,
@@ -145,7 +147,7 @@ class CompressorTestCase(SimpleTestCase):
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" />',
+ '<link rel="stylesheet" href="/static/css/two.css" type="text/css">',
),
]
split = self.css_node.split_contents()
@@ -188,6 +190,11 @@ class CompressorTestCase(SimpleTestCase):
output = css_tag('/static/CACHE/css/58a8c0714e59.css')
self.assertEqual(output, self.css_node.output().strip())
+ def test_css_preload_output(self):
+ # this needs to have the same hash as in the test above
+ out = '<link rel="preload" href="/static/CACHE/css/58a8c0714e59.css" as="style" />'
+ self.assertEqual(out, self.css_node.output(mode="preload"))
+
def test_js_split(self):
out = [
(
@@ -212,12 +219,17 @@ class CompressorTestCase(SimpleTestCase):
self.assertEqual(out, list(self.js_node.hunks()))
def test_js_output(self):
- out = '<script type="text/javascript" src="/static/CACHE/js/74e158ccb432.js"></script>'
+ out = '<script src="/static/CACHE/js/8a0fed36c317.js"></script>'
self.assertEqual(out, self.js_node.output())
+ def test_js_preload_output(self):
+ # this needs to have the same hash as in the test above
+ out = '<link rel="preload" href="/static/CACHE/js/8a0fed36c317.js" as="script" />'
+ self.assertEqual(out, self.js_node.output(mode="preload"))
+
def test_js_override_url(self):
self.js_node.context.update({'url': 'This is not a url, just a text'})
- out = '<script type="text/javascript" src="/static/CACHE/js/74e158ccb432.js"></script>'
+ out = '<script src="/static/CACHE/js/8a0fed36c317.js"></script>'
self.assertEqual(out, self.js_node.output())
def test_css_override_url(self):
@@ -230,22 +242,22 @@ class CompressorTestCase(SimpleTestCase):
self.assertEqualCollapsed(self.js, self.js_node.output())
def test_js_return_if_on(self):
- output = '<script type="text/javascript" src="/static/CACHE/js/74e158ccb432.js"></script>'
+ output = '<script src="/static/CACHE/js/8a0fed36c317.js"></script>'
self.assertEqual(output, self.js_node.output())
@override_settings(COMPRESS_OUTPUT_DIR='custom')
def test_custom_output_dir1(self):
- output = '<script type="text/javascript" src="/static/custom/js/74e158ccb432.js"></script>'
+ output = '<script src="/static/custom/js/8a0fed36c317.js"></script>'
self.assertEqual(output, JsCompressor('js', self.js).output())
@override_settings(COMPRESS_OUTPUT_DIR='')
def test_custom_output_dir2(self):
- output = '<script type="text/javascript" src="/static/js/74e158ccb432.js"></script>'
+ output = '<script src="/static/js/8a0fed36c317.js"></script>'
self.assertEqual(output, JsCompressor('js', 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/74e158ccb432.js"></script>'
+ output = '<script src="/static/custom/nested/js/8a0fed36c317.js"></script>'
self.assertEqual(output, JsCompressor('js', self.js).output())
@override_settings(COMPRESS_PRECOMPILERS=(
@@ -306,7 +318,7 @@ class CssMediaTestCase(SimpleTestCase):
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')),
+ ('text/foobar', '%s %s {infile} {outfile}' % (sys.executable, os.path.join(test_dir, 'precompiler.py'))),
), COMPRESS_ENABLED=False)
def test_passthough_when_compress_disabled(self):
css = """\
@@ -368,8 +380,8 @@ class JSWithParensTestCase(SimpleTestCase):
js_node = JsCompressor('js', self.js)
content = js_node.filter_input()
- self.assertEqual(content[0], ';obj = {};')
- self.assertEqual(content[1], ';pollos = {}')
+ self.assertEqual(content[0], 'obj = {};;')
+ self.assertEqual(content[1], 'pollos = {};')
class CacheTestCase(SimpleTestCase):
@@ -434,7 +446,7 @@ class CompressorInDebugModeTestCase(SimpleTestCase):
css.write(test_css_content)
# We should generate a link with the hash of the original content, not
# the modified one
- expected = '<link rel="stylesheet" href="/static/CACHE/css/%s.css" type="text/css" />' % hashed
+ expected = '<link rel="stylesheet" href="/static/CACHE/css/%s.css" type="text/css">' % hashed
compressor = CssCompressor('css', self.css)
compressor.storage = DefaultStorage()
output = compressor.output()
diff --git a/compressor/tests/test_base.py.orig b/compressor/tests/test_base.py.orig
new file mode 100644
index 0000000..c751bcb
--- /dev/null
+++ b/compressor/tests/test_base.py.orig
@@ -0,0 +1,477 @@
+from __future__ import with_statement, unicode_literals
+import os
+import re
+import sys
+from tempfile import mkdtemp
+from shutil import rmtree, copytree
+
+from bs4 import BeautifulSoup
+
+from django.core.cache.backends import locmem
+from django.test import SimpleTestCase
+from django.test.utils import override_settings
+
+from compressor import cache as cachemod
+from compressor.base import SOURCE_FILE, SOURCE_HUNK
+from compressor.cache import get_cachekey, get_precompiler_cachekey, get_hexdigest
+from compressor.conf import settings
+from compressor.css import CssCompressor
+from compressor.exceptions import FilterDoesNotExist, FilterError
+from compressor.js import JsCompressor
+from compressor.storage import DefaultStorage
+
+
+def make_soup(markup):
+ return BeautifulSoup(markup, "html.parser")
+
+
+def css_tag(href, **kwargs):
+ rendered_attrs = ''.join([' %s="%s"' % (k, v) for k, v in kwargs.items()])
+ 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,
+ charset=None):
+ pass
+
+ def input(self, **kwargs):
+ return 'OUTPUT'
+
+
+class PassthroughPrecompiler(object):
+ """A filter whose outputs the input unmodified """
+ def __init__(self, content, attrs, filter_type=None, filename=None,
+ charset=None):
+ self.content = content
+
+ def input(self, **kwargs):
+ return self.content
+
+
+test_dir = os.path.abspath(os.path.join(os.path.dirname(__file__)))
+
+
+class PrecompilerAndAbsoluteFilterTestCase(SimpleTestCase):
+
+ def setUp(self):
+ self.html_orig = '<link rel="stylesheet" href="/static/css/relative_url.css" type="text/css" />'
+ self.html_auto_close_removed = '<link rel="stylesheet" href="/static/css/relative_url.css" type="text/css">'
+ self.html_link_to_precompiled_css = '<link rel="stylesheet" href="/static/CACHE/css/relative_url.e8602322bfa6.css" type="text/css">'
+ self.html_link_to_absolutized_css = '<link rel="stylesheet" href="/static/CACHE/css/relative_url.376db5682982.css" type="text/css">'
+ self.css_orig = "p { background: url('../img/python.png'); }" # content of relative_url.css
+ self.css_absolutized = "p { background: url('/static/img/python.png?ccb38978f900'); }"
+
+ def helper(self, enabled, use_precompiler, use_absolute_filter, expected_output):
+ precompiler = (('text/css', 'compressor.tests.test_base.PassthroughPrecompiler'),) if use_precompiler else ()
+ filters = ('compressor.filters.css_default.CssAbsoluteFilter',) if use_absolute_filter else ()
+
+ with self.settings(COMPRESS_ENABLED=enabled,
+ COMPRESS_PRECOMPILERS=precompiler,
+ COMPRESS_FILTERS={'css': filters}):
+ css_node = CssCompressor('css', self.html_orig)
+ output = list(css_node.hunks())[0]
+ self.assertEqual(output, expected_output)
+
+ @override_settings(COMPRESS_CSS_HASHING_METHOD="content")
+ def test_precompiler_enables_absolute(self):
+ """
+ Tests whether specifying a precompiler also runs the CssAbsoluteFilter even if
+ compression is disabled, but only if the CssAbsoluteFilter is actually contained
+ in the filters setting.
+ While at it, ensure that everything runs as expected when compression is enabled.
+ """
+ self.helper(enabled=False, use_precompiler=False, use_absolute_filter=False, expected_output=self.html_auto_close_removed)
+ self.helper(enabled=False, use_precompiler=False, use_absolute_filter=True, expected_output=self.html_auto_close_removed)
+ self.helper(enabled=False, use_precompiler=True, use_absolute_filter=False, expected_output=self.html_link_to_precompiled_css)
+ self.helper(enabled=False, use_precompiler=True, use_absolute_filter=True, expected_output=self.html_link_to_absolutized_css)
+ self.helper(enabled=True, use_precompiler=False, use_absolute_filter=False, expected_output=self.css_orig)
+ self.helper(enabled=True, use_precompiler=False, use_absolute_filter=True, expected_output=self.css_absolutized)
+ self.helper(enabled=True, use_precompiler=True, use_absolute_filter=False, expected_output=self.css_orig)
+ self.helper(enabled=True, use_precompiler=True, use_absolute_filter=True, expected_output=self.css_absolutized)
+
+
+@override_settings(
+ COMPRESS_ENABLED=True,
+ COMPRESS_PRECOMPILERS=(),
+ COMPRESS_DEBUG_TOGGLE='nocompress',
+)
+class CompressorTestCase(SimpleTestCase):
+
+ def setUp(self):
+ self.css = """\
+<link rel="stylesheet" href="/static/css/one.css" type="text/css">
+<style type="text/css">p { border:5px solid green;}</style>
+<<<<<<< HEAD
+<link rel="stylesheet" href="/static/css/two.css" type="text/css" />"""
+=======
+<link rel="stylesheet" href="/static/css/two.css" type="text/css">"""
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+ self.css_node = CssCompressor('css', self.css)
+
+ self.js = """\
+<script src="/static/js/one.js" type="text/javascript"></script>
+<script type="text/javascript">obj.value = "value";</script>"""
+ self.js_node = JsCompressor('js', self.js)
+
+ def assertEqualCollapsed(self, a, b):
+ """
+ assertEqual with internal newlines collapsed to single, and
+ trailing whitespace removed.
+ """
+ def collapse(s):
+ return re.sub(r'\n+', '\n', s).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.
+ """
+ def mangle(split):
+ return [(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, '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.assertEqualSplits(split, out)
+
+ def test_css_hunks(self):
+ 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 = '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)
+
+ def test_css_output_with_bom_input(self):
+ out = 'body { background:#990; }\n.compress-test {color: red;}'
+ css = ("""<link rel="stylesheet" href="/static/css/one.css" type="text/css" />
+ <link rel="stylesheet" href="/static/css/utf-8_with-BOM.css" type="text/css" />""")
+ css_node_with_bom = CssCompressor('css', css)
+ hunks = '\n'.join([h for h in css_node_with_bom.hunks()])
+ self.assertEqual(out, hunks)
+
+ def test_css_mtimes(self):
+ is_date = re.compile(r'^\d{10}[\.\d]+$')
+ for date in self.css_node.mtimes:
+ self.assertTrue(is_date.match(str(float(date))),
+ "mtimes is returning something that doesn't look like a date: %s" % date)
+
+ @override_settings(COMPRESS_ENABLED=False)
+ def test_css_return_if_off(self):
+ self.assertEqualCollapsed(self.css, self.css_node.output())
+
+ def test_cachekey(self):
+ is_cachekey = re.compile(r'\w{12}')
+ self.assertTrue(is_cachekey.match(self.css_node.cachekey),
+ r"cachekey is returning something that doesn't look like r'\w{12}'")
+
+ def test_css_return_if_on(self):
+ output = css_tag('/static/CACHE/css/58a8c0714e59.css')
+ self.assertEqual(output, self.css_node.output().strip())
+
+ def test_css_preload_output(self):
+ # this needs to have the same hash as in the test above
+ out = '<link rel="preload" href="/static/CACHE/css/58a8c0714e59.css" as="style" />'
+ self.assertEqual(out, self.css_node.output(mode="preload"))
+
+ def test_js_split(self):
+ out = [
+ (
+ 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.assertEqualSplits(split, out)
+
+ def test_js_hunks(self):
+ out = ['obj = {};', 'obj.value = "value";']
+ self.assertEqual(out, list(self.js_node.hunks()))
+
+ def test_js_output(self):
+ out = '<script src="/static/CACHE/js/8a0fed36c317.js"></script>'
+ self.assertEqual(out, self.js_node.output())
+
+ def test_js_preload_output(self):
+ # this needs to have the same hash as in the test above
+ out = '<link rel="preload" href="/static/CACHE/js/8a0fed36c317.js" as="script" />'
+ self.assertEqual(out, self.js_node.output(mode="preload"))
+
+ def test_js_override_url(self):
+ self.js_node.context.update({'url': 'This is not a url, just a text'})
+ out = '<script src="/static/CACHE/js/8a0fed36c317.js"></script>'
+ self.assertEqual(out, self.js_node.output())
+
+ def test_css_override_url(self):
+ self.css_node.context.update({'url': 'This is not a url, just a text'})
+ output = css_tag('/static/CACHE/css/58a8c0714e59.css')
+ self.assertEqual(output, self.css_node.output().strip())
+
+ @override_settings(COMPRESS_PRECOMPILERS=(), COMPRESS_ENABLED=False)
+ def test_js_return_if_off(self):
+ self.assertEqualCollapsed(self.js, self.js_node.output())
+
+ def test_js_return_if_on(self):
+ output = '<script src="/static/CACHE/js/8a0fed36c317.js"></script>'
+ self.assertEqual(output, self.js_node.output())
+
+ @override_settings(COMPRESS_OUTPUT_DIR='custom')
+ def test_custom_output_dir1(self):
+<<<<<<< HEAD
+ output = '<script type="text/javascript" src="/static/custom/js/74e158ccb432.js"></script>'
+=======
+ output = '<script src="/static/custom/js/8a0fed36c317.js"></script>'
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+ self.assertEqual(output, JsCompressor('js', self.js).output())
+
+ @override_settings(COMPRESS_OUTPUT_DIR='')
+ def test_custom_output_dir2(self):
+<<<<<<< HEAD
+ output = '<script type="text/javascript" src="/static/js/74e158ccb432.js"></script>'
+=======
+ output = '<script src="/static/js/8a0fed36c317.js"></script>'
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+ self.assertEqual(output, JsCompressor('js', self.js).output())
+
+ @override_settings(COMPRESS_OUTPUT_DIR='/custom/nested/')
+ def test_custom_output_dir3(self):
+<<<<<<< HEAD
+ output = '<script type="text/javascript" src="/static/custom/nested/js/74e158ccb432.js"></script>'
+=======
+ output = '<script src="/static/custom/nested/js/8a0fed36c317.js"></script>'
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+ self.assertEqual(output, JsCompressor('js', self.js).output())
+
+ @override_settings(COMPRESS_PRECOMPILERS=(
+ ('text/foobar', 'compressor.tests.test_base.TestPrecompiler'),
+ ), COMPRESS_ENABLED=True)
+ def test_precompiler_class_used(self):
+ css = '<style type="text/foobar">p { border:10px solid red;}</style>'
+ css_node = CssCompressor('css', 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):
+ css = '<style type="text/foobar">p { border:10px solid red;}</style>'
+ css_node = CssCompressor('css', css)
+ self.assertRaises(FilterDoesNotExist, css_node.output, 'inline')
+
+ @override_settings(COMPRESS_PRECOMPILERS=(
+ ('text/foobar', './foo -I ./bar/baz'),
+ ), COMPRESS_ENABLED=True)
+ def test_command_with_dot_precompiler(self):
+ css = '<style type="text/foobar">p { border:10px solid red;}</style>'
+ css_node = CssCompressor('css', css)
+ self.assertRaises(FilterError, css_node.output, 'inline')
+
+ @override_settings(COMPRESS_PRECOMPILERS=(
+ ('text/django', 'compressor.filters.template.TemplateFilter'),
+ ), COMPRESS_ENABLED=True)
+ def test_template_precompiler(self):
+ css = '<style type="text/django">p { border:10px solid {% if 1 %}green{% else %}red{% endif %};}</style>'
+ css_node = CssCompressor('css', css)
+ output = make_soup(css_node.output('inline'))
+ self.assertEqual(output.text, 'p { border:10px solid green;}')
+
+
+class CssMediaTestCase(SimpleTestCase):
+ def setUp(self):
+ self.css = """\
+<link rel="stylesheet" href="/static/css/one.css" type="text/css" media="screen">
+<style type="text/css" media="print">p { border:5px solid green;}</style>
+<link rel="stylesheet" href="/static/css/two.css" type="text/css" media="all">
+<style type="text/css">h1 { border:5px solid green;}</style>"""
+
+ def test_css_output(self):
+ css_node = CssCompressor('css', self.css)
+ links = make_soup(css_node.output()).find_all('link')
+ media = ['screen', 'print', 'all', None]
+ self.assertEqual(len(links), 4)
+ self.assertEqual(media, [l.get('media', None) for l in links])
+
+ 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', css)
+ media = ['screen', 'print', 'all', None, 'print']
+ links = make_soup(css_node.output()).find_all('link')
+ self.assertEqual(media, [l.get('media', None) for l in links])
+
+ @override_settings(COMPRESS_PRECOMPILERS=(
+ ('text/foobar', '%s %s {infile} {outfile}' % (sys.executable, os.path.join(test_dir, 'precompiler.py'))),
+ ), COMPRESS_ENABLED=False)
+ def test_passthough_when_compress_disabled(self):
+ 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', css)
+ output = make_soup(css_node.output()).find_all(['link', 'style'])
+ self.assertEqual(['/static/css/one.css', '/static/css/two.css', None],
+ [l.get('href', None) for l in output])
+ self.assertEqual(['screen', 'screen', 'screen'],
+ [l.get('media', None) for l in output])
+
+
+@override_settings(COMPRESS_VERBOSE=True)
+class VerboseTestCase(CompressorTestCase):
+ pass
+
+
+class CacheBackendTestCase(CompressorTestCase):
+
+ def test_correct_backend(self):
+ from compressor.cache import cache
+ self.assertEqual(cache.__class__, locmem.LocMemCache)
+
+
+class JsAsyncDeferTestCase(SimpleTestCase):
+ def setUp(self):
+ self.js = """\
+ <script src="/static/js/one.js" type="text/javascript"></script>
+ <script src="/static/js/two.js" type="text/javascript" async></script>
+ <script src="/static/js/three.js" type="text/javascript" defer></script>
+ <script type="text/javascript">obj.value = "value";</script>
+ <script src="/static/js/one.js" type="text/javascript" async></script>
+ <script src="/static/js/two.js" type="text/javascript" async></script>
+ <script src="/static/js/three.js" type="text/javascript"></script>"""
+
+ def test_js_output(self):
+ def extract_attr(tag):
+ if tag.has_attr('async'):
+ return 'async'
+ if tag.has_attr('defer'):
+ return 'defer'
+ js_node = JsCompressor('js', self.js)
+ output = [None, 'async', 'defer', None, 'async', None]
+ scripts = make_soup(js_node.output()).find_all('script')
+ attrs = [extract_attr(s) for s in scripts]
+ self.assertEqual(output, attrs)
+
+
+class JSWithParensTestCase(SimpleTestCase):
+ def setUp(self):
+ self.js = """
+ <script src="/static/js/one.js"></script>
+ <script src="/static/js/two.js"></script>
+ """
+
+ def test_js_content(self):
+ js_node = JsCompressor('js', self.js)
+
+ content = js_node.filter_input()
+ self.assertEqual(content[0], 'obj = {};;')
+ self.assertEqual(content[1], 'pollos = {};')
+
+
+class CacheTestCase(SimpleTestCase):
+
+ def setUp(self):
+ cachemod._cachekey_func = None
+
+ def test_get_cachekey_basic(self):
+ self.assertEqual(get_cachekey("foo"), "django_compressor.foo")
+
+ @override_settings(COMPRESS_CACHE_KEY_FUNCTION='.leading.dot')
+ def test_get_cachekey_leading_dot(self):
+ self.assertRaises(ImportError, lambda: get_cachekey("foo"))
+
+ @override_settings(COMPRESS_CACHE_KEY_FUNCTION='invalid.module')
+ def test_get_cachekey_invalid_mod(self):
+ self.assertRaises(ImportError, lambda: get_cachekey("foo"))
+
+ def test_get_precompiler_cachekey(self):
+ try:
+ get_precompiler_cachekey("asdf", "asdf")
+ except TypeError:
+ self.fail("get_precompiler_cachekey raised TypeError unexpectedly")
+
+
+class CompressorInDebugModeTestCase(SimpleTestCase):
+
+ def setUp(self):
+ self.css = '<link rel="stylesheet" href="/static/css/one.css" type="text/css" />'
+ self.tmpdir = mkdtemp()
+ new_static_root = os.path.join(self.tmpdir, "static")
+ copytree(settings.STATIC_ROOT, new_static_root)
+
+ self.override_settings = self.settings(
+ COMPRESS_ENABLED=True,
+ COMPRESS_PRECOMPILERS=(),
+ COMPRESS_DEBUG_TOGGLE='nocompress',
+ DEBUG=True,
+ STATIC_ROOT=new_static_root,
+ COMPRESS_ROOT=new_static_root,
+ STATICFILES_DIRS=[settings.COMPRESS_ROOT]
+ )
+ self.override_settings.__enter__()
+
+ def tearDown(self):
+ rmtree(self.tmpdir)
+ self.override_settings.__exit__(None, None, None)
+
+ def test_filename_in_debug_mode(self):
+ # In debug mode, compressor should look for files using staticfiles
+ # finders only, and not look into the global static directory, where
+ # files can be outdated
+ css_filename = os.path.join(settings.COMPRESS_ROOT, "css", "one.css")
+ # Store the hash of the original file's content
+ with open(css_filename) as f:
+ css_content = f.read()
+ hashed = get_hexdigest(css_content, 12)
+ # Now modify the file in the STATIC_ROOT
+ test_css_content = "p { font-family: 'test' }"
+ with open(css_filename, "a") as css:
+ css.write("\n")
+ css.write(test_css_content)
+ # We should generate a link with the hash of the original content, not
+ # the modified one
+<<<<<<< HEAD
+ expected = '<link rel="stylesheet" href="/static/CACHE/css/%s.css" type="text/css" />' % hashed
+=======
+ expected = '<link rel="stylesheet" href="/static/CACHE/css/%s.css" type="text/css">' % hashed
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+ compressor = CssCompressor('css', self.css)
+ compressor.storage = DefaultStorage()
+ output = compressor.output()
+ self.assertEqual(expected, output)
+ with open(os.path.join(settings.COMPRESS_ROOT, "CACHE", "css",
+ "%s.css" % hashed), "r") as f:
+ result = f.read()
+ self.assertTrue(test_css_content not in result)
diff --git a/compressor/tests/test_filters.py b/compressor/tests/test_filters.py
index ec94b36..10aed42 100644
--- a/compressor/tests/test_filters.py
+++ b/compressor/tests/test_filters.py
@@ -5,7 +5,7 @@ import os
import sys
import mock
-from django.utils import six
+import six
from django.utils.encoding import smart_text
from django.test import TestCase
from django.test.utils import override_settings
diff --git a/compressor/tests/test_jinja2ext.py b/compressor/tests/test_jinja2ext.py
index 514d713..42e0ab8 100644
--- a/compressor/tests/test_jinja2ext.py
+++ b/compressor/tests/test_jinja2ext.py
@@ -95,7 +95,7 @@ class TestJinja2CompressorExtension(TestCase):
<script type="text/javascript" charset="utf-8">obj.value = "value";</script>
{% endcompress %}""")
context = {'STATIC_URL': settings.COMPRESS_URL}
- out = '<script type="text/javascript" src="/static/CACHE/js/output.74e158ccb432.js"></script>'
+ out = '<script src="/static/CACHE/js/output.8a0fed36c317.js"></script>'
self.assertEqual(out, template.render(context))
def test_nonascii_js_tag(self):
@@ -104,7 +104,7 @@ class TestJinja2CompressorExtension(TestCase):
<script type="text/javascript" charset="utf-8">var test_value = "\u2014";</script>
{% endcompress %}""")
context = {'STATIC_URL': settings.COMPRESS_URL}
- out = '<script type="text/javascript" src="/static/CACHE/js/output.a18195c6ae48.js"></script>'
+ out = '<script src="/static/CACHE/js/output.8c00f1cf1e0a.js"></script>'
self.assertEqual(out, template.render(context))
def test_nonascii_latin1_js_tag(self):
@@ -113,7 +113,7 @@ class TestJinja2CompressorExtension(TestCase):
<script type="text/javascript">var test_value = "\u2014";</script>
{% endcompress %}""")
context = {'STATIC_URL': settings.COMPRESS_URL}
- out = '<script type="text/javascript" src="/static/CACHE/js/output.f64debbd8878.js"></script>'
+ out = '<script src="/static/CACHE/js/output.06a98ccfd380.js"></script>'
self.assertEqual(out, template.render(context))
def test_css_inline(self):
@@ -134,7 +134,7 @@ class TestJinja2CompressorExtension(TestCase):
<script type="text/javascript" charset="utf-8">obj.value = "value";</script>
{% endcompress %}""")
context = {'STATIC_URL': settings.COMPRESS_URL}
- out = '<script type="text/javascript">;obj={};;obj.value="value";</script>'
+ out = '<script>obj={};;obj.value="value";;</script>'
self.assertEqual(out, template.render(context))
def test_nonascii_inline_css(self):
@@ -143,6 +143,6 @@ class TestJinja2CompressorExtension(TestCase):
'<style type="text/css">'
'/* русский текст */'
'</style>{% endcompress %}')
- out = '<link rel="stylesheet" href="/static/CACHE/css/output.c836c9caed5c.css" type="text/css" />'
+ out = '<link rel="stylesheet" href="/static/CACHE/css/output.c836c9caed5c.css" type="text/css">'
context = {'STATIC_URL': settings.COMPRESS_URL}
self.assertEqual(out, template.render(context))
diff --git a/compressor/tests/test_mtime_cache.py b/compressor/tests/test_mtime_cache.py
index 28b279e..bcc1265 100644
--- a/compressor/tests/test_mtime_cache.py
+++ b/compressor/tests/test_mtime_cache.py
@@ -8,7 +8,7 @@ class TestMtimeCacheCommand(TestCase):
# FIXME: add actual tests, improve the existing ones.
exclusion_patterns = [
- '*CACHE*', '*custom*', '*066cd253eada.js', '*d728fc7f9301.js', '*74e158ccb432.js', 'test.txt*'
+ '*CACHE*', '*custom*', '*066cd253eada.js', '*d728fc7f9301.js', '*8a0fed36c317.js', 'test.txt*'
]
def default_ignore(self):
diff --git a/compressor/tests/test_offline.py b/compressor/tests/test_offline.py
index d18696c..2b3c801 100644
--- a/compressor/tests/test_offline.py
+++ b/compressor/tests/test_offline.py
@@ -9,12 +9,12 @@ from importlib import import_module
from mock import patch
from unittest import SkipTest
+import six
from django.core.management import call_command
from django.core.management.base import CommandError
from django.template import Template, Context
from django.test import TestCase
from django.test.utils import override_settings
-from django.utils import six
from compressor.cache import flush_offline_manifest, get_offline_manifest
from compressor.conf import settings
@@ -172,7 +172,7 @@ class OfflineTestCaseMixin(object):
def _render_script(self, hash):
return (
- '<script type="text/javascript" src="{}CACHE/js/{}.{}.js">'
+ '<script src="{}CACHE/js/{}.{}.js">'
'</script>'.format(
settings.COMPRESS_URL_PLACEHOLDER, self.expected_basename, hash
)
@@ -181,7 +181,7 @@ class OfflineTestCaseMixin(object):
def _render_link(self, hash):
return (
'<link rel="stylesheet" href="{}CACHE/css/{}.{}.css" '
- 'type="text/css" />'.format(
+ 'type="text/css">'.format(
settings.COMPRESS_URL_PLACEHOLDER, self.expected_basename, hash
)
)
@@ -240,7 +240,7 @@ class OfflineTestCaseMixin(object):
class OfflineCompressBasicTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = 'basic'
- expected_hash = 'a432b6ddb2c4'
+ expected_hash = '822ac7501287'
@patch.object(CompressCommand, 'compress')
def test_handle_no_args(self, compress_mock):
@@ -327,7 +327,7 @@ class OfflineCompressSkipDuplicatesTestCase(OfflineTestCaseMixin, TestCase):
# 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([self._render_script('a432b6ddb2c4')], result)
+ self.assertEqual([self._render_script('822ac7501287')], result)
rendered_template = self._render_template(engine)
# But rendering the template returns both (identical) scripts.
self.assertEqual(
@@ -342,19 +342,19 @@ class SuperMixin:
class OfflineCompressBlockSuperTestCase(
SuperMixin, OfflineTestCaseMixin, TestCase):
templates_dir = 'test_block_super'
- expected_hash = '68c645740177'
+ expected_hash = '817b5defb197'
class OfflineCompressBlockSuperMultipleTestCase(
SuperMixin, OfflineTestCaseMixin, TestCase):
templates_dir = 'test_block_super_multiple'
- expected_hash = 'f87403f4d8af'
+ expected_hash = 'd3f749e83c81'
class OfflineCompressBlockSuperMultipleCachedLoaderTestCase(
SuperMixin, OfflineTestCaseMixin, TestCase):
templates_dir = 'test_block_super_multiple_cached'
- expected_hash = 'ea860151aa21'
+ expected_hash = '055f88f4751f'
additional_test_settings = {
'TEMPLATE_LOADERS': (
('django.template.loaders.cached.Loader', (
@@ -373,8 +373,8 @@ class OfflineCompressBlockSuperTestCaseWithExtraContent(
count, result = CompressCommand().handle_inner(engines=[engine], verbosity=0)
self.assertEqual(2, count)
self.assertEqual([
- self._render_script('9717f9c7e9ff'),
- self._render_script('68c645740177')
+ self._render_script('bfcec76e0f28'),
+ self._render_script('817b5defb197')
], result)
rendered_template = self._render_template(engine)
self.assertEqual(rendered_template, self._render_result(result, ''))
@@ -382,7 +382,7 @@ class OfflineCompressBlockSuperTestCaseWithExtraContent(
class OfflineCompressConditionTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = 'test_condition'
- expected_hash = '58517669cb7c'
+ expected_hash = 'a3275743dc69'
additional_test_settings = {
'COMPRESS_OFFLINE_CONTEXT': {
'condition': 'red',
@@ -392,23 +392,23 @@ class OfflineCompressConditionTestCase(OfflineTestCaseMixin, TestCase):
class OfflineCompressTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = 'test_templatetag'
- expected_hash = '16f8880b81ab'
+ expected_hash = '2bb88185b4f5'
class OfflineCompressStaticTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = 'test_static_templatetag'
- expected_hash = '2607a2085687'
+ expected_hash = 'be0b1eade28b'
class OfflineCompressTemplateTagNamedTestCase(OfflineTestCaseMixin, TestCase):
templates_dir = 'test_templatetag_named'
expected_basename = 'output_name'
- expected_hash = 'a432b6ddb2c4'
+ expected_hash = '822ac7501287'
class OfflineCompressTestCaseWithContext(OfflineTestCaseMixin, TestCase):
templates_dir = 'test_with_context'
- expected_hash = '045b3ad664c8'
+ expected_hash = 'c6bf81bca7ad'
additional_test_settings = {
'COMPRESS_OFFLINE_CONTEXT': {
'content': 'OK!',
@@ -419,7 +419,7 @@ class OfflineCompressTestCaseWithContext(OfflineTestCaseMixin, TestCase):
class OfflineCompressTestCaseWithContextSuper(
SuperMixin, OfflineTestCaseMixin, TestCase):
templates_dir = 'test_with_context_super'
- expected_hash = '9a8b47adfe17'
+ expected_hash = 'dd79e1bd1527'
additional_test_settings = {
'COMPRESS_OFFLINE_CONTEXT': {
'content': 'OK!',
@@ -429,7 +429,7 @@ class OfflineCompressTestCaseWithContextSuper(
class OfflineCompressTestCaseWithContextList(OfflineTestCaseMixin, TestCase):
templates_dir = 'test_with_context'
- expected_hash = ['3b6cd13d4bde', '5aef37564182', 'c6d6c723a18b']
+ expected_hash = ['8b4a7452e1c5', '55b3123e884c', 'bfc63829cc58']
additional_test_settings = {
'COMPRESS_OFFLINE_CONTEXT': list(offline_context_generator())
}
@@ -445,7 +445,7 @@ class OfflineCompressTestCaseWithContextList(OfflineTestCaseMixin, TestCase):
class OfflineCompressTestCaseWithContextListSuper(
SuperMixin, OfflineCompressTestCaseWithContextList):
templates_dir = 'test_with_context_super'
- expected_hash = ['dc68dd60aed4', 'c2e50f475853', '045b48455bee']
+ expected_hash = ['b39975a8f6ea', 'ed565a1d262f', '6ac9e4b29feb']
additional_test_settings = {
'COMPRESS_OFFLINE_CONTEXT': list(offline_context_generator())
}
@@ -454,7 +454,7 @@ class OfflineCompressTestCaseWithContextListSuper(
class OfflineCompressTestCaseWithContextGenerator(
OfflineTestCaseMixin, TestCase):
templates_dir = 'test_with_context'
- expected_hash = ['3b6cd13d4bde', '5aef37564182', 'c6d6c723a18b']
+ expected_hash = ['8b4a7452e1c5', '55b3123e884c', 'bfc63829cc58']
additional_test_settings = {
'COMPRESS_OFFLINE_CONTEXT': 'compressor.tests.test_offline.'
'offline_context_generator'
@@ -473,7 +473,7 @@ class OfflineCompressTestCaseWithContextGenerator(
class OfflineCompressTestCaseWithContextGeneratorSuper(
SuperMixin, OfflineCompressTestCaseWithContextGenerator):
templates_dir = 'test_with_context_super'
- expected_hash = ['dc68dd60aed4', 'c2e50f475853', '045b48455bee']
+ expected_hash = ['b39975a8f6ea', 'ed565a1d262f', '6ac9e4b29feb']
additional_test_settings = {
'COMPRESS_OFFLINE_CONTEXT': 'compressor.tests.test_offline.'
'offline_context_generator'
@@ -487,7 +487,7 @@ class OfflineCompressStaticUrlIndependenceTestCase(
I.e. users can use the manifest with any other STATIC_URL in the future.
"""
templates_dir = 'test_static_url_independence'
- expected_hash = '5014de5edcbe'
+ expected_hash = 'b0bfc3754fd4'
additional_test_settings = {
'STATIC_URL': '/custom/static/url/',
# We use ``COMPRESS_OFFLINE_CONTEXT`` generator to make sure that
@@ -514,7 +514,7 @@ class OfflineCompressStaticUrlIndependenceTestCase(
class OfflineCompressTestCaseWithContextVariableInheritance(
OfflineTestCaseMixin, TestCase):
templates_dir = 'test_with_context_variable_inheritance'
- expected_hash = '0d88c897f64a'
+ expected_hash = 'b8376aad1357'
additional_test_settings = {
'COMPRESS_OFFLINE_CONTEXT': {
'parent_template': 'base.html',
@@ -537,7 +537,7 @@ class OfflineCompressTestCaseWithContextVariableInheritanceSuper(
'parent_template': 'base2.html',
}]
}
- expected_hash = ['6a2f85c623c6', '04b482ba2855']
+ expected_hash = ['cee48db7cedc', 'c877c436363a']
class OfflineCompressTestCaseWithContextGeneratorImportError(
@@ -601,8 +601,8 @@ class OfflineCompressTestCaseErrors(OfflineTestCaseMixin, TestCase):
self.assertIn(self._render_link('7ff52cb38987'), result)
self.assertIn(self._render_link('2db2b4d36380'), result)
- self.assertIn(self._render_script('3910ce35946a'), result)
- self.assertIn(self._render_script('244f05154671'), result)
+ self.assertIn(self._render_script('eeabdac29232'), result)
+ self.assertIn(self._render_script('9a7f06880ce3'), result)
class OfflineCompressTestCaseWithError(OfflineTestCaseMixin, TestCase):
@@ -634,7 +634,7 @@ class OfflineCompressEmptyTag(OfflineTestCaseMixin, TestCase):
compressor encounters such an emptystring in the manifest.
"""
templates_dir = 'basic'
- expected_hash = 'a432b6ddb2c4'
+ expected_hash = '822ac7501287'
def _test_offline(self, engine):
CompressCommand().handle_inner(engines=[engine], verbosity=0)
@@ -647,8 +647,8 @@ class OfflineCompressBlockSuperBaseCompressed(OfflineTestCaseMixin, TestCase):
template_names = ['base.html', 'base2.html',
'test_compressor_offline.html']
templates_dir = 'test_block_super_base_compressed'
- expected_hash_offline = ['5a2fda9ac8e4', '5b7c5e6473f8', 'f87403f4d8af']
- expected_hash = ['028c3fc42232', '2e9d3f5545a6', 'f87403f4d8af']
+ expected_hash_offline = ['e4e9263fa4c0', '9cecd41a505f', 'd3f749e83c81']
+ expected_hash = ['028c3fc42232', '2e9d3f5545a6', 'd3f749e83c81']
# Block.super not supported for Jinja2 yet.
engines = ('django',)
@@ -715,9 +715,9 @@ class OfflineCompressComplexTestCase(OfflineTestCaseMixin, TestCase):
count, result = CompressCommand().handle_inner(engines=[engine], verbosity=0)
self.assertEqual(3, count)
self.assertEqual([
- self._render_script('2c1f0f85a90d'),
- self._render_script('8b594c4f7264'),
- self._render_script('e0e424964c8c')
+ self._render_script('76a82cfab9ab'),
+ self._render_script('7219642b8ab4'),
+ self._render_script('567bb77b13db')
], result)
rendered_template = self._render_template(engine)
self.assertEqual(
@@ -765,20 +765,20 @@ class TestCompressCommand(OfflineTestCaseMixin, TestCase):
call_command('compress', engines=["django"], **opts)
manifest_django = get_offline_manifest()
manifest_django_expected = self._build_expected_manifest(
- {'0fed9c02607acba22316a328075a81a74e0983ae79470daa9d3707a337623dc3': '023629c58235'})
+ {'0fed9c02607acba22316a328075a81a74e0983ae79470daa9d3707a337623dc3': '0241107e9a9a'})
self.assertEqual(manifest_django, manifest_django_expected)
call_command('compress', engines=["jinja2"], **opts)
manifest_jinja2 = get_offline_manifest()
manifest_jinja2_expected = self._build_expected_manifest(
- {'077408d23d4a829b8f88db2eadcf902b29d71b14f94018d900f38a3f8ed24c94': 'b6695d1aa847'})
+ {'077408d23d4a829b8f88db2eadcf902b29d71b14f94018d900f38a3f8ed24c94': '5694ca83dd14'})
self.assertEqual(manifest_jinja2, manifest_jinja2_expected)
call_command('compress', engines=["django", "jinja2"], **opts)
manifest_both = get_offline_manifest()
manifest_both_expected = self._build_expected_manifest(
- {'0fed9c02607acba22316a328075a81a74e0983ae79470daa9d3707a337623dc3': '023629c58235',
- '077408d23d4a829b8f88db2eadcf902b29d71b14f94018d900f38a3f8ed24c94': 'b6695d1aa847'})
+ {'0fed9c02607acba22316a328075a81a74e0983ae79470daa9d3707a337623dc3': '0241107e9a9a',
+ '077408d23d4a829b8f88db2eadcf902b29d71b14f94018d900f38a3f8ed24c94': '5694ca83dd14'})
self.assertEqual(manifest_both, manifest_both_expected)
@@ -817,7 +817,7 @@ class OfflineCompressTestCaseWithLazyStringAlikeUrls(OfflineCompressTestCaseWith
'compressor.tests.test_offline.static_url_context_generator'
)
}
- expected_hash = '2607a2085687'
+ expected_hash = 'be0b1eade28b'
def _test_offline(self, engine):
count, result = CompressCommand().handle_inner(engines=[engine], verbosity=0)
diff --git a/compressor/tests/test_offline.py.orig b/compressor/tests/test_offline.py.orig
new file mode 100644
index 0000000..b941fa0
--- /dev/null
+++ b/compressor/tests/test_offline.py.orig
@@ -0,0 +1,868 @@
+from __future__ import with_statement, unicode_literals
+import copy
+from contextlib import contextmanager
+
+import io
+import os
+from importlib import import_module
+
+from mock import patch
+from unittest import SkipTest
+
+import six
+from django.core.management import call_command
+from django.core.management.base import CommandError
+from django.template import Template, Context
+from django.test import TestCase
+from django.test.utils import override_settings
+
+from compressor.cache import flush_offline_manifest, get_offline_manifest
+from compressor.conf import settings
+from compressor.exceptions import OfflineGenerationError
+from compressor.management.commands.compress import Command as CompressCommand
+from compressor.storage import default_storage
+from compressor.utils import get_mod_func
+
+from django.urls import get_script_prefix, set_script_prefix
+
+
+def offline_context_generator():
+ for i in range(1, 4):
+ yield {'content': 'OK %d!' % i}
+
+
+def static_url_context_generator():
+ yield {'STATIC_URL': settings.STATIC_URL}
+
+
+class LazyScriptNamePrefixedUrl(six.text_type):
+ """
+ Lazy URL with ``SCRIPT_NAME`` WSGI param as path prefix.
+
+ .. code-block :: python
+
+ settings.STATIC_URL = LazyScriptNamePrefixedUrl('/static/')
+
+ # HTTP request to '/some/page/' without SCRIPT_NAME
+ str(settings.STATIC_URL) == '/static/'
+
+ # HTTP request to '/app/prefix/some/page/` with SCRIPT_NAME = '/app/prefix/'
+ str(settings.STATIC_URL) == '/app/prefix/static/'
+
+ # HTTP request to '/another/prefix/some/page/` with SCRIPT_NAME = '/another/prefix/'
+ str(settings.STATIC_URL) == '/another/prefix/static/'
+
+ The implementation is incomplete, all ``str`` methods must be overridden
+ in order to work correctly with the rest of Django core.
+ """
+ def __str__(self):
+ return get_script_prefix() + self[1:] if self.startswith('/') else self
+
+ def __unicode__(self):
+ return str(self)
+
+ def split(self, *args, **kwargs):
+ """
+ Override ``.split()`` method to make it work with ``{% static %}``.
+ """
+ return six.text_type(self).split(*args, **kwargs)
+
+
+@contextmanager
+def script_prefix(new_prefix):
+ """
+ Override ``SCRIPT_NAME`` WSGI param, yield, then restore its original value.
+
+ :param new_prefix: New ``SCRIPT_NAME`` value.
+ """
+ old_prefix = get_script_prefix()
+ set_script_prefix(new_prefix)
+ yield
+ set_script_prefix(old_prefix)
+
+
+class OfflineTestCaseMixin(object):
+ CHARSET = 'utf-8'
+ template_name = 'test_compressor_offline.html'
+ # Change this for each test class
+ templates_dir = ''
+ expected_basename = 'output'
+ expected_hash = ''
+ # Engines to test
+ engines = ('django', 'jinja2')
+ additional_test_settings = None
+
+ def setUp(self):
+ # Reset template dirs, because it enables us to force compress to
+ # consider only a specific directory (helps us make true,
+ # independent unit tests).
+ # Specify both Jinja2 and Django template locations. When the wrong
+ # engine is used to parse a template, the TemplateSyntaxError will
+ # cause the template to be skipped over.
+ # We've hardcoded TEMPLATES[0] to be Django templates backend and
+ # TEMPLATES[1] to be Jinja2 templates backend in test_settings.
+ TEMPLATES = copy.deepcopy(settings.TEMPLATES)
+
+ django_template_dir = os.path.join(
+ TEMPLATES[0]['DIRS'][0], self.templates_dir)
+ jinja2_template_dir = os.path.join(
+ TEMPLATES[1]['DIRS'][0], self.templates_dir)
+
+ TEMPLATES[0]['DIRS'] = [django_template_dir]
+ TEMPLATES[1]['DIRS'] = [jinja2_template_dir]
+
+ override_settings = {
+ 'TEMPLATES': TEMPLATES,
+ 'COMPRESS_ENABLED': True,
+ 'COMPRESS_OFFLINE': True
+ }
+
+ if 'jinja2' in self.engines:
+ override_settings['COMPRESS_JINJA2_GET_ENVIRONMENT'] = (
+ lambda: self._get_jinja2_env())
+
+ if self.additional_test_settings is not None:
+ override_settings.update(self.additional_test_settings)
+
+ self.override_settings = self.settings(**override_settings)
+ self.override_settings.__enter__()
+
+ if 'django' in self.engines:
+ self.template_path = os.path.join(
+ django_template_dir, self.template_name)
+
+ with io.open(self.template_path,
+ encoding=self.CHARSET) as file_:
+ self.template = Template(file_.read())
+
+ if 'jinja2' in self.engines:
+ self.template_path_jinja2 = os.path.join(
+ jinja2_template_dir, self.template_name)
+ jinja2_env = override_settings['COMPRESS_JINJA2_GET_ENVIRONMENT']()
+
+ with io.open(self.template_path_jinja2,
+ encoding=self.CHARSET) as file_:
+ self.template_jinja2 = jinja2_env.from_string(file_.read())
+
+ def tearDown(self):
+ self.override_settings.__exit__(None, None, None)
+
+ manifest_path = os.path.join('CACHE', 'manifest.json')
+ if default_storage.exists(manifest_path):
+ default_storage.delete(manifest_path)
+
+ def _prepare_contexts(self, engine):
+ contexts = settings.COMPRESS_OFFLINE_CONTEXT
+ if not isinstance(contexts, (list, tuple)):
+ contexts = [contexts]
+ if engine == 'django':
+ return [Context(c) for c in contexts]
+ if engine == 'jinja2':
+ return contexts
+ return None
+
+ def _render_template(self, engine):
+ contexts = self._prepare_contexts(engine)
+ if engine == 'django':
+ return ''.join(self.template.render(c) for c in contexts)
+ if engine == 'jinja2':
+ return '\n'.join(
+ self.template_jinja2.render(c) for c in contexts) + '\n'
+ return None
+
+ def _render_script(self, hash):
+ return (
+ '<script src="{}CACHE/js/{}.{}.js">'
+ '</script>'.format(
+ settings.COMPRESS_URL_PLACEHOLDER, self.expected_basename, hash
+ )
+ )
+
+ def _render_link(self, hash):
+ return (
+ '<link rel="stylesheet" href="{}CACHE/css/{}.{}.css" '
+ 'type="text/css">'.format(
+ settings.COMPRESS_URL_PLACEHOLDER, self.expected_basename, hash
+ )
+ )
+
+ def _render_result(self, result, separator='\n'):
+ return (separator.join(result) + '\n').replace(
+ settings.COMPRESS_URL_PLACEHOLDER, six.text_type(settings.COMPRESS_URL)
+ )
+
+ def _test_offline(self, engine):
+ hashes = self.expected_hash
+ if not isinstance(hashes, (list, tuple)):
+ hashes = [hashes]
+ count, result = CompressCommand().handle_inner(engines=[engine], verbosity=0)
+ self.assertEqual(len(hashes), count)
+ self.assertEqual([self._render_script(h) for h in hashes], result)
+ rendered_template = self._render_template(engine)
+ self.assertEqual(rendered_template, self._render_result(result))
+
+ def test_offline_django(self):
+ if 'django' not in self.engines:
+ raise SkipTest('This test class does not support django engine.')
+ self._test_offline(engine='django')
+
+ def test_offline_jinja2(self):
+ if 'jinja2' not in self.engines:
+ raise SkipTest('This test class does not support jinja2 engine.')
+ self._test_offline(engine='jinja2')
+
+ def _get_jinja2_env(self):
+ import jinja2
+ 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.TEMPLATES[1]['DIRS'], encoding=self.CHARSET)
+ return loader
+
+
+class OfflineCompressBasicTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'basic'
+ expected_hash = '822ac7501287'
+
+ @patch.object(CompressCommand, 'compress')
+ def test_handle_no_args(self, compress_mock):
+ compress_mock.return_value = {}, 1, []
+ CompressCommand().handle()
+ self.assertEqual(compress_mock.call_count, 1)
+
+ @patch.object(CompressCommand, 'compress')
+ def test_handle_compress_disabled(self, compress_mock):
+ with self.settings(COMPRESS_ENABLED=False):
+ with self.assertRaises(CommandError):
+ CompressCommand().handle()
+ self.assertEqual(compress_mock.call_count, 0)
+
+ @patch.object(CompressCommand, 'compress')
+ def test_handle_compress_offline_disabled(self, compress_mock):
+ with self.settings(COMPRESS_OFFLINE=False):
+ with self.assertRaises(CommandError):
+ CompressCommand().handle()
+ self.assertEqual(compress_mock.call_count, 0)
+
+ @patch.object(CompressCommand, 'compress')
+ def test_handle_compress_offline_disabled_force(self, compress_mock):
+ compress_mock.return_value = {}, 1, []
+ with self.settings(COMPRESS_OFFLINE=False):
+ CompressCommand().handle(force=True)
+ self.assertEqual(compress_mock.call_count, 1)
+
+ def test_rendering_without_manifest_raises_exception(self):
+ # flush cached manifest
+ flush_offline_manifest()
+ self.assertRaises(OfflineGenerationError,
+ self.template.render, Context({}))
+
+ 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().handle_inner(engines=[engine], verbosity=0)
+ 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([self._render_script(self.expected_hash)], result)
+ rendered_template = self._render_template(engine)
+ self.assertEqual(rendered_template, self._render_result(result))
+
+ def test_deleting_manifest_does_not_affect_rendering(self):
+ for engine in self.engines:
+ self._test_deleting_manifest_does_not_affect_rendering(engine)
+
+ def test_get_loaders(self):
+ TEMPLATE_LOADERS = (
+ ('django.template.loaders.cached.Loader', (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ )),
+ )
+ with self.settings(TEMPLATE_LOADERS=TEMPLATE_LOADERS):
+ from django.template.loaders.filesystem import (
+ Loader as FileSystemLoader)
+ from django.template.loaders.app_directories import (
+ Loader as AppDirectoriesLoader)
+ loaders = CompressCommand().get_loaders()
+ self.assertTrue(isinstance(loaders[0], FileSystemLoader))
+ self.assertTrue(isinstance(loaders[1], AppDirectoriesLoader))
+
+ @patch("compressor.offline.django.DjangoParser.render_node",
+ side_effect=Exception(b"non-ascii character here:\xc3\xa4"))
+ def test_non_ascii_exception_messages(self, mock):
+ with self.assertRaises(CommandError):
+ CompressCommand().handle(verbosity=0)
+
+
+class OfflineCompressSkipDuplicatesTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_duplicate'
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().handle_inner(engines=[engine], verbosity=0)
+ # 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([self._render_script('822ac7501287')], result)
+ rendered_template = self._render_template(engine)
+ # But rendering the template returns both (identical) scripts.
+ self.assertEqual(
+ rendered_template, self._render_result(result * 2, ''))
+
+
+class SuperMixin:
+ # Block.super not supported for Jinja2 yet.
+ engines = ('django',)
+
+
+class OfflineCompressBlockSuperTestCase(
+ SuperMixin, OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_block_super'
+<<<<<<< HEAD
+ expected_hash = '68c645740177'
+=======
+ expected_hash = '817b5defb197'
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+
+
+class OfflineCompressBlockSuperMultipleTestCase(
+ SuperMixin, OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_block_super_multiple'
+<<<<<<< HEAD
+ expected_hash = 'f87403f4d8af'
+=======
+ expected_hash = 'd3f749e83c81'
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+
+
+class OfflineCompressBlockSuperMultipleCachedLoaderTestCase(
+ SuperMixin, OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_block_super_multiple_cached'
+<<<<<<< HEAD
+ expected_hash = 'ea860151aa21'
+=======
+ expected_hash = '055f88f4751f'
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+ additional_test_settings = {
+ 'TEMPLATE_LOADERS': (
+ ('django.template.loaders.cached.Loader', (
+ 'django.template.loaders.filesystem.Loader',
+ 'django.template.loaders.app_directories.Loader',
+ )),
+ )
+ }
+
+
+class OfflineCompressBlockSuperTestCaseWithExtraContent(
+ SuperMixin, OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_block_super_extra'
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().handle_inner(engines=[engine], verbosity=0)
+ self.assertEqual(2, count)
+ self.assertEqual([
+ self._render_script('bfcec76e0f28'),
+ self._render_script('817b5defb197')
+ ], result)
+ rendered_template = self._render_template(engine)
+ self.assertEqual(rendered_template, self._render_result(result, ''))
+
+
+class OfflineCompressConditionTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_condition'
+ expected_hash = 'a3275743dc69'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': {
+ 'condition': 'red',
+ }
+ }
+
+
+class OfflineCompressTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_templatetag'
+ expected_hash = '2bb88185b4f5'
+
+
+class OfflineCompressStaticTemplateTagTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_static_templatetag'
+ expected_hash = 'be0b1eade28b'
+
+
+class OfflineCompressTemplateTagNamedTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_templatetag_named'
+ expected_basename = 'output_name'
+<<<<<<< HEAD
+ expected_hash = 'a432b6ddb2c4'
+=======
+ expected_hash = '822ac7501287'
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+
+
+class OfflineCompressTestCaseWithContext(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context'
+ expected_hash = 'c6bf81bca7ad'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': {
+ 'content': 'OK!',
+ }
+ }
+
+
+class OfflineCompressTestCaseWithContextSuper(
+ SuperMixin, OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context_super'
+ expected_hash = 'dd79e1bd1527'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': {
+ 'content': 'OK!',
+ }
+ }
+
+
+class OfflineCompressTestCaseWithContextList(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context'
+ expected_hash = ['8b4a7452e1c5', '55b3123e884c', 'bfc63829cc58']
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': list(offline_context_generator())
+ }
+
+ def _prepare_contexts(self, engine):
+ if engine == 'django':
+ return [Context(c) for c in settings.COMPRESS_OFFLINE_CONTEXT]
+ if engine == 'jinja2':
+ return settings.COMPRESS_OFFLINE_CONTEXT
+ return None
+
+
+class OfflineCompressTestCaseWithContextListSuper(
+ SuperMixin, OfflineCompressTestCaseWithContextList):
+ templates_dir = 'test_with_context_super'
+ expected_hash = ['b39975a8f6ea', 'ed565a1d262f', '6ac9e4b29feb']
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': list(offline_context_generator())
+ }
+
+
+class OfflineCompressTestCaseWithContextGenerator(
+ OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context'
+ expected_hash = ['8b4a7452e1c5', '55b3123e884c', 'bfc63829cc58']
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': 'compressor.tests.test_offline.'
+ 'offline_context_generator'
+ }
+
+ def _prepare_contexts(self, engine):
+ module, function = get_mod_func(settings.COMPRESS_OFFLINE_CONTEXT)
+ contexts = getattr(import_module(module), function)()
+ if engine == 'django':
+ return (Context(c) for c in contexts)
+ if engine == 'jinja2':
+ return contexts
+ return None
+
+
+class OfflineCompressTestCaseWithContextGeneratorSuper(
+ SuperMixin, OfflineCompressTestCaseWithContextGenerator):
+ templates_dir = 'test_with_context_super'
+ expected_hash = ['b39975a8f6ea', 'ed565a1d262f', '6ac9e4b29feb']
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': 'compressor.tests.test_offline.'
+ 'offline_context_generator'
+ }
+
+
+class OfflineCompressStaticUrlIndependenceTestCase(
+ OfflineCompressTestCaseWithContextGenerator):
+ """
+ Test that the offline manifest is independent of STATIC_URL.
+ I.e. users can use the manifest with any other STATIC_URL in the future.
+ """
+ templates_dir = 'test_static_url_independence'
+ expected_hash = 'b0bfc3754fd4'
+ additional_test_settings = {
+ 'STATIC_URL': '/custom/static/url/',
+ # We use ``COMPRESS_OFFLINE_CONTEXT`` generator to make sure that
+ # ``STATIC_URL`` is not cached when rendering the template.
+ 'COMPRESS_OFFLINE_CONTEXT': (
+ 'compressor.tests.test_offline.static_url_context_generator'
+ )
+ }
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().handle_inner(engines=[engine], verbosity=0)
+ self.assertEqual(1, count)
+ self.assertEqual([self._render_script(self.expected_hash)], result)
+ self.assertEqual(
+ self._render_template(engine), self._render_result(result))
+
+ # Changing STATIC_URL setting doesn't break things despite that
+ # offline compression was made with different STATIC_URL.
+ with self.settings(STATIC_URL='/another/static/url/'):
+ self.assertEqual(
+ self._render_template(engine), self._render_result(result))
+
+
+class OfflineCompressTestCaseWithContextVariableInheritance(
+ OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context_variable_inheritance'
+ expected_hash = 'b8376aad1357'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': {
+ 'parent_template': 'base.html',
+ }
+ }
+
+ def _render_result(self, result, separator='\n'):
+ return '\n' + super(
+ OfflineCompressTestCaseWithContextVariableInheritance, self
+ )._render_result(result, separator)
+
+
+class OfflineCompressTestCaseWithContextVariableInheritanceSuper(
+ SuperMixin, OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context_variable_inheritance_super'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': [{
+ 'parent_template': 'base1.html',
+ }, {
+ 'parent_template': 'base2.html',
+ }]
+ }
+<<<<<<< HEAD
+ expected_hash = ['6a2f85c623c6', '04b482ba2855']
+=======
+ expected_hash = ['cee48db7cedc', 'c877c436363a']
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+
+
+class OfflineCompressTestCaseWithContextGeneratorImportError(
+ OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_with_context'
+
+ def _test_offline(self, engine):
+ # Test that we are properly generating ImportError when
+ # COMPRESS_OFFLINE_CONTEXT looks like a function but can't be imported
+ # for whatever reason.
+
+ with self.settings(
+ COMPRESS_OFFLINE_CONTEXT='invalid_mod.invalid_func'):
+ # Path with invalid module name -- ImportError:
+ self.assertRaises(
+ ImportError, CompressCommand().handle_inner, engines=[engine])
+
+ with self.settings(COMPRESS_OFFLINE_CONTEXT='compressor'):
+ # Valid module name only without function -- AttributeError:
+ self.assertRaises(
+ ImportError, CompressCommand().handle_inner, engines=[engine])
+
+ with self.settings(
+ COMPRESS_OFFLINE_CONTEXT='compressor.tests.invalid_function'):
+ # Path with invalid function name -- AttributeError:
+ self.assertRaises(
+ ImportError, CompressCommand().handle_inner, engines=[engine])
+
+ with self.settings(
+ COMPRESS_OFFLINE_CONTEXT='compressor.tests.test_offline'):
+ # Path without function attempts call on module -- TypeError:
+ self.assertRaises(
+ ImportError, CompressCommand().handle_inner, engines=[engine])
+
+ valid_path = 'compressor.tests.test_offline.offline_context_generator'
+ with self.settings(COMPRESS_OFFLINE_CONTEXT=valid_path):
+ # Valid path to generator function -- no ImportError:
+
+ try:
+ CompressCommand().handle_inner(engines=[engine], verbosity=0)
+ except ImportError:
+ self.fail('Valid path to offline context generator must'
+ ' not raise ImportError.')
+
+
+class OfflineCompressTestCaseErrors(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_error_handling'
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().handle_inner(engines=[engine], verbosity=0)
+
+ 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(self._render_link('7ff52cb38987'), result)
+ self.assertIn(self._render_link('2db2b4d36380'), result)
+
+ self.assertIn(self._render_script('eeabdac29232'), result)
+ self.assertIn(self._render_script('9a7f06880ce3'), result)
+
+
+class OfflineCompressTestCaseWithError(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_error_handling'
+ additional_test_settings = {
+ 'COMPRESS_PRECOMPILERS': (('text/coffeescript', 'nonexisting-binary'),)
+ }
+
+ 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
+ production.
+ """
+ with self.settings(DEBUG=True):
+ self.assertRaises(
+ CommandError, CompressCommand().handle_inner, engines=[engine], verbosity=0)
+
+ with self.settings(DEBUG=False):
+ self.assertRaises(
+ CommandError, CompressCommand().handle_inner, engines=[engine], verbosity=0)
+
+
+class OfflineCompressEmptyTag(OfflineTestCaseMixin, TestCase):
+ """
+ In case of a compress template tag with no content, an entry
+ will be added to the manifest with an empty string as value.
+ This test makes sure there is no recompression happening when
+ compressor encounters such an emptystring in the manifest.
+ """
+ templates_dir = 'basic'
+<<<<<<< HEAD
+ expected_hash = 'a432b6ddb2c4'
+=======
+ expected_hash = '822ac7501287'
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+
+ def _test_offline(self, engine):
+ CompressCommand().handle_inner(engines=[engine], verbosity=0)
+ manifest = get_offline_manifest()
+ manifest[list(manifest)[0]] = ''
+ self.assertEqual(self._render_template(engine), '\n')
+
+
+class OfflineCompressBlockSuperBaseCompressed(OfflineTestCaseMixin, TestCase):
+ template_names = ['base.html', 'base2.html',
+ 'test_compressor_offline.html']
+ templates_dir = 'test_block_super_base_compressed'
+ expected_hash_offline = ['e4e9263fa4c0', '9cecd41a505f', 'd3f749e83c81']
+ expected_hash = ['028c3fc42232', '2e9d3f5545a6', 'd3f749e83c81']
+ # Block.super not supported for Jinja2 yet.
+ engines = ('django',)
+
+ def setUp(self):
+ super(OfflineCompressBlockSuperBaseCompressed, self).setUp()
+
+ self.template_paths = []
+ self.templates = []
+ for template_name in self.template_names:
+ template_path = os.path.join(
+ settings.TEMPLATES[0]['DIRS'][0], template_name)
+ self.template_paths.append(template_path)
+ with io.open(template_path,
+ encoding=self.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().handle_inner(engines=[engine], verbosity=0)
+ self.assertEqual(len(self.expected_hash), count)
+ for expected_hash, template in zip(self.expected_hash_offline, self.templates):
+ expected = self._render_script(expected_hash)
+ self.assertIn(expected, result)
+ rendered_template = self._render_template(template, engine)
+ self.assertEqual(
+ rendered_template, self._render_result([expected]))
+
+
+class OfflineCompressInlineNonAsciiTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_inline_non_ascii'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': {
+ 'test_non_ascii_value': '\u2014',
+ }
+ }
+
+ def _test_offline(self, engine):
+ _, result = CompressCommand().handle_inner(engines=[engine], verbosity=0)
+ rendered_template = self._render_template(engine)
+ self.assertEqual(rendered_template, ''.join(result) + '\n')
+
+
+class OfflineCompressComplexTestCase(OfflineTestCaseMixin, TestCase):
+ templates_dir = 'test_complex'
+ additional_test_settings = {
+ 'COMPRESS_OFFLINE_CONTEXT': {
+ 'condition': 'OK!',
+ # Django templating does not allow definition of tuples in the
+ # templates.
+ # Make sure this is same as test_templates_jinja2/test_complex.
+ 'my_names': ('js/one.js', 'js/nonasc.js'),
+ }
+ }
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().handle_inner(engines=[engine], verbosity=0)
+ self.assertEqual(3, count)
+ self.assertEqual([
+ self._render_script('76a82cfab9ab'),
+ self._render_script('7219642b8ab4'),
+ self._render_script('567bb77b13db')
+ ], result)
+ rendered_template = self._render_template(engine)
+ self.assertEqual(
+ rendered_template, self._render_result([result[0], result[2]], ''))
+
+
+class OfflineCompressExtendsRecursionTestCase(OfflineTestCaseMixin, TestCase):
+ """
+ Test that templates extending templates with the same name
+ (e.g. admin/index.html) don't cause an infinite test_extends_recursion
+ """
+ templates_dir = 'test_extends_recursion'
+
+ INSTALLED_APPS = [
+ 'django.contrib.admin',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.staticfiles',
+ 'compressor',
+ ]
+
+ @override_settings(INSTALLED_APPS=INSTALLED_APPS)
+ def _test_offline(self, engine):
+ count, _ = CompressCommand().handle_inner(engines=[engine], verbosity=0)
+ self.assertEqual(count, 1)
+
+
+class TestCompressCommand(OfflineTestCaseMixin, TestCase):
+ templates_dir = "test_compress_command"
+
+ def _test_offline(self, engine):
+ raise SkipTest("Not utilized for this test case")
+
+ def _build_expected_manifest(self, expected):
+ return {
+ k: self._render_script(v) for k, v in expected.items()
+ }
+
+ def test_multiple_engines(self):
+ opts = {
+ "force": True,
+ "verbosity": 0,
+ }
+
+ call_command('compress', engines=["django"], **opts)
+ manifest_django = get_offline_manifest()
+ manifest_django_expected = self._build_expected_manifest(
+ {'0fed9c02607acba22316a328075a81a74e0983ae79470daa9d3707a337623dc3': '0241107e9a9a'})
+ self.assertEqual(manifest_django, manifest_django_expected)
+
+ call_command('compress', engines=["jinja2"], **opts)
+ manifest_jinja2 = get_offline_manifest()
+ manifest_jinja2_expected = self._build_expected_manifest(
+ {'077408d23d4a829b8f88db2eadcf902b29d71b14f94018d900f38a3f8ed24c94': '5694ca83dd14'})
+ self.assertEqual(manifest_jinja2, manifest_jinja2_expected)
+
+ call_command('compress', engines=["django", "jinja2"], **opts)
+ manifest_both = get_offline_manifest()
+ manifest_both_expected = self._build_expected_manifest(
+ {'0fed9c02607acba22316a328075a81a74e0983ae79470daa9d3707a337623dc3': '0241107e9a9a',
+ '077408d23d4a829b8f88db2eadcf902b29d71b14f94018d900f38a3f8ed24c94': '5694ca83dd14'})
+ self.assertEqual(manifest_both, manifest_both_expected)
+
+
+class OfflineCompressTestCaseWithLazyStringAlikeUrls(OfflineCompressTestCaseWithContextGenerator):
+ """
+ Test offline compressing with ``STATIC_URL`` and ``COMPRESS_URL`` as instances of
+ *lazy string-alike objects* instead of strings.
+
+ In particular, lazy string-alike objects that add ``SCRIPT_NAME`` WSGI param
+ as URL path prefix.
+
+ For example:
+
+ - We've generated offline assets and deployed them with our Django project.
+ - We've configured HTTP server (e.g. Nginx) to serve our app at two different URLs:
+ ``http://example.com/my/app/`` and ``http://app.example.com/``.
+ - Both URLs are leading to the same app, but in the first case we pass
+ ``SCRIPT_NAME = /my/app/`` to WSGI app server (e.g. to uWSGI, which is *behind* Nginx).
+ - Django (1.11.7, as of today) *ignores* ``SCRIPT_NAME`` when generating
+ static URLs, while it uses ``SCRIPT_NAME`` when generating Django views URLs -
+ see https://code.djangoproject.com/ticket/25598.
+ - As a solution - we can use a lazy string-alike object instead of ``str`` for ``STATIC_URL``
+ so it will know about ``SCRIPT_NAME`` and add it as a prefix every time we do any
+ string operation with ``STATIC_URL``.
+ - However, there are some cases when we cannot force CPython to render our lazy string
+ correctly - e.g. ``some_string.replace(STATIC_URL, '...')``. So we need to do explicit
+ ``str`` type cast: ``some_string.replace(str(STATIC_URL), '...')``.
+ """
+ templates_dir = 'test_static_templatetag'
+ additional_test_settings = {
+ 'STATIC_URL': LazyScriptNamePrefixedUrl('/static/'),
+ 'COMPRESS_URL': LazyScriptNamePrefixedUrl('/static/'),
+ # We use ``COMPRESS_OFFLINE_CONTEXT`` generator to make sure that
+ # ``STATIC_URL`` is not cached when rendering the template.
+ 'COMPRESS_OFFLINE_CONTEXT': (
+ 'compressor.tests.test_offline.static_url_context_generator'
+ )
+ }
+ expected_hash = 'be0b1eade28b'
+
+ def _test_offline(self, engine):
+ count, result = CompressCommand().handle_inner(engines=[engine], verbosity=0)
+ self.assertEqual(1, count)
+
+ # Change ``SCRIPT_NAME`` WSGI param - it can be changed on every HTTP request,
+ # e.g. passed via HTTP header.
+ for script_name in ['', '/app/prefix/', '/another/prefix/']:
+ with script_prefix(script_name):
+ self.assertEqual(
+ six.text_type(settings.STATIC_URL),
+ script_name.rstrip('/') + '/static/'
+ )
+
+ self.assertEqual(
+ six.text_type(settings.COMPRESS_URL),
+ script_name.rstrip('/') + '/static/'
+ )
+
+ expected_result = self._render_result(result)
+ actual_result = self._render_template(engine)
+
+ self.assertEqual(actual_result, expected_result)
+ self.assertIn(six.text_type(settings.COMPRESS_URL), actual_result)
diff --git a/compressor/tests/test_parsers.py b/compressor/tests/test_parsers.py
index 6b37503..a4975d2 100644
--- a/compressor/tests/test_parsers.py
+++ b/compressor/tests/test_parsers.py
@@ -147,10 +147,7 @@ class BeautifulSoupParserTests(ParserTestCase, CompressorTestCase):
@override_settings(COMPRESS_ENABLED=False)
def test_css_return_if_off(self):
- # in addition to unspecified attribute order,
- # bs4 output doesn't have the extra space, so we add that here
- fixed_output = self.css_node.output().replace('"/>', '" />')
- self.assertEqual(len(self.css), len(fixed_output))
+ self.assertEqual(len(self.css), len(self.css_node.output()))
class HtmlParserTests(ParserTestCase, CompressorTestCase):
diff --git a/compressor/tests/test_sekizai.py b/compressor/tests/test_sekizai.py
index c8c5fd7..d1809dd 100644
--- a/compressor/tests/test_sekizai.py
+++ b/compressor/tests/test_sekizai.py
@@ -24,7 +24,7 @@ class TestSekizaiCompressorExtension(TestCase):
html = template.render(context).strip()
self.assertEqual(html,
'''<script src="https://code.jquery.com/jquery-3.3.1.min.js" type="text/javascript"></script>
-<script type="text/javascript" src="/static/CACHE/js/output.cb5ca6a608a4.js"></script>
+<script src="/static/CACHE/js/output.e682d84f6b17.js"></script>
<script async="async" defer="defer" src="https://maps.googleapis.com/maps/api/js?key=XYZ"></script>''')
def test_postprocess_css(self):
@@ -38,5 +38,5 @@ class TestSekizaiCompressorExtension(TestCase):
context = SekizaiContext()
html = template.render(context).strip()
self.assertEqual(html,
-'''<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/css/select2.min.css" rel="stylesheet" type="text/css" />
-<link rel="stylesheet" href="/static/CACHE/css/output.20f9b535162f.css" type="text/css" />''')
+'''<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/css/select2.min.css" rel="stylesheet" type="text/css">
+<link rel="stylesheet" href="/static/CACHE/css/output.20f9b535162f.css" type="text/css">''')
diff --git a/compressor/tests/test_sekizai.py.orig b/compressor/tests/test_sekizai.py.orig
new file mode 100644
index 0000000..5eab344
--- /dev/null
+++ b/compressor/tests/test_sekizai.py.orig
@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+from __future__ import with_statement, unicode_literals
+
+from django.template import Template
+from django.test import TestCase
+from sekizai.context import SekizaiContext
+
+
+class TestSekizaiCompressorExtension(TestCase):
+ """
+ Test case for Sekizai extension.
+ """
+ def test_postprocess_js(self):
+ template_string = '''
+{% load static compress sekizai_tags %}
+{% addtoblock "js" %}<script src="{% static 'js/one.js' %}" type="text/javascript"></script>{% endaddtoblock %}
+{% addtoblock "js" %}<script async="async" defer="defer" src="https://maps.googleapis.com/maps/api/js?key={{ apiKey }}"></script>{% endaddtoblock %}
+{% addtoblock "js" %}<script src="{% static 'js/two.js' %}" type="text/javascript"></script>{% endaddtoblock %}
+{% addtoblock "js" %}<script src="https://code.jquery.com/jquery-3.3.1.min.js" type="text/javascript"></script>{% endaddtoblock %}
+{% addtoblock "js" %}<script src="{% static 'js/three.js' %}" type="text/javascript"></script>{% endaddtoblock %}
+{% render_block "js" postprocessor "compressor.contrib.sekizai.compress" %}'''
+ template = Template(template_string)
+ context = SekizaiContext({'apiKey': 'XYZ'})
+ html = template.render(context).strip()
+ self.assertEqual(html,
+'''<script src="https://code.jquery.com/jquery-3.3.1.min.js" type="text/javascript"></script>
+<<<<<<< HEAD
+<script type="text/javascript" src="/static/CACHE/js/output.cb5ca6a608a4.js"></script>
+=======
+<script src="/static/CACHE/js/output.e682d84f6b17.js"></script>
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+<script async="async" defer="defer" src="https://maps.googleapis.com/maps/api/js?key=XYZ"></script>''')
+
+ def test_postprocess_css(self):
+ template_string = '''
+{% load static compress sekizai_tags %}
+{% addtoblock "css" %}<link href="{% static 'css/one.css' %}" rel="stylesheet" type="text/css" />{% endaddtoblock %}
+{% addtoblock "css" %}<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/css/select2.min.css" rel="stylesheet" type="text/css" />{% endaddtoblock %}
+{% addtoblock "css" %}<link href="{% static 'css/two.css' %}" rel="stylesheet" type="text/css" />{% endaddtoblock %}
+{% render_block "css" postprocessor "compressor.contrib.sekizai.compress" %}'''
+ template = Template(template_string)
+ context = SekizaiContext()
+ html = template.render(context).strip()
+ self.assertEqual(html,
+<<<<<<< HEAD
+'''<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/css/select2.min.css" rel="stylesheet" type="text/css" />
+<link rel="stylesheet" href="/static/CACHE/css/output.20f9b535162f.css" type="text/css" />''')
+=======
+'''<link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.5/css/select2.min.css" rel="stylesheet" type="text/css">
+<link rel="stylesheet" href="/static/CACHE/css/output.20f9b535162f.css" type="text/css">''')
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
diff --git a/compressor/tests/test_signals.py b/compressor/tests/test_signals.py
index 6ba855e..447014b 100644
--- a/compressor/tests/test_signals.py
+++ b/compressor/tests/test_signals.py
@@ -16,9 +16,9 @@ from compressor.signals import post_compress
class PostCompressSignalTestCase(TestCase):
def setUp(self):
self.css = """\
-<link rel="stylesheet" href="/static/css/one.css" type="text/css" />
+<link rel="stylesheet" href="/static/css/one.css" type="text/css">
<style type="text/css">p { border:5px solid green;}</style>
-<link rel="stylesheet" href="/static/css/two.css" type="text/css" />"""
+<link rel="stylesheet" href="/static/css/two.css" type="text/css">"""
self.css_node = CssCompressor('css', self.css)
self.js = """\
@@ -59,7 +59,7 @@ class PostCompressSignalTestCase(TestCase):
css = """\
<link rel="stylesheet" href="/static/css/one.css" media="handheld" type="text/css" />
<style type="text/css" media="print">p { border:5px solid green;}</style>
-<link rel="stylesheet" href="/static/css/two.css" type="text/css" />"""
+<link rel="stylesheet" href="/static/css/two.css" type="text/css">"""
css_node = CssCompressor('css', css)
def listener(sender, **kwargs):
diff --git a/compressor/tests/test_signals.py.orig b/compressor/tests/test_signals.py.orig
new file mode 100644
index 0000000..6df72ee
--- /dev/null
+++ b/compressor/tests/test_signals.py.orig
@@ -0,0 +1,78 @@
+from django.test import TestCase
+from django.test.utils import override_settings
+
+from mock import Mock
+
+from compressor.css import CssCompressor
+from compressor.js import JsCompressor
+from compressor.signals import post_compress
+
+
+@override_settings(
+ COMPRESS_ENABLED=True,
+ COMPRESS_PRECOMPILERS=(),
+ COMPRESS_DEBUG_TOGGLE='nocompress'
+)
+class PostCompressSignalTestCase(TestCase):
+ def setUp(self):
+ self.css = """\
+<link rel="stylesheet" href="/static/css/one.css" type="text/css">
+<style type="text/css">p { border:5px solid green;}</style>
+<<<<<<< HEAD
+<link rel="stylesheet" href="/static/css/two.css" type="text/css" />"""
+=======
+<link rel="stylesheet" href="/static/css/two.css" type="text/css">"""
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+ self.css_node = CssCompressor('css', self.css)
+
+ self.js = """\
+<script src="/static/js/one.js" type="text/javascript"></script>
+<script type="text/javascript">obj.value = "value";</script>"""
+ self.js_node = JsCompressor('js', self.js)
+
+ def tearDown(self):
+ post_compress.disconnect()
+
+ def test_js_signal_sent(self):
+ def listener(sender, **kwargs):
+ pass
+ callback = Mock(wraps=listener)
+ post_compress.connect(callback)
+ self.js_node.output()
+ args, kwargs = callback.call_args
+ self.assertEqual(JsCompressor, kwargs['sender'])
+ self.assertEqual('js', kwargs['type'])
+ self.assertEqual('file', kwargs['mode'])
+ context = kwargs['context']
+ assert 'url' in context['compressed']
+
+ def test_css_signal_sent(self):
+ def listener(sender, **kwargs):
+ pass
+ callback = Mock(wraps=listener)
+ post_compress.connect(callback)
+ self.css_node.output()
+ args, kwargs = callback.call_args
+ self.assertEqual(CssCompressor, kwargs['sender'])
+ self.assertEqual('css', kwargs['type'])
+ self.assertEqual('file', kwargs['mode'])
+ context = kwargs['context']
+ assert 'url' in context['compressed']
+
+ def test_css_signal_multiple_media_attributes(self):
+ css = """\
+<link rel="stylesheet" href="/static/css/one.css" media="handheld" type="text/css" />
+<style type="text/css" media="print">p { border:5px solid green;}</style>
+<<<<<<< HEAD
+<link rel="stylesheet" href="/static/css/two.css" type="text/css" />"""
+=======
+<link rel="stylesheet" href="/static/css/two.css" type="text/css">"""
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+ css_node = CssCompressor('css', css)
+
+ def listener(sender, **kwargs):
+ pass
+ callback = Mock(wraps=listener)
+ post_compress.connect(callback)
+ css_node.output()
+ self.assertEqual(3, callback.call_count)
diff --git a/compressor/tests/test_templatetags.py b/compressor/tests/test_templatetags.py
index 8cb199a..37c1dcc 100644
--- a/compressor/tests/test_templatetags.py
+++ b/compressor/tests/test_templatetags.py
@@ -98,7 +98,7 @@ class TemplatetagTestCase(TestCase):
<script type="text/javascript">obj.value = "value";</script>
{% endcompress %}
"""
- out = '<script type="text/javascript" src="/static/CACHE/js/output.74e158ccb432.js"></script>'
+ out = '<script src="/static/CACHE/js/output.8a0fed36c317.js"></script>'
self.assertEqual(out, render(template, self.context))
def test_nonascii_js_tag(self):
@@ -107,7 +107,7 @@ class TemplatetagTestCase(TestCase):
<script type="text/javascript">var test_value = "\u2014";</script>
{% endcompress %}
"""
- out = '<script type="text/javascript" src="/static/CACHE/js/output.a18195c6ae48.js"></script>'
+ out = '<script src="/static/CACHE/js/output.8c00f1cf1e0a.js"></script>'
self.assertEqual(out, render(template, self.context))
def test_nonascii_latin1_js_tag(self):
@@ -116,7 +116,7 @@ class TemplatetagTestCase(TestCase):
<script type="text/javascript">var test_value = "\u2014";</script>
{% endcompress %}
"""
- out = '<script type="text/javascript" src="/static/CACHE/js/output.f64debbd8878.js"></script>'
+ out = '<script src="/static/CACHE/js/output.06a98ccfd380.js"></script>'
self.assertEqual(out, render(template, self.context))
def test_compress_tag_with_illegal_arguments(self):
@@ -151,7 +151,7 @@ class TemplatetagTestCase(TestCase):
<link rel="stylesheet" href="{{ STATIC_URL }}css/two.css" type="text/css">
{% endcompress %}"""
- out_js = '<script type="text/javascript">;obj={};;obj.value="value";</script>'
+ out_js = '<script>obj={};;obj.value="value";;</script>'
out_css = '\n'.join(('<style type="text/css">body { background:#990; }',
'p { border:5px solid green;}',
'body { color:#fff; }</style>'))
@@ -177,7 +177,7 @@ class TemplatetagTestCase(TestCase):
<script type="text/javascript">var tmpl="{% templatetag openblock %} if x == 3 %}x IS 3{% templatetag openblock %} endif %}"</script>
{% endaddtoblock %}{% render_block "js" postprocessor "compressor.contrib.sekizai.compress" %}
"""
- out = '<script type="text/javascript" src="/static/CACHE/js/output.4d88842b99b3.js"></script>'
+ out = '<script src="/static/CACHE/js/output.ffc39dec05fd.js"></script>'
self.assertEqual(out, render(template, self.context, SekizaiContext))
@@ -206,7 +206,7 @@ class PrecompilerTemplatetagTestCase(TestCase):
template = """{% load compress %}{% compress js %}
<script type="text/coffeescript"># this is a comment.</script>
{% endcompress %}"""
- out = script(src="/static/CACHE/js/output.fb128b610c3e.js")
+ out = script(src="/static/CACHE/js/output.ec862f0ff42c.js")
self.assertEqual(out, render(template, self.context))
def test_compress_coffeescript_tag_and_javascript_tag(self):
@@ -214,7 +214,7 @@ class PrecompilerTemplatetagTestCase(TestCase):
<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/output.cf3495aaff6e.js")
+ out = script(src="/static/CACHE/js/output.fb4a0d84e914.js")
self.assertEqual(out, render(template, self.context))
@override_settings(COMPRESS_ENABLED=False)
@@ -224,7 +224,7 @@ class PrecompilerTemplatetagTestCase(TestCase):
<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.'))
+ script('# this too is a comment.', scripttype="text/javascript"))
self.assertEqual(out, render(template, self.context))
@override_settings(COMPRESS_ENABLED=False)
@@ -272,8 +272,8 @@ class PrecompilerTemplatetagTestCase(TestCase):
<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))
@@ -287,13 +287,13 @@ class PrecompilerTemplatetagTestCase(TestCase):
<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.222f958fb191.css" type="text/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">',
+ '<link rel="stylesheet" href="/static/CACHE/css/test.222f958fb191.css" type="text/css">'])
self.assertEqual(out, render(template, self.context))
-def script(content="", src="", scripttype="text/javascript"):
+def script(content="", src="", scripttype=""):
"""
returns a unicode text html script element.
diff --git a/docs/changelog.txt b/docs/changelog.txt
index a61ddb6..afe8ea9 100644
--- a/docs/changelog.txt
+++ b/docs/changelog.txt
@@ -1,7 +1,7 @@
Changelog
=========
-v2.3 (2019-06-31)
+v2.3 (2019-05-31)
-----------------
`Full Changelog <https://github.com/django-compressor/django-compressor/compare/2.2...2.3>`_
diff --git a/docs/changelog.txt.orig b/docs/changelog.txt.orig
new file mode 100644
index 0000000..d33708f
--- /dev/null
+++ b/docs/changelog.txt.orig
@@ -0,0 +1,542 @@
+Changelog
+=========
+
+<<<<<<< HEAD
+v2.3 (2019-06-31)
+=======
+v2.3 (2019-05-31)
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+-----------------
+
+`Full Changelog <https://github.com/django-compressor/django-compressor/compare/2.2...2.3>`_
+
+- Drop support for Django 1.8, 1.9 and 1.10
+- Add support for Django 2.1 and 2.2, as well as Python 3.7
+- Update all dependencies. This required minor code changes, you might need to update some optional dependencies if you use any
+- Allow the mixed use of JS/CSS in Sekizai's templatetags `{% addtoblock "js" %}` and `{% addtoblock "css" %}` (#891)
+- Allow the implementation of new types other than css and js. (#900)
+- Update jinja2 extension to behave similar to the django tag (#899)
+- Fix crash in offline compression when child nodelist is None, again (#605)
+- Support STATIC_URL and COMPRESS_URL being string-like objects
+- Improve compress command memory usage (#870)
+- Ensure generated file always contains a base name (#775)
+- Add BrotliCompressorFileStorage (#867)
+
+v2.2 (2017-08-16)
+-----------------
+
+`Full Changelog <https://github.com/django-compressor/django-compressor/compare/2.1.1...2.2>`_
+
+- Switch from MD5 to SHA256 for hashes generation.
+
+- Add Django 1.11 compatibility
+
+- Various compatibility fixes for Python 3.6 and Django 1.8
+
+- Made OfflineGenerationError easier to debug
+
+- Drop support for Python 3.2
+
+- Add new CssRelativeFilter which works like CssAbsoluteFilter but outputs relative URLs.
+
+- Fix URL CssAbsoluteFilter URL detection
+
+v2.1.1 (2017-02-02)
+-------------------
+
+- Fix to file permissions issue with packaging.
+
+
+v2.1 (2016-08-09)
+-----------------
+
+`Full Changelog <https://github.com/django-compressor/django-compressor/compare/2.0...2.1>`_
+
+- Add Django 1.10 compatibility
+
+- Add support for inheritance using a variable in offline compression
+
+- Fix recursion error with offline compression when extending templates with the same name
+
+- Fix UnicodeDecodeError when using CompilerFilter and caching
+
+- Fix CssAbsoluteFilter changing double quotes to single quotes, breaking SVG
+
+
+v2.0 (2016-01-07)
+-----------------
+
+`Full Changelog <https://github.com/django-compressor/django-compressor/compare/1.6...2.0>`_
+
+- Add Django 1.9 compatibility
+
+- Remove official support for Django 1.4 and 1.7
+
+- Add official support for Python 3.5
+
+- Remove official support for Python 2.6
+
+- Remove support for coffin and jingo
+
+- Fix Jinja2 compatibility for Django 1.8+
+
+- Stop bundling vendored versions of rcssmin and rjsmin, make them proper dependencies
+
+- Remove support for CSSTidy
+
+- Remove support for beautifulsoup 3.
+
+- Replace cssmin by csscompressor (cssmin is still available for backwards-compatibility but points to rcssmin)
+
+
+v1.6 (2015-11-19)
+-----------------
+
+`Full Changelog <https://github.com/django-compressor/django-compressor/compare/1.5...1.6>`_
+
+- Upgrade rcssmin and rjsmin
+
+- Apply CssAbsoluteFilter to precompiled css even when compression is disabled
+
+- Add optional caching to CompilerFilter to avoid re-compiling unchanged files
+
+- Fix various deprecation warnings on Django 1.7 / 1.8
+
+- Fix TemplateFilter
+
+- Fix double-rendering bug with sekizai extension
+
+- Fix debug mode using destination directory instead of staticfiles finders first
+
+- Removed some silent exception catching in compress command
+
+
+v1.5 (2015-03-27)
+-----------------
+
+`Full Changelog <https://github.com/django-compressor/django-compressor/compare/1.4...1.5>`_
+
+- Fix compress command and run automated tests for Django 1.8
+
+- Fix Django 1.8 warnings
+
+- Handle TypeError from import_module
+
+- Fix reading UTF-8 files which have BOM
+
+- Fix incompatibility with Windows (shell_quote is not supported)
+
+- Run automated tests on Django 1.7
+
+- Ignore non-existent {{ block.super }} in offline compression instead of raising AttributeError
+
+- Support for clean-css
+
+- Fix link markup
+
+- Add support for COMPRESS_CSS_HASHING_METHOD = None
+
+- Remove compatibility with old 'staticfiles' app
+
+- In compress command, use get_template() instead of opening template files manually, fixing compatibility issues with custom template loaders
+
+- Fix FilterBase so that does not override self.type for subclasses if filter_type is not specified at init
+
+- Remove unnecessary filename and existence checks in CssAbsoluteFilter
+
+
+v1.4 (2014-06-20)
+-----------------
+
+- Added Python 3 compatibility.
+
+- Added compatibility with Django 1.6.x and dropped support for Django 1.3.X.
+
+- Fixed compatibility with html5lib 1.0.
+
+- Added offline compression for Jinja2 with Jingo and Coffin integration.
+
+- Improved support for template inheritance in offline compression.
+
+- Made offline compression avoid compressing the same block multiple times.
+
+- Added a ``testenv`` target in the Makefile to make it easier to set up the
+ test environment.
+
+- Allowed data-uri filter to handle external/protocol-relative references.
+
+- Made ``CssCompressor`` class easier to extend.
+
+- Added support for explicitly stating the block being ended.
+
+- Added rcssmin and updated rjsmin.
+
+- Removed implicit requirement on BeautifulSoup.
+
+- Made GzipCompressorFileStorage set access and modified times to the same time
+ as the corresponding base file.
+
+- Defaulted to using django's simplejson, if present.
+
+- Fixed CompilerFilter to always output Unicode strings.
+
+- Fixed windows line endings in offline compression.
+
+v1.3 (2013-03-18)
+-----------------
+
+- *Backward incompatible changes*
+
+ - Dropped support for Python 2.5. Removed ``any`` and ``walk`` compatibility
+ functions in ``compressor.utils``.
+
+ - Removed compatibility with some old django setttings:
+
+ - :attr:`~COMPRESS_ROOT` no longer uses ``MEDIA_ROOT`` if ``STATIC_ROOT`` is
+ not defined. It expects ``STATIC_ROOT`` to be defined instead.
+
+ - :attr:`~COMPRESS_URL` no longer uses ``MEDIA_URL`` if ``STATIC_URL`` is
+ not defined. It expects ``STATIC_URL`` to be defined instead.
+
+ - :attr:`~COMPRESS_CACHE_BACKEND` no longer uses ``CACHE_BACKEND`` and simply
+ defaults to ``default``.
+
+- Added precompiler class support. This enables you to write custom precompilers
+ with Python logic in them instead of just relying on executables.
+
+- Made CssAbsoluteFilter smarter: it now handles URLs with hash fragments or
+ querystring correctly. In addition, it now leaves alone fragment-only URLs.
+
+- Removed a ``fsync()`` call in ``CompilerFilter`` to improve performance.
+ We already called ``self.infile.flush()`` so that call was not necessary.
+
+- Added an extension to provide django-sekizai support.
+ See :ref:`django-sekizai Support <django-sekizai_support>` for more
+ information.
+
+- Fixed a ``DeprecationWarning`` regarding the use of ``django.utils.hashcompat``
+
+- Updated bundled ``rjsmin.py`` to fix some JavaScript compression errors.
+
+v1.2
+----
+
+- Added compatibility with Django 1.4 and dropped support for Django 1.2.X.
+
+- Added contributing docs. Be sure to check them out and start contributing!
+
+- Moved CI to Travis: http://travis-ci.org/django-compressor/django-compressor
+
+- Introduced a new ``compressed`` context dictionary that is passed to
+ the templates that are responsible for rendering the compressed snippets.
+
+ This is a **backwards-incompatible change** if you've overridden any of
+ the included templates:
+
+ - ``compressor/css_file.html``
+ - ``compressor/css_inline.html``
+ - ``compressor/js_file.html``
+ - ``compressor/js_inline.html``
+
+ The variables passed to those templates have been namespaced in a
+ dictionary, so it's easy to fix your own templates.
+
+ For example, the old ``compressor/js_file.html``::
+
+ <script type="text/javascript" src="{{ url }}"></script>
+
+ The new ``compressor/js_file.html``::
+
+ <script type="text/javascript" src="{{ compressed.url }}"></script>
+
+- Removed old templates named ``compressor/css.html`` and
+ ``compressor/js.html`` that were originally left for backwards
+ compatibility. If you've overridden them, just rename them to
+ ``compressor/css_file.html`` or ``compressor/js_file.html`` and
+ make sure you've accounted for the backwards incompatible change
+ of the template context mentioned above.
+
+- Reverted an unfortunate change to the YUI filter that prepended
+ ``'java -jar'`` to the binary name, which doesn't alway work, e.g.
+ if the YUI compressor is shipped as a script like
+ ``/usr/bin/yui-compressor``.
+
+- Changed the sender parameter of the :func:`~compressor.signals.post_compress`
+ signal to be either :class:`compressor.css.CssCompressor` or
+ :class:`compressor.js.JsCompressor` for easier customization.
+
+- Correctly handle offline compressing files that are found in ``{% if %}``
+ template blocks.
+
+- Renamed the second option for the ``COMPRESS_CSS_HASHING_METHOD`` setting
+ from ``'hash'`` to ``'content'`` to better describe what it does. The old
+ name is also supported, as well as the default being ``'mtime'``.
+
+- Fixed CssAbsoluteFilter, ``src`` attributes in includes now get transformed.
+
+- Added a new hook to allow developers to completely bypass offline
+ compression in CompressorNode subclasses: ``is_offline_compression_enabled``.
+
+- Dropped versiontools from required dependencies again.
+
+v1.1.2
+------
+
+- Fixed an installation issue related to versiontools.
+
+v1.1.1
+------
+
+- Fixed a stupid ImportError bug introduced in 1.1.
+
+- Fixed Jinja2 docs of since ``JINJA2_EXTENSIONS`` expects
+ a class, not a module.
+
+- Fixed a Windows bug with regard to file resolving with
+ staticfiles finders.
+
+- Stopped a potential memory leak when memoizing the rendered
+ output.
+
+- Fixed the integration between staticfiles (e.g. in Django <= 1.3.1)
+ and compressor which prevents the collectstatic management command
+ to work.
+
+ .. warning::
+
+ Make sure to **remove** the ``path`` method of your custom
+ :ref:`remote storage <remote_storages>` class!
+
+v1.1
+----
+
+- Made offline compression completely independent from cache (by writing a
+ manifest.json file).
+
+ You can now easily run the :ref:`compress <pre-compression>` management
+ command locally and transfer the :attr:`~django.conf.settings.COMPRESS_ROOT`
+ dir to your server.
+
+- Updated installation instructions to properly mention all dependencies,
+ even those internally used.
+
+- Fixed a bug introduced in 1.0 which would prevent the proper deactivation
+ of the compression in production.
+
+- Added a Jinja2_ :doc:`contrib extension </jinja2>`.
+
+- Made sure the rel attribute of link tags can be mixed case.
+
+- Avoid overwriting context variables needed for compressor to work.
+
+- Stopped the compress management command to require model validation.
+
+- Added missing imports and fixed a few :pep:`8` issues.
+
+.. _Jinja2: http://jinja.pocoo.org/2/
+
+v1.0.1
+------
+
+- Fixed regression in ``compressor.utils.staticfiles`` compatibility
+ module.
+
+v1.0
+----
+
+- **BACKWARDS-INCOMPATIBLE** Stopped swallowing exceptions raised by
+ rendering the template tag in production (``DEBUG = False``). This
+ has the potential to breaking lots of apps but on the other hand
+ will help find bugs.
+
+- **BACKWARDS-INCOMPATIBLE** The default function to create the cache
+ key stopped containing the server hostname. Instead the cache key
+ now only has the form ``'django_compressor.<KEY>'``.
+
+ To revert to the previous way simply set the ``COMPRESS_CACHE_KEY_FUNCTION``
+ to ``'compressor.cache.socket_cachekey'``.
+
+- **BACKWARDS-INCOMPATIBLE** Renamed ambigously named
+ ``COMPRESS_DATA_URI_MAX_SIZE`` setting to ``COMPRESS_DATA_URI_MAX_SIZE``.
+ It's the maximum size the ``compressor.filters.datauri.DataUriFilter``
+ filter will embed files as data: URIs.
+
+- Added ``COMPRESS_CSS_HASHING_METHOD`` setting with the options ``'mtime'``
+ (default) and ``'hash'`` for the ``CssAbsoluteFilter`` filter. The latter
+ uses the content of the file to calculate the cache-busting hash.
+
+- Added support for ``{{ block.super }}`` to ``compress`` management command.
+
+- Dropped Django 1.1.X support.
+
+- Fixed compiler filters on Windows.
+
+- Handle new-style cached template loaders in the compress management command.
+
+- Documented included filters.
+
+- Added `Slim It`_ filter.
+
+- Added new CallbackOutputFilter to ease the implementation of Python-based
+ callback filters that only need to pass the content to a callable.
+
+- Make use of `django-appconf`_ for settings handling and `versiontools`_
+ for versions.
+
+- Uses the current context when rendering the render templates.
+
+- Added :func:`post_compress<compressor.signals.post_compress>` signal.
+
+.. _`Slim It`: http://slimit.org/
+.. _`django-appconf`: https://django-appconf.readthedocs.io/
+.. _`versiontools`: http://pypi.python.org/pypi/versiontools
+
+v0.9.2
+------
+
+- Fixed stdin handling of precompiler filter.
+
+v0.9.1
+------
+
+- Fixed encoding related issue.
+
+- Minor cleanups.
+
+v0.9
+----
+
+- Fixed the precompiler support to also use the full file path instead of a
+ temporarily created file.
+
+- Enabled test coverage.
+
+- Refactored caching and other utility code.
+
+- Switched from SHA1 to MD5 for hash generation to lower the computational impact.
+
+v0.8
+----
+
+- Replace naive jsmin.py with rJSmin (http://opensource.perlig.de/rjsmin/)
+ and fixed a few problems with JavaScript comments.
+
+- Fixed converting relative URLs in CSS files when running in debug mode.
+
+.. note::
+
+ If you relied on the ``split_contents`` method of ``Compressor`` classes,
+ please make sure a fourth item is returned in the iterable that denotes
+ the base name of the file that is compressed.
+
+v0.7.1
+------
+
+- Fixed import error when using the standalone django-staticfiles app.
+
+v0.7
+----
+
+- Created new parser, HtmlParser, based on the stdlib HTMLParser module.
+
+- Added a new default AutoSelectParser, which picks the LxmlParser if lxml
+ is available and falls back to HtmlParser.
+
+- Use unittest2 for testing goodness.
+
+- Fixed YUI JavaScript filter argument handling.
+
+- Updated bundled jsmin to use version by Dave St.Germain that was refactored for speed.
+
+v0.6.4
+------
+
+- Fixed Closure filter argument handling.
+
+v0.6.3
+------
+
+- Fixed options mangling in CompilerFilter initialization.
+
+- Fixed tox configuration.
+
+- Extended documentation and README.
+
+- In the compress command ignore hidden files when looking for templates.
+
+- Restructured utilities and added staticfiles compat layer.
+
+- Restructered parsers and added a html5lib based parser.
+
+v0.6.2
+------
+
+- Minor bugfixes that caused the compression not working reliably in
+ development mode (e.g. updated files didn't trigger a new compression).
+
+v0.6.1
+------
+
+- Fixed staticfiles support to also use its finder API to find files during
+ development -- when the static files haven't been collected in
+ ``STATIC_ROOT``.
+
+- Fixed regression with the ``COMPRESS`` setting, pre-compilation and
+ staticfiles.
+
+v0.6
+----
+
+Major improvements and a lot of bugfixes, some of which are:
+
+- New precompilation support, which allows compilation of files and
+ hunks with easily configurable compilers before calling the actual
+ output filters. See the
+ :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS` for more details.
+
+- New staticfiles support. With the introduction of the staticfiles app
+ to Django 1.3, compressor officially supports finding the files to
+ compress using the app's finder API. Have a look at the documentation
+ about :ref:`remote storages <remote_storages>` in case you want to use
+ those together with compressor.
+
+- New ``compress`` management command which allows pre-running of what the
+ compress template tag does. See the
+ :ref:`pre-compression <pre-compression>` docs for more information.
+
+- Various performance improvements by better caching and mtime cheking.
+
+- Deprecated ``COMPRESS_LESSC_BINARY`` setting because it's now
+ superseded by the :attr:`~django.conf.settings.COMPRESS_PRECOMPILERS`
+ setting. Just make sure to use the correct mimetype when linking to less
+ files or adding inline code and add the following to your settings::
+
+ COMPRESS_PRECOMPILERS = (
+ ('text/less', 'lessc {infile} {outfile}'),
+ )
+
+- Added cssmin_ filter (``compressor.filters.CSSMinFilter``) based on
+ Zachary Voase's Python port of the YUI CSS compression algorithm.
+
+- Reimplemented the dog-piling prevention.
+
+- Make sure the CssAbsoluteFilter works for relative paths.
+
+- Added inline render mode. See :ref:`usage <usage>` docs.
+
+- Added ``mtime_cache`` management command to add and/or remove all mtimes
+ from the cache.
+
+- Moved docs to Read The Docs: https://django-compressor.readthedocs.io/en/latest/
+
+- Added optional ``compressor.storage.GzipCompressorFileStorage`` storage
+ backend that gzips of the saved files automatically for easier deployment.
+
+- Reimplemented a few filters on top of the new
+ ``compressor.filters.base.CompilerFilter`` to be a bit more DRY.
+
+- Added tox based test configuration, testing on Django 1.1-1.3 and Python
+ 2.5-2.7.
+
+.. _cssmin: http://pypi.python.org/pypi/cssmin/
+
diff --git a/docs/usage.txt b/docs/usage.txt
index 52b7b6f..ac572c7 100644
--- a/docs/usage.txt
+++ b/docs/usage.txt
@@ -6,7 +6,7 @@ Usage
.. code-block:: django
{% load compress %}
- {% compress <js/css> [<file/inline> [block_name]] %}
+ {% compress <js/css> [<file/inline/preload> [block_name]] %}
<html of inline or linked JS/CSS>
{% endcompress %}
@@ -48,6 +48,21 @@ Result:
obj.value = "value";
</script>
+Adding the ``preload`` parameter will generate the preload tag for the compressed resource in the template:
+
+ .. code-block:: django
+
+ {% compress js preload %}
+ <script src="/static/js/one.js" type="text/javascript" charset="utf-8"></script>
+ {% endcompress %}
+
+Result:
+
+ .. code-block:: django
+
+ <link rel="preload" href="/static/CACHE/js/d01466eb4fc6.js" as="script" />
+
+
Specifying a ``block_name`` will change the output filename. It can also be
accessed in the :ref:`post_compress signal <signals>` in the ``context`` parameter.
diff --git a/requirements/tests.txt b/requirements/tests.txt
index a595d22..428c09a 100644
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -2,7 +2,7 @@ flake8==3.5.0
coverage==4.5.1
html5lib==1.0.1
mock==2.0.0
-Jinja2==2.10.0
+Jinja2==2.10.1
lxml==4.2.5
beautifulsoup4==4.6.3
django-sekizai==0.10.0
diff --git a/requirements/tests.txt.orig b/requirements/tests.txt.orig
new file mode 100644
index 0000000..b4755ae
--- /dev/null
+++ b/requirements/tests.txt.orig
@@ -0,0 +1,16 @@
+flake8==3.5.0
+coverage==4.5.1
+html5lib==1.0.1
+mock==2.0.0
+<<<<<<< HEAD
+Jinja2==2.10.0
+=======
+Jinja2==2.10.1
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+lxml==4.2.5
+beautifulsoup4==4.6.3
+django-sekizai==0.10.0
+csscompressor==0.9.5
+rcssmin==1.0.6
+rjsmin==1.1.0
+brotli==1.0.6
diff --git a/setup.py b/setup.py
index 1e48a28..2c58557 100644
--- a/setup.py
+++ b/setup.py
@@ -144,5 +144,6 @@ setup(
'django-appconf >= 1.0',
'rcssmin == 1.0.6',
'rjsmin == 1.1.0',
+ 'six == 1.12.0',
],
)
diff --git a/setup.py.orig b/setup.py.orig
new file mode 100644
index 0000000..0efaad9
--- /dev/null
+++ b/setup.py.orig
@@ -0,0 +1,152 @@
+from __future__ import print_function
+import ast
+import os
+import sys
+import codecs
+from fnmatch import fnmatchcase
+from distutils.util import convert_path
+from setuptools import setup, find_packages
+
+class VersionFinder(ast.NodeVisitor):
+ def __init__(self):
+ self.version = None
+
+ def visit_Assign(self, node):
+ if node.targets[0].id == '__version__':
+ self.version = node.value.s
+
+
+def read(*parts):
+ filename = os.path.join(os.path.dirname(__file__), *parts)
+ with codecs.open(filename, encoding='utf-8') as fp:
+ return fp.read()
+
+
+def find_version(*parts):
+ finder = VersionFinder()
+ finder.visit(ast.parse(read(*parts)))
+ return finder.version
+
+
+# Provided as an attribute, so you can append to these instead
+# of replicating them:
+standard_exclude = ('*.py', '*.pyc', '*$py.class', '*~', '.*', '*.bak')
+standard_exclude_directories = ('.*', 'CVS', '_darcs', './build',
+ './dist', 'EGG-INFO', '*.egg-info')
+
+
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+# Note: you may want to copy this into your setup.py file verbatim, as
+# you can't import this from another package, when you don't know if
+# that package is installed yet.
+def find_package_data(where='.', package='',
+ exclude=standard_exclude,
+ exclude_directories=standard_exclude_directories,
+ only_in_packages=True,
+ show_ignored=False):
+ """
+ Return a dictionary suitable for use in ``package_data``
+ in a distutils ``setup.py`` file.
+
+ The dictionary looks like::
+
+ {'package': [files]}
+
+ Where ``files`` is a list of all the files in that package that
+ don't match anything in ``exclude``.
+
+ If ``only_in_packages`` is true, then top-level directories that
+ are not packages won't be included (but directories under packages
+ will).
+
+ Directories matching any pattern in ``exclude_directories`` will
+ be ignored; by default directories with leading ``.``, ``CVS``,
+ and ``_darcs`` will be ignored.
+
+ If ``show_ignored`` is true, then all the files that aren't
+ included in package data are shown on stderr (for debugging
+ purposes).
+
+ Note patterns use wildcards, or can be exact paths (including
+ leading ``./``), and all searching is case-insensitive.
+ """
+
+ out = {}
+ stack = [(convert_path(where), '', package, only_in_packages)]
+ while stack:
+ where, prefix, package, only_in_packages = stack.pop(0)
+ for name in os.listdir(where):
+ fn = os.path.join(where, name)
+ if os.path.isdir(fn):
+ bad_name = False
+ for pattern in exclude_directories:
+ if (fnmatchcase(name, pattern) or fn.lower() == pattern.lower()):
+ bad_name = True
+ if show_ignored:
+ print("Directory %s ignored by pattern %s" %
+ (fn, pattern), file=sys.stderr)
+ break
+ if bad_name:
+ continue
+ if (os.path.isfile(os.path.join(fn, '__init__.py')) and not prefix):
+ if not package:
+ new_package = name
+ else:
+ new_package = package + '.' + name
+ stack.append((fn, '', new_package, False))
+ else:
+ stack.append((fn, prefix + name + '/', package, only_in_packages))
+ elif package or not only_in_packages:
+ # is a file
+ bad_name = False
+ for pattern in exclude:
+ if (fnmatchcase(name, pattern) or fn.lower() == pattern.lower()):
+ bad_name = True
+ if show_ignored:
+ print("File %s ignored by pattern %s" %
+ (fn, pattern), file=sys.stderr)
+ break
+ if bad_name:
+ continue
+ out.setdefault(package, []).append(prefix + name)
+ return out
+
+setup(
+ name="django_compressor",
+ version=find_version("compressor", "__init__.py"),
+ url='https://django-compressor.readthedocs.io/en/latest/',
+ license='MIT',
+ description="Compresses linked and inline JavaScript or CSS into single cached files.",
+ long_description=read('README.rst'),
+ author='Jannis Leidel',
+ author_email='jannis@leidel.info',
+ packages=find_packages(),
+ package_data=find_package_data(),
+ classifiers=[
+ 'Development Status :: 5 - Production/Stable',
+ 'Framework :: Django',
+ 'Intended Audience :: Developers',
+ 'License :: OSI Approved :: MIT License',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python',
+ 'Programming Language :: Python :: 2',
+ 'Programming Language :: Python :: 2.7',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.4',
+ 'Programming Language :: Python :: 3.5',
+ 'Programming Language :: Python :: 3.6',
+ 'Programming Language :: Python :: 3.7',
+ 'Topic :: Internet :: WWW/HTTP',
+ ],
+ zip_safe=False,
+ install_requires=[
+ 'django-appconf >= 1.0',
+ 'rcssmin == 1.0.6',
+ 'rjsmin == 1.1.0',
+<<<<<<< HEAD
+=======
+ 'six == 1.12.0',
+>>>>>>> 1b26e63e3ebce49570e57f6a5233af15278a518b
+ ],
+)