Merge branch 'release/1.5'
This commit is contained in:
25
.travis.yml
25
.travis.yml
@@ -3,20 +3,25 @@ before_install:
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install csstidy libxml2-dev libxslt-dev
|
||||
install:
|
||||
- pip install tox coveralls
|
||||
- pip install tox
|
||||
script:
|
||||
- tox
|
||||
env:
|
||||
- TOXENV=py33-1.6.X
|
||||
- TOXENV=py32-1.6.X
|
||||
- TOXENV=py27-1.6.X
|
||||
- TOXENV=py26-1.6.X
|
||||
- TOXENV=py33-1.5.X
|
||||
- TOXENV=py32-1.5.X
|
||||
- TOXENV=py27-1.5.X
|
||||
- TOXENV=py26-1.4.X
|
||||
- TOXENV=py26-1.5.X
|
||||
- TOXENV=py27-1.4.X
|
||||
- TOXENV=py26-1.4.X
|
||||
- TOXENV=py27-1.5.X
|
||||
- TOXENV=py26-1.6.X
|
||||
- TOXENV=py27-1.6.X
|
||||
- TOXENV=py32-1.6.X
|
||||
- TOXENV=py33-1.6.X
|
||||
- TOXENV=py27-1.7.X
|
||||
- TOXENV=py32-1.7.X
|
||||
- TOXENV=py33-1.7.X
|
||||
- TOXENV=py34-1.7.X
|
||||
- TOXENV=py27-1.8.X
|
||||
- TOXENV=py32-1.8.X
|
||||
- TOXENV=py33-1.8.X
|
||||
- TOXENV=py34-1.8.X
|
||||
notifications:
|
||||
irc: "irc.freenode.org#django-compressor"
|
||||
after_success: coveralls
|
||||
|
||||
1
AUTHORS
1
AUTHORS
@@ -29,6 +29,7 @@ Bojan Mihelac
|
||||
Boris Shemigon
|
||||
Brad Whittington
|
||||
Bruno Renié
|
||||
Carlton Gibson
|
||||
Cassus Adam Banko
|
||||
Chris Adams
|
||||
Chris Streeter
|
||||
|
||||
@@ -4,16 +4,19 @@ Django Compressor
|
||||
.. image:: https://coveralls.io/repos/django-compressor/django-compressor/badge.png?branch=develop
|
||||
:target: https://coveralls.io/r/django-compressor/django-compressor?branch=develop
|
||||
|
||||
.. image:: https://pypip.in/v/django_compressor/badge.png
|
||||
.. image:: https://pypip.in/v/django_compressor/badge.svg
|
||||
:target: https://pypi.python.org/pypi/django_compressor
|
||||
|
||||
.. image:: https://pypip.in/d/django_compressor/badge.png
|
||||
.. image:: https://pypip.in/d/django_compressor/badge.svg
|
||||
:target: https://pypi.python.org/pypi/django_compressor
|
||||
|
||||
.. image:: https://secure.travis-ci.org/django-compressor/django-compressor.png?branch=develop
|
||||
.. image:: https://secure.travis-ci.org/django-compressor/django-compressor.svg?branch=develop
|
||||
:alt: Build Status
|
||||
:target: http://travis-ci.org/django-compressor/django-compressor
|
||||
|
||||
.. image:: https://caniusepython3.com/project/django_compressor.svg
|
||||
:target: https://caniusepython3.com/project/django_compressor
|
||||
|
||||
Django Compressor combines and compresses linked and inline Javascript
|
||||
or CSS in a Django template into cacheable static files by using the
|
||||
``compress`` template tag.
|
||||
|
||||
@@ -5,7 +5,10 @@ import codecs
|
||||
from django.core.files.base import ContentFile
|
||||
from django.template import Context
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.importlib import import_module
|
||||
try:
|
||||
from importlib import import_module
|
||||
except:
|
||||
from django.utils.importlib import import_module
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
try:
|
||||
@@ -140,6 +143,9 @@ class Compressor(object):
|
||||
"""
|
||||
Reads file contents using given `charset` and returns it as text.
|
||||
"""
|
||||
if charset == 'utf-8':
|
||||
# Removes BOM
|
||||
charset = 'utf-8-sig'
|
||||
with codecs.open(filename, 'r', charset) as fd:
|
||||
try:
|
||||
return fd.read()
|
||||
@@ -247,7 +253,7 @@ class Compressor(object):
|
||||
mod_name, cls_name = get_mod_func(filter_or_command)
|
||||
try:
|
||||
mod = import_module(mod_name)
|
||||
except ImportError:
|
||||
except (ImportError, TypeError):
|
||||
filter = CompilerFilter(
|
||||
content, filter_type=self.type, filename=filename,
|
||||
charset=charset, command=filter_or_command)
|
||||
|
||||
@@ -4,11 +4,21 @@ import os
|
||||
import socket
|
||||
import time
|
||||
|
||||
from django.core.cache import get_cache
|
||||
try:
|
||||
from django.core.cache import caches
|
||||
def get_cache(name):
|
||||
return caches[name]
|
||||
except ImportError:
|
||||
from django.core.cache import get_cache
|
||||
|
||||
from django.core.files.base import ContentFile
|
||||
from django.utils.encoding import force_text, smart_bytes
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
try:
|
||||
from importlib import import_module
|
||||
except:
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
from compressor.conf import settings
|
||||
from compressor.storage import default_storage
|
||||
@@ -39,7 +49,7 @@ def get_cachekey(*args, **kwargs):
|
||||
mod_name, func_name = get_mod_func(
|
||||
settings.COMPRESS_CACHE_KEY_FUNCTION)
|
||||
_cachekey_func = getattr(import_module(mod_name), func_name)
|
||||
except (AttributeError, ImportError) as e:
|
||||
except (AttributeError, ImportError, TypeError) as e:
|
||||
raise ImportError("Couldn't import cache key function %s: %s" %
|
||||
(settings.COMPRESS_CACHE_KEY_FUNCTION, e))
|
||||
return _cachekey_func(*args, **kwargs)
|
||||
|
||||
@@ -45,6 +45,8 @@ class CompressorConf(AppConf):
|
||||
YUGLIFY_BINARY = 'yuglify'
|
||||
YUGLIFY_CSS_ARGUMENTS = '--terminal'
|
||||
YUGLIFY_JS_ARGUMENTS = '--terminal'
|
||||
CLEAN_CSS_BINARY = 'cleancss'
|
||||
CLEAN_CSS_ARGUMENTS = ''
|
||||
DATA_URI_MAX_SIZE = 1024
|
||||
|
||||
# the cache backend to use
|
||||
|
||||
@@ -34,7 +34,7 @@ class CssCompressor(Compressor):
|
||||
self.media_nodes[-1][1].split_content.append(data)
|
||||
else:
|
||||
node = self.__class__(content=self.parser.elem_str(elem),
|
||||
context=self.context)
|
||||
context=self.context)
|
||||
node.split_content.append(data)
|
||||
self.media_nodes.append((media, node))
|
||||
return self.split_content
|
||||
|
||||
@@ -3,9 +3,27 @@ import io
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from platform import system
|
||||
|
||||
if system() != "Windows":
|
||||
try:
|
||||
from shlex import quote as shell_quote # Python 3
|
||||
except ImportError:
|
||||
from pipes import quote as shell_quote # Python 2
|
||||
else:
|
||||
from subprocess import list2cmdline
|
||||
def shell_quote(s):
|
||||
# shlex.quote/pipes.quote is not compatible with Windows
|
||||
return list2cmdline([s])
|
||||
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.files.temp import NamedTemporaryFile
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
try:
|
||||
from importlib import import_module
|
||||
except ImportError:
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
from django.utils.encoding import smart_text
|
||||
from django.utils import six
|
||||
|
||||
@@ -26,7 +44,7 @@ class FilterBase(object):
|
||||
"""
|
||||
def __init__(self, content, filter_type=None, filename=None, verbose=0,
|
||||
charset=None):
|
||||
self.type = filter_type
|
||||
self.type = filter_type or getattr(self, 'type', None)
|
||||
self.content = content
|
||||
self.verbose = verbose or settings.COMPRESS_VERBOSE
|
||||
self.logger = logger
|
||||
@@ -65,7 +83,7 @@ class CallbackOutputFilter(FilterBase):
|
||||
try:
|
||||
mod_name, func_name = get_mod_func(self.callback)
|
||||
func = getattr(import_module(mod_name), func_name)
|
||||
except ImportError:
|
||||
except (ImportError, TypeError):
|
||||
if self.dependencies:
|
||||
if len(self.dependencies) == 1:
|
||||
warning = "dependency (%s) is" % self.dependencies[0]
|
||||
@@ -147,6 +165,12 @@ class CompilerFilter(FilterBase):
|
||||
self.outfile = NamedTemporaryFile(mode='r+', suffix=ext)
|
||||
options["outfile"] = self.outfile.name
|
||||
|
||||
# Quote infile and outfile for spaces etc.
|
||||
if "infile" in options:
|
||||
options["infile"] = shell_quote(options["infile"])
|
||||
if "outfile" in options:
|
||||
options["outfile"] = shell_quote(options["outfile"])
|
||||
|
||||
try:
|
||||
command = self.command.format(**options)
|
||||
proc = subprocess.Popen(
|
||||
|
||||
10
compressor/filters/cleancss.py
Normal file
10
compressor/filters/cleancss.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from compressor.conf import settings
|
||||
from compressor.filters import CompilerFilter
|
||||
|
||||
|
||||
class CleanCSSFilter(CompilerFilter):
|
||||
command = "{binary} {args} -o {outfile} {infile}"
|
||||
options = (
|
||||
("binary", settings.COMPRESS_CLEAN_CSS_BINARY),
|
||||
("args", settings.COMPRESS_CLEAN_CSS_ARGUMENTS),
|
||||
)
|
||||
@@ -5,7 +5,6 @@ import posixpath
|
||||
from compressor.cache import get_hashed_mtime, get_hashed_content
|
||||
from compressor.conf import settings
|
||||
from compressor.filters import FilterBase, FilterError
|
||||
from compressor.utils import staticfiles
|
||||
|
||||
URL_PATTERN = re.compile(r'url\(([^\)]+)\)')
|
||||
SRC_PATTERN = re.compile(r'src=([\'"])(.+?)\1')
|
||||
@@ -22,10 +21,7 @@ class CssAbsoluteFilter(FilterBase):
|
||||
self.has_scheme = False
|
||||
|
||||
def input(self, filename=None, basename=None, **kwargs):
|
||||
if filename is not None:
|
||||
filename = os.path.normcase(os.path.abspath(filename))
|
||||
if (not (filename and filename.startswith(self.root)) and
|
||||
not self.find(basename)):
|
||||
if not filename:
|
||||
return self.content
|
||||
self.path = basename.replace(os.sep, '/')
|
||||
self.path = self.path.lstrip('/')
|
||||
@@ -40,10 +36,6 @@ class CssAbsoluteFilter(FilterBase):
|
||||
return SRC_PATTERN.sub(self.src_converter,
|
||||
URL_PATTERN.sub(self.url_converter, self.content))
|
||||
|
||||
def find(self, basename):
|
||||
if settings.DEBUG and basename and staticfiles.finders:
|
||||
return staticfiles.finders.find(basename)
|
||||
|
||||
def guess_filename(self, url):
|
||||
local_path = url
|
||||
if self.has_scheme:
|
||||
@@ -70,6 +62,8 @@ class CssAbsoluteFilter(FilterBase):
|
||||
suffix = get_hashed_mtime(filename)
|
||||
elif settings.COMPRESS_CSS_HASHING_METHOD in ("hash", "content"):
|
||||
suffix = get_hashed_content(filename)
|
||||
elif settings.COMPRESS_CSS_HASHING_METHOD is None:
|
||||
suffix = None
|
||||
else:
|
||||
raise FilterError('COMPRESS_CSS_HASHING_METHOD is configured '
|
||||
'with an unknown method (%s).' %
|
||||
|
||||
@@ -12,14 +12,42 @@ class JsCompressor(Compressor):
|
||||
def split_contents(self):
|
||||
if self.split_content:
|
||||
return self.split_content
|
||||
self.extra_nodes = []
|
||||
for elem in self.parser.js_elems():
|
||||
attribs = self.parser.elem_attribs(elem)
|
||||
if 'src' in attribs:
|
||||
basename = self.get_basename(attribs['src'])
|
||||
filename = self.get_filename(basename)
|
||||
content = (SOURCE_FILE, filename, basename, elem)
|
||||
self.split_content.append(content)
|
||||
else:
|
||||
content = self.parser.elem_content(elem)
|
||||
self.split_content.append((SOURCE_HUNK, content, None, elem))
|
||||
content = (SOURCE_HUNK, self.parser.elem_content(elem), None, elem)
|
||||
self.split_content.append(content)
|
||||
if 'async' in attribs:
|
||||
extra = ' async'
|
||||
elif 'defer' in attribs:
|
||||
extra = ' defer'
|
||||
else:
|
||||
extra = ''
|
||||
# Append to the previous node if it had the same attribute
|
||||
append_to_previous = (self.extra_nodes and
|
||||
self.extra_nodes[-1][0] == extra)
|
||||
if append_to_previous and settings.COMPRESS_ENABLED:
|
||||
self.extra_nodes[-1][1].split_content.append(content)
|
||||
else:
|
||||
node = self.__class__(content=self.parser.elem_str(elem),
|
||||
context=self.context)
|
||||
node.split_content.append(content)
|
||||
self.extra_nodes.append((extra, node))
|
||||
return self.split_content
|
||||
|
||||
def output(self, *args, **kwargs):
|
||||
if (settings.COMPRESS_ENABLED or settings.COMPRESS_PRECOMPILERS or
|
||||
kwargs.get('forced', False)):
|
||||
self.split_contents()
|
||||
if hasattr(self, 'extra_nodes'):
|
||||
ret = []
|
||||
for extra, subnode in self.extra_nodes:
|
||||
subnode.extra_context.update({'extra': extra})
|
||||
ret.append(subnode.output(*args, **kwargs))
|
||||
return '\n'.join(ret)
|
||||
return super(JsCompressor, self).output(*args, **kwargs)
|
||||
|
||||
@@ -5,6 +5,7 @@ import sys
|
||||
from fnmatch import fnmatch
|
||||
from optparse import make_option
|
||||
|
||||
import django
|
||||
from django.core.management.base import NoArgsCommand, CommandError
|
||||
import django.template
|
||||
from django.template import Context
|
||||
@@ -53,24 +54,30 @@ class Command(NoArgsCommand):
|
||||
requires_model_validation = False
|
||||
|
||||
def get_loaders(self):
|
||||
from django.template.loader import template_source_loaders
|
||||
if template_source_loaders is None:
|
||||
try:
|
||||
from django.template.loader import (
|
||||
find_template as finder_func)
|
||||
except ImportError:
|
||||
from django.template.loader import (
|
||||
find_template_source as finder_func) # noqa
|
||||
try:
|
||||
# Force django to calculate template_source_loaders from
|
||||
# TEMPLATE_LOADERS settings, by asking to find a dummy template
|
||||
source, name = finder_func('test')
|
||||
except django.template.TemplateDoesNotExist:
|
||||
pass
|
||||
# Reload template_source_loaders now that it has been calculated ;
|
||||
# it should contain the list of valid, instanciated template loaders
|
||||
# to use.
|
||||
if django.VERSION < (1, 8):
|
||||
from django.template.loader import template_source_loaders
|
||||
if template_source_loaders is None:
|
||||
try:
|
||||
from django.template.loader import (
|
||||
find_template as finder_func)
|
||||
except ImportError:
|
||||
from django.template.loader import (
|
||||
find_template_source as finder_func) # noqa
|
||||
try:
|
||||
# Force django to calculate template_source_loaders from
|
||||
# TEMPLATE_LOADERS settings, by asking to find a dummy template
|
||||
source, name = finder_func('test')
|
||||
except django.template.TemplateDoesNotExist:
|
||||
pass
|
||||
# Reload template_source_loaders now that it has been calculated ;
|
||||
# it should contain the list of valid, instanciated template loaders
|
||||
# to use.
|
||||
from django.template.loader import template_source_loaders
|
||||
else:
|
||||
from django.template import engines
|
||||
template_source_loaders = []
|
||||
for e in engines.all():
|
||||
template_source_loaders.extend(e.engine.get_template_loaders(e.engine.loaders))
|
||||
loaders = []
|
||||
# If template loader is CachedTemplateLoader, return the loaders
|
||||
# that it wraps around. So if we have
|
||||
@@ -130,7 +137,7 @@ class Command(NoArgsCommand):
|
||||
if get_template_sources is None:
|
||||
get_template_sources = loader.get_template_sources
|
||||
paths.update(list(get_template_sources('')))
|
||||
except (ImportError, AttributeError):
|
||||
except (ImportError, AttributeError, TypeError):
|
||||
# Yeah, this didn't work out so well, let's move on
|
||||
pass
|
||||
if not paths:
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from __future__ import absolute_import
|
||||
import io
|
||||
from copy import copy
|
||||
|
||||
import django
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.template import Template
|
||||
from django.template import Context
|
||||
from django.template.base import Node, VariableNode, TextNode, NodeList
|
||||
from django.template.defaulttags import IfNode
|
||||
from django.template.loader import get_template
|
||||
from django.template.loader_tags import ExtendsNode, BlockNode, BlockContext
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist
|
||||
from compressor.templatetags.compress import CompressorNode
|
||||
|
||||
|
||||
def handle_extendsnode(extendsnode, block_context=None):
|
||||
def handle_extendsnode(extendsnode, block_context=None, original=None):
|
||||
"""Create a copy of Node tree of a derived template replacing
|
||||
all blocks tags with the nodes of appropriate blocks.
|
||||
Also handles {{ block.super }} tags.
|
||||
@@ -27,6 +27,9 @@ def handle_extendsnode(extendsnode, block_context=None):
|
||||
block_context.add_blocks(blocks)
|
||||
|
||||
context = Context(settings.COMPRESS_OFFLINE_CONTEXT)
|
||||
if original is not None:
|
||||
context.template = original
|
||||
|
||||
compiled_parent = extendsnode.get_parent(context)
|
||||
parent_nodelist = compiled_parent.nodelist
|
||||
# If the parent template has an ExtendsNode it is not the root.
|
||||
@@ -34,7 +37,7 @@ def handle_extendsnode(extendsnode, block_context=None):
|
||||
# The ExtendsNode has to be the first non-text node.
|
||||
if not isinstance(node, TextNode):
|
||||
if isinstance(node, ExtendsNode):
|
||||
return handle_extendsnode(node, block_context)
|
||||
return handle_extendsnode(node, block_context, original)
|
||||
break
|
||||
# Add blocks of the root template to block context.
|
||||
blocks = dict((n.name, n) for n in
|
||||
@@ -55,6 +58,8 @@ def remove_block_nodes(nodelist, block_stack, block_context):
|
||||
if not block_stack:
|
||||
continue
|
||||
node = block_context.get_block(block_stack[-1].name)
|
||||
if not node:
|
||||
continue
|
||||
if isinstance(node, BlockNode):
|
||||
expanded_block = expand_blocknode(node, block_stack, block_context)
|
||||
new_nodelist.extend(expanded_block)
|
||||
@@ -93,13 +98,15 @@ class DjangoParser(object):
|
||||
self.charset = charset
|
||||
|
||||
def parse(self, template_name):
|
||||
with io.open(template_name, mode='rb') as file:
|
||||
try:
|
||||
return Template(file.read().decode(self.charset))
|
||||
except template.TemplateSyntaxError as e:
|
||||
raise TemplateSyntaxError(str(e))
|
||||
except template.TemplateDoesNotExist as e:
|
||||
raise TemplateDoesNotExist(str(e))
|
||||
try:
|
||||
if django.VERSION < (1, 8):
|
||||
return get_template(template_name)
|
||||
else:
|
||||
return get_template(template_name).template
|
||||
except template.TemplateSyntaxError as e:
|
||||
raise TemplateSyntaxError(str(e))
|
||||
except template.TemplateDoesNotExist as e:
|
||||
raise TemplateDoesNotExist(str(e))
|
||||
|
||||
def process_template(self, template, context):
|
||||
return True
|
||||
@@ -111,15 +118,17 @@ class DjangoParser(object):
|
||||
pass
|
||||
|
||||
def render_nodelist(self, template, context, node):
|
||||
if django.VERSION >= (1, 8):
|
||||
context.template = template
|
||||
return node.nodelist.render(context)
|
||||
|
||||
def render_node(self, template, context, node):
|
||||
return node.render(context, forced=True)
|
||||
|
||||
def get_nodelist(self, node):
|
||||
def get_nodelist(self, node, original=None):
|
||||
if isinstance(node, ExtendsNode):
|
||||
try:
|
||||
return handle_extendsnode(node)
|
||||
return handle_extendsnode(node, block_context=None, original=original)
|
||||
except template.TemplateSyntaxError as e:
|
||||
raise TemplateSyntaxError(str(e))
|
||||
except template.TemplateDoesNotExist as e:
|
||||
@@ -134,10 +143,12 @@ class DjangoParser(object):
|
||||
nodelist = getattr(node, 'nodelist', [])
|
||||
return nodelist
|
||||
|
||||
def walk_nodes(self, node):
|
||||
for node in self.get_nodelist(node):
|
||||
def walk_nodes(self, node, original=None):
|
||||
if django.VERSION >= (1, 8) and original is None:
|
||||
original = node
|
||||
for node in self.get_nodelist(node, original):
|
||||
if isinstance(node, CompressorNode) and node.is_offline_compression_enabled(forced=True):
|
||||
yield node
|
||||
else:
|
||||
for node in self.walk_nodes(node):
|
||||
for node in self.walk_nodes(node, original):
|
||||
yield node
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from django.utils import six
|
||||
from django.utils.functional import LazyObject
|
||||
from django.utils.importlib import import_module
|
||||
try:
|
||||
from importlib import import_module
|
||||
except ImportError:
|
||||
from django.utils.importlib import import_module
|
||||
|
||||
# support legacy parser module usage
|
||||
from compressor.parser.base import ParserBase # noqa
|
||||
@@ -30,5 +33,5 @@ class AutoSelectParser(LazyObject):
|
||||
import_module(dependency)
|
||||
self._wrapped = parser(content)
|
||||
break
|
||||
except ImportError:
|
||||
except (ImportError, TypeError):
|
||||
continue
|
||||
|
||||
@@ -1 +1 @@
|
||||
<script type="text/javascript" src="{{ compressed.url }}"></script>
|
||||
<script type="text/javascript" src="{{ compressed.url }}"{{ compressed.extra }}></script>
|
||||
@@ -3,7 +3,13 @@ import django
|
||||
|
||||
TEST_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'tests')
|
||||
|
||||
COMPRESS_CACHE_BACKEND = 'locmem://'
|
||||
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
|
||||
'LOCATION': 'unique-snowflake'
|
||||
}
|
||||
}
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
@@ -13,9 +19,17 @@ DATABASES = {
|
||||
}
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.staticfiles',
|
||||
'compressor',
|
||||
'coffin',
|
||||
'jingo',
|
||||
]
|
||||
if django.VERSION < (1, 8):
|
||||
INSTALLED_APPS.append('jingo')
|
||||
|
||||
STATICFILES_FINDERS = [
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
'compressor.finders.CompressorFinder',
|
||||
]
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
@@ -38,3 +52,5 @@ SECRET_KEY = "iufoj=mibkpdz*%bob952x(%49rqgv8gg45k36kjcg76&-y5=!"
|
||||
PASSWORD_HASHERS = (
|
||||
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = []
|
||||
|
||||
@@ -7,11 +7,11 @@ import sys
|
||||
def main():
|
||||
p = optparse.OptionParser()
|
||||
p.add_option('-f', '--file', action="store",
|
||||
type="string", dest="filename",
|
||||
help="File to read from, defaults to stdin", default=None)
|
||||
type="string", dest="filename",
|
||||
help="File to read from, defaults to stdin", default=None)
|
||||
p.add_option('-o', '--output', action="store",
|
||||
type="string", dest="outfile",
|
||||
help="File to write to, defaults to stdout", default=None)
|
||||
type="string", dest="outfile",
|
||||
help="File to write to, defaults to stdout", default=None)
|
||||
|
||||
options, arguments = p.parse_args()
|
||||
|
||||
|
||||
1
compressor/tests/static/css/filename with spaces.css
Normal file
1
compressor/tests/static/css/filename with spaces.css
Normal file
@@ -0,0 +1 @@
|
||||
body { background:#424242; }
|
||||
1
compressor/tests/static/css/utf-8_with-BOM.css
Normal file
1
compressor/tests/static/css/utf-8_with-BOM.css
Normal file
@@ -0,0 +1 @@
|
||||
.compress-test {color: red;}
|
||||
1
compressor/tests/static/js/three.js
Normal file
1
compressor/tests/static/js/three.js
Normal file
@@ -0,0 +1 @@
|
||||
hermanos = {}
|
||||
1
compressor/tests/static/js/two.js
Normal file
1
compressor/tests/static/js/two.js
Normal file
@@ -0,0 +1 @@
|
||||
pollos = {}
|
||||
@@ -12,11 +12,13 @@ from django.core.cache.backends import locmem
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from compressor.base import SOURCE_HUNK, SOURCE_FILE
|
||||
from compressor import cache as cachemod
|
||||
from compressor.base import SOURCE_FILE, SOURCE_HUNK
|
||||
from compressor.cache import get_cachekey
|
||||
from compressor.conf import settings
|
||||
from compressor.css import CssCompressor
|
||||
from compressor.exceptions import FilterDoesNotExist, FilterError
|
||||
from compressor.js import JsCompressor
|
||||
from compressor.exceptions import FilterDoesNotExist
|
||||
|
||||
|
||||
def make_soup(markup):
|
||||
@@ -112,6 +114,14 @@ class CompressorTestCase(SimpleTestCase):
|
||||
hunks = '\n'.join([h for h in self.css_node.hunks()])
|
||||
self.assertEqual(out, hunks)
|
||||
|
||||
def test_css_output_with_bom_input(self):
|
||||
out = 'body { background:#990; }\n.compress-test {color: red;}'
|
||||
css = ("""<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)
|
||||
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:
|
||||
@@ -208,6 +218,14 @@ class CompressorTestCase(SimpleTestCase):
|
||||
css_node = CssCompressor(css)
|
||||
self.assertRaises(FilterDoesNotExist, css_node.output, 'inline')
|
||||
|
||||
@override_settings(COMPRESS_PRECOMPILERS=(
|
||||
('text/foobar', './foo -I ./bar/baz'),
|
||||
), COMPRESS_ENABLED=True)
|
||||
def test_command_with_dot_precompiler(self):
|
||||
css = '<style type="text/foobar">p { border:10px solid red;}</style>'
|
||||
css_node = CssCompressor(css)
|
||||
self.assertRaises(FilterError, css_node.output, 'inline')
|
||||
|
||||
|
||||
class CssMediaTestCase(SimpleTestCase):
|
||||
def setUp(self):
|
||||
@@ -267,4 +285,49 @@ class CacheBackendTestCase(CompressorTestCase):
|
||||
|
||||
def test_correct_backend(self):
|
||||
from compressor.cache import cache
|
||||
self.assertEqual(cache.__class__, locmem.CacheClass)
|
||||
self.assertEqual(cache.__class__, locmem.LocMemCache)
|
||||
|
||||
|
||||
class JsAsyncDeferTestCase(SimpleTestCase):
|
||||
def setUp(self):
|
||||
self.js = """\
|
||||
<script src="/static/js/one.js" type="text/javascript"></script>
|
||||
<script src="/static/js/two.js" type="text/javascript" async></script>
|
||||
<script src="/static/js/three.js" type="text/javascript" defer></script>
|
||||
<script type="text/javascript">obj.value = "value";</script>
|
||||
<script src="/static/js/one.js" type="text/javascript" async></script>
|
||||
<script src="/static/js/two.js" type="text/javascript" async></script>
|
||||
<script src="/static/js/three.js" type="text/javascript"></script>"""
|
||||
|
||||
def test_js_output(self):
|
||||
def extract_attr(tag):
|
||||
if tag.has_attr('async'):
|
||||
return 'async'
|
||||
if tag.has_attr('defer'):
|
||||
return 'defer'
|
||||
js_node = JsCompressor(self.js)
|
||||
output = [None, 'async', 'defer', None, 'async', None]
|
||||
if six.PY3:
|
||||
scripts = make_soup(js_node.output()).find_all('script')
|
||||
attrs = [extract_attr(i) for i in scripts]
|
||||
else:
|
||||
scripts = make_soup(js_node.output()).findAll('script')
|
||||
attrs = [s.get('async') or s.get('defer') for s in scripts]
|
||||
self.assertEqual(output, attrs)
|
||||
|
||||
|
||||
class CacheTestCase(SimpleTestCase):
|
||||
|
||||
def setUp(self):
|
||||
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"))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import with_statement, unicode_literals
|
||||
from collections import defaultdict
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
@@ -17,9 +18,18 @@ from compressor.filters.base import CompilerFilter
|
||||
from compressor.filters.cssmin import CSSMinFilter
|
||||
from compressor.filters.css_default import CssAbsoluteFilter
|
||||
from compressor.filters.template import TemplateFilter
|
||||
from compressor.filters.closure import ClosureCompilerFilter
|
||||
from compressor.filters.csstidy import CSSTidyFilter
|
||||
from compressor.filters.yuglify import YUglifyCSSFilter, YUglifyJSFilter
|
||||
from compressor.filters.yui import YUICSSFilter, YUIJSFilter
|
||||
from compressor.filters.cleancss import CleanCSSFilter
|
||||
from compressor.tests.test_base import test_dir
|
||||
|
||||
|
||||
def blankdict(*args, **kwargs):
|
||||
return defaultdict(lambda: '', *args, **kwargs)
|
||||
|
||||
|
||||
@unittest.skipIf(find_command(settings.COMPRESS_CSSTIDY_BINARY) is None,
|
||||
'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY)
|
||||
class CssTidyTestCase(TestCase):
|
||||
@@ -30,7 +40,6 @@ class CssTidyTestCase(TestCase):
|
||||
color: black;
|
||||
}
|
||||
""")
|
||||
from compressor.filters.csstidy import CSSTidyFilter
|
||||
ret = CSSTidyFilter(content).input()
|
||||
self.assertIsInstance(ret, six.text_type)
|
||||
self.assertEqual(
|
||||
@@ -39,10 +48,13 @@ class CssTidyTestCase(TestCase):
|
||||
|
||||
class PrecompilerTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.filename = os.path.join(test_dir, 'static/css/one.css')
|
||||
self.test_precompiler = os.path.join(test_dir, 'precompiler.py')
|
||||
self.setup_infile()
|
||||
|
||||
def setup_infile(self, filename='static/css/one.css'):
|
||||
self.filename = os.path.join(test_dir, filename)
|
||||
with io.open(self.filename, encoding=settings.FILE_CHARSET) as file:
|
||||
self.content = file.read()
|
||||
self.test_precompiler = os.path.join(test_dir, 'precompiler.py')
|
||||
|
||||
def test_precompiler_infile_outfile(self):
|
||||
command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
|
||||
@@ -51,6 +63,14 @@ class PrecompilerTestCase(TestCase):
|
||||
charset=settings.FILE_CHARSET, command=command)
|
||||
self.assertEqual("body { color:#990; }", compiler.input())
|
||||
|
||||
def test_precompiler_infile_with_spaces(self):
|
||||
self.setup_infile('static/css/filename with spaces.css')
|
||||
command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(
|
||||
content=self.content, filename=self.filename,
|
||||
charset=settings.FILE_CHARSET, command=command)
|
||||
self.assertEqual("body { color:#424242; }", compiler.input())
|
||||
|
||||
def test_precompiler_infile_stdout(self):
|
||||
command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler)
|
||||
compiler = CompilerFilter(
|
||||
@@ -99,8 +119,8 @@ class CssMinTestCase(TestCase):
|
||||
class CssAbsolutizingTestCase(TestCase):
|
||||
hashing_method = 'mtime'
|
||||
hashing_func = staticmethod(get_hashed_mtime)
|
||||
content = ("p { background: url('../../img/python.png') }"
|
||||
"p { filter: Alpha(src='../../img/python.png') }")
|
||||
template = ("p { background: url('%(url)simg/python.png%(query)s%(hash)s%(frag)s') }"
|
||||
"p { filter: Alpha(src='%(url)simg/python.png%(query)s%(hash)s%(frag)s') }")
|
||||
|
||||
def setUp(self):
|
||||
self.old_enabled = settings.COMPRESS_ENABLED
|
||||
@@ -120,40 +140,55 @@ class CssAbsolutizingTestCase(TestCase):
|
||||
settings.COMPRESS_URL = self.old_url
|
||||
settings.COMPRESS_CSS_HASHING_METHOD = self.old_hashing_method
|
||||
|
||||
def test_css_no_hash(self):
|
||||
settings.COMPRESS_CSS_HASHING_METHOD = None
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
content = self.template % blankdict(url='../../')
|
||||
params = blankdict({
|
||||
'url': settings.COMPRESS_URL,
|
||||
})
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
settings.COMPRESS_URL = params['url'] = 'http://static.example.com/'
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter(self):
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
|
||||
params = {
|
||||
content = self.template % blankdict(url='../../')
|
||||
params = blankdict({
|
||||
'url': settings.COMPRESS_URL,
|
||||
'hash': self.hashing_func(imagefilename),
|
||||
}
|
||||
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
|
||||
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
|
||||
filter = CssAbsoluteFilter(self.content)
|
||||
'hash': '?' + self.hashing_func(imagefilename),
|
||||
})
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
settings.COMPRESS_URL = params['url'] = 'http://static.example.com/'
|
||||
filter = CssAbsoluteFilter(self.content)
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
|
||||
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_url_fragment(self):
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
|
||||
params = {
|
||||
content = self.template % blankdict(url='../../', frag='#foo')
|
||||
params = blankdict({
|
||||
'url': settings.COMPRESS_URL,
|
||||
'hash': self.hashing_func(imagefilename),
|
||||
}
|
||||
content = "p { background: url('../../img/python.png#foo') }"
|
||||
|
||||
output = "p { background: url('%(url)simg/python.png?%(hash)s#foo') }" % params
|
||||
'hash': '?' + self.hashing_func(imagefilename),
|
||||
'frag': '#foo',
|
||||
})
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
settings.COMPRESS_URL = params['url'] = 'http://media.example.com/'
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
output = "p { background: url('%(url)simg/python.png?%(hash)s#foo') }" % params
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_only_url_fragment(self):
|
||||
@@ -161,64 +196,78 @@ class CssAbsolutizingTestCase(TestCase):
|
||||
content = "p { background: url('#foo') }"
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(content, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
settings.COMPRESS_URL = 'http://media.example.com/'
|
||||
filter = CssAbsoluteFilter(content)
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
self.assertEqual(content, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_querystring(self):
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
|
||||
params = {
|
||||
content = self.template % blankdict(url='../../', query='?foo')
|
||||
params = blankdict({
|
||||
'url': settings.COMPRESS_URL,
|
||||
'hash': self.hashing_func(imagefilename),
|
||||
}
|
||||
content = "p { background: url('../../img/python.png?foo') }"
|
||||
|
||||
output = "p { background: url('%(url)simg/python.png?foo&%(hash)s') }" % params
|
||||
'query': '?foo',
|
||||
'hash': '&' + self.hashing_func(imagefilename),
|
||||
})
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
settings.COMPRESS_URL = params['url'] = 'http://media.example.com/'
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
output = "p { background: url('%(url)simg/python.png?foo&%(hash)s') }" % params
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_https(self):
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
|
||||
params = {
|
||||
content = self.template % blankdict(url='../../')
|
||||
params = blankdict({
|
||||
'url': settings.COMPRESS_URL,
|
||||
'hash': self.hashing_func(imagefilename),
|
||||
}
|
||||
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
|
||||
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
|
||||
filter = CssAbsoluteFilter(self.content)
|
||||
'hash': '?' + self.hashing_func(imagefilename),
|
||||
})
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
settings.COMPRESS_URL = params['url'] = 'https://static.example.com/'
|
||||
filter = CssAbsoluteFilter(self.content)
|
||||
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
|
||||
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
|
||||
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_relative_path(self):
|
||||
filename = os.path.join(settings.TEST_DIR, 'whatever', '..', 'static', 'whatever/../css/url/test.css')
|
||||
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
|
||||
params = {
|
||||
content = self.template % blankdict(url='../../')
|
||||
params = blankdict({
|
||||
'url': settings.COMPRESS_URL,
|
||||
'hash': self.hashing_func(imagefilename),
|
||||
}
|
||||
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
|
||||
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
|
||||
filter = CssAbsoluteFilter(self.content)
|
||||
'hash': '?' + self.hashing_func(imagefilename),
|
||||
})
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
settings.COMPRESS_URL = params['url'] = 'https://static.example.com/'
|
||||
filter = CssAbsoluteFilter(self.content)
|
||||
output = ("p { background: url('%(url)simg/python.png?%(hash)s') }"
|
||||
"p { filter: Alpha(src='%(url)simg/python.png?%(hash)s') }") % params
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
|
||||
|
||||
def test_css_absolute_filter_filename_outside_compress_root(self):
|
||||
filename = '/foo/bar/baz/test.css'
|
||||
content = self.template % blankdict(url='../qux/')
|
||||
params = blankdict({
|
||||
'url': settings.COMPRESS_URL + 'bar/qux/',
|
||||
})
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='bar/baz/test.css'))
|
||||
settings.COMPRESS_URL = 'https://static.example.com/'
|
||||
params['url'] = settings.COMPRESS_URL + 'bar/qux/'
|
||||
output = self.template % params
|
||||
filter = CssAbsoluteFilter(content)
|
||||
self.assertEqual(output, filter.input(filename=filename, basename='bar/baz/test.css'))
|
||||
|
||||
def test_css_hunks(self):
|
||||
hash_dict = {
|
||||
'hash1': self.hashing_func(os.path.join(settings.COMPRESS_ROOT, 'img/python.png')),
|
||||
@@ -253,14 +302,6 @@ class CssAbsolutizingTestCaseWithHash(CssAbsolutizingTestCase):
|
||||
hashing_method = 'content'
|
||||
hashing_func = staticmethod(get_hashed_content)
|
||||
|
||||
def setUp(self):
|
||||
super(CssAbsolutizingTestCaseWithHash, self).setUp()
|
||||
self.css = """
|
||||
<link rel="stylesheet" href="/static/css/url/url1.css" type="text/css" charset="utf-8">
|
||||
<link rel="stylesheet" href="/static/css/url/2/url2.css" type="text/css" charset="utf-8">
|
||||
"""
|
||||
self.css_node = CssCompressor(self.css)
|
||||
|
||||
|
||||
class CssDataUriTestCase(TestCase):
|
||||
def setUp(self):
|
||||
@@ -301,3 +342,38 @@ class TemplateTestCase(TestCase):
|
||||
#footer {font-weight: bold;}
|
||||
"""
|
||||
self.assertEqual(input, TemplateFilter(content).input())
|
||||
|
||||
|
||||
class SpecializedFiltersTest(TestCase):
|
||||
"""
|
||||
Test to check the Specializations of filters.
|
||||
"""
|
||||
def test_closure_filter(self):
|
||||
filter = ClosureCompilerFilter('')
|
||||
self.assertEqual(filter.options, (('binary', six.text_type('java -jar compiler.jar')), ('args', six.text_type(''))))
|
||||
|
||||
def test_csstidy_filter(self):
|
||||
filter = CSSTidyFilter('')
|
||||
self.assertEqual(filter.options, (('binary', six.text_type('csstidy')), ('args', six.text_type('--template=highest'))))
|
||||
|
||||
def test_yuglify_filters(self):
|
||||
filter = YUglifyCSSFilter('')
|
||||
self.assertEqual(filter.command, '{binary} {args} --type=css')
|
||||
self.assertEqual(filter.options, (('binary', six.text_type('yuglify')), ('args', six.text_type('--terminal'))))
|
||||
|
||||
filter = YUglifyJSFilter('')
|
||||
self.assertEqual(filter.command, '{binary} {args} --type=js')
|
||||
self.assertEqual(filter.options, (('binary', six.text_type('yuglify')), ('args', six.text_type('--terminal'))))
|
||||
|
||||
def test_yui_filters(self):
|
||||
filter = YUICSSFilter('')
|
||||
self.assertEqual(filter.command, '{binary} {args} --type=css')
|
||||
self.assertEqual(filter.options, (('binary', six.text_type('java -jar yuicompressor.jar')), ('args', six.text_type(''))))
|
||||
|
||||
filter = YUIJSFilter('', verbose=1)
|
||||
self.assertEqual(filter.command, '{binary} {args} --type=js --verbose')
|
||||
self.assertEqual(filter.options, (('binary', six.text_type('java -jar yuicompressor.jar')), ('args', six.text_type('')), ('verbose', 1)))
|
||||
|
||||
def test_clean_css_filter(self):
|
||||
filter = CleanCSSFilter('')
|
||||
self.assertEqual(filter.options, (('binary', six.text_type('cleancss')), ('args', six.text_type(''))))
|
||||
|
||||
@@ -65,8 +65,7 @@ class TestJinja2CompressorExtension(TestCase):
|
||||
self.assertEqual(tag_body, template.render())
|
||||
|
||||
def test_empty_tag(self):
|
||||
template = self.env.from_string("""{% compress js %}{% block js %}
|
||||
{% endblock %}{% endcompress %}""")
|
||||
template = self.env.from_string("""{% compress js %}{% block js %}{% endblock %}{% endcompress %}""")
|
||||
context = {'STATIC_URL': settings.COMPRESS_URL}
|
||||
self.assertEqual('', template.render(context))
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import io
|
||||
import os
|
||||
import sys
|
||||
|
||||
import django
|
||||
from django.core.management.base import CommandError
|
||||
from django.template import Template, Context
|
||||
from django.test import TestCase
|
||||
@@ -44,10 +45,6 @@ class OfflineTestCaseMixin(object):
|
||||
engines = ("django",)
|
||||
|
||||
def setUp(self):
|
||||
self._old_compress = settings.COMPRESS_ENABLED
|
||||
self._old_compress_offline = settings.COMPRESS_OFFLINE
|
||||
self._old_template_dirs = settings.TEMPLATE_DIRS
|
||||
self._old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
|
||||
self.log = StringIO()
|
||||
|
||||
# Reset template dirs, because it enables us to force compress to
|
||||
@@ -58,11 +55,18 @@ class OfflineTestCaseMixin(object):
|
||||
# template to be skipped over.
|
||||
django_template_dir = os.path.join(settings.TEST_DIR, 'test_templates', self.templates_dir)
|
||||
jinja2_template_dir = os.path.join(settings.TEST_DIR, 'test_templates_jinja2', self.templates_dir)
|
||||
settings.TEMPLATE_DIRS = (django_template_dir, jinja2_template_dir)
|
||||
|
||||
# Enable offline compress
|
||||
settings.COMPRESS_ENABLED = True
|
||||
settings.COMPRESS_OFFLINE = True
|
||||
override_settings = {
|
||||
'TEMPLATE_DIRS': (django_template_dir, jinja2_template_dir,),
|
||||
'COMPRESS_ENABLED': True,
|
||||
'COMPRESS_OFFLINE': True
|
||||
}
|
||||
|
||||
if "jinja2" in self.engines:
|
||||
override_settings["COMPRESS_JINJA2_GET_ENVIRONMENT"] = lambda: self._get_jinja2_env()
|
||||
|
||||
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)
|
||||
@@ -70,22 +74,16 @@ class OfflineTestCaseMixin(object):
|
||||
with io.open(self.template_path, encoding=settings.FILE_CHARSET) as file:
|
||||
self.template = Template(file.read())
|
||||
|
||||
self._old_jinja2_get_environment = settings.COMPRESS_JINJA2_GET_ENVIRONMENT
|
||||
|
||||
if "jinja2" in self.engines:
|
||||
# Setup Jinja2 settings.
|
||||
settings.COMPRESS_JINJA2_GET_ENVIRONMENT = lambda: self._get_jinja2_env()
|
||||
jinja2_env = settings.COMPRESS_JINJA2_GET_ENVIRONMENT()
|
||||
jinja2_env = override_settings["COMPRESS_JINJA2_GET_ENVIRONMENT"]()
|
||||
self.template_path_jinja2 = os.path.join(jinja2_template_dir, self.template_name)
|
||||
|
||||
with io.open(self.template_path_jinja2, encoding=settings.FILE_CHARSET) as file:
|
||||
self.template_jinja2 = jinja2_env.from_string(file.read())
|
||||
|
||||
def tearDown(self):
|
||||
settings.COMPRESS_JINJA2_GET_ENVIRONMENT = self._old_jinja2_get_environment
|
||||
settings.COMPRESS_ENABLED = self._old_compress
|
||||
settings.COMPRESS_OFFLINE = self._old_compress_offline
|
||||
settings.TEMPLATE_DIRS = self._old_template_dirs
|
||||
self.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)
|
||||
@@ -454,6 +452,8 @@ class OfflineGenerationComplexTestCase(OfflineTestCaseMixin, TestCase):
|
||||
# It seems there is no evidence nor indicated support for Python 3+.
|
||||
@unittest.skipIf(sys.version_info >= (3, 2),
|
||||
"Coffin does not support 3.2+")
|
||||
@unittest.skipIf(django.VERSION >= (1, 8),
|
||||
"Import error on 1.8")
|
||||
class OfflineGenerationCoffinTestCase(OfflineTestCaseMixin, TestCase):
|
||||
templates_dir = "test_coffin"
|
||||
expected_hash = "32c8281e3346"
|
||||
@@ -478,6 +478,8 @@ class OfflineGenerationCoffinTestCase(OfflineTestCaseMixin, TestCase):
|
||||
# is also evident in its tox.ini file.
|
||||
@unittest.skipIf(sys.version_info >= (3, 2) and sys.version_info < (3, 3),
|
||||
"Jingo does not support 3.2")
|
||||
@unittest.skipIf(django.VERSION >= (1, 8),
|
||||
"Import error on 1.8")
|
||||
class OfflineGenerationJingoTestCase(OfflineTestCaseMixin, TestCase):
|
||||
templates_dir = "test_jingo"
|
||||
expected_hash = "61ec584468eb"
|
||||
|
||||
@@ -5,4 +5,9 @@
|
||||
<script type="text/javascript">
|
||||
alert("this alert shouldn't be alone!");
|
||||
</script>
|
||||
{% block orphan %}
|
||||
{{ block.super }}
|
||||
An 'orphan' block that refers to a non-existent super block.
|
||||
Contents of this block are ignored.
|
||||
{% endblock %}
|
||||
{% endspaceless %}{% endblock %}
|
||||
|
||||
@@ -4,20 +4,10 @@ from django.core.exceptions import ImproperlyConfigured
|
||||
|
||||
from compressor.conf import settings
|
||||
|
||||
INSTALLED = ("staticfiles" in settings.INSTALLED_APPS or
|
||||
"django.contrib.staticfiles" in settings.INSTALLED_APPS)
|
||||
if "django.contrib.staticfiles" in settings.INSTALLED_APPS:
|
||||
from django.contrib.staticfiles import finders # noqa
|
||||
|
||||
if INSTALLED:
|
||||
if "django.contrib.staticfiles" in settings.INSTALLED_APPS:
|
||||
from django.contrib.staticfiles import finders
|
||||
else:
|
||||
try:
|
||||
from staticfiles import finders # noqa
|
||||
except ImportError:
|
||||
# Old (pre 1.0) and incompatible version of staticfiles
|
||||
INSTALLED = False
|
||||
|
||||
if (INSTALLED and "compressor.finders.CompressorFinder"
|
||||
if ("compressor.finders.CompressorFinder"
|
||||
not in settings.STATICFILES_FINDERS):
|
||||
raise ImproperlyConfigured(
|
||||
"When using Django Compressor together with staticfiles, "
|
||||
|
||||
@@ -1,12 +1,46 @@
|
||||
Changelog
|
||||
=========
|
||||
|
||||
v1.4
|
||||
----
|
||||
v1.5 (03/27/2015)
|
||||
-----------------
|
||||
|
||||
`Full Changelog <https://github.com/django-compressor/django-compressor/compare/1.4...HEAD>`_
|
||||
|
||||
- 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 (06/20/2014)
|
||||
-----------------
|
||||
|
||||
- Added Python 3 compatibility.
|
||||
|
||||
- Added compatibility with Django 1.6.x.
|
||||
- Added compatibility with Django 1.6.x and dropped support for Django 1.3.X.
|
||||
|
||||
- Fixed compatibility with html5lib 1.0.
|
||||
|
||||
@@ -46,7 +80,7 @@ v1.3 (03/18/2013)
|
||||
- Dropped support for Python 2.5. Removed ``any`` and ``walk`` compatibility
|
||||
functions in ``compressor.utils``.
|
||||
|
||||
- Removed compatibility with Django 1.2 for default values of some settings:
|
||||
- Removed compatibility with some old django setttings:
|
||||
|
||||
- :attr:`~COMPRESS_ROOT` no longer uses ``MEDIA_ROOT`` if ``STATIC_ROOT`` is
|
||||
not defined. It expects ``STATIC_ROOT`` to be defined instead.
|
||||
|
||||
@@ -9,11 +9,12 @@ tidy, everybody has to follow a few rules (nothing major, I promise :) )
|
||||
Community
|
||||
---------
|
||||
|
||||
People interested in developing for the Django Compressor should head
|
||||
over to #django-compressor on the `freenode`_ IRC network for help and to
|
||||
discuss the development.
|
||||
People interested in developing for the Django Compressor should:
|
||||
|
||||
1. Head over to #django-compressor on the `freenode`_ IRC network for help and to
|
||||
discuss the development.
|
||||
2. Open an issue on GitHub explaining your ideas.
|
||||
|
||||
You may also be interested in following `@jezdez`_ on Twitter.
|
||||
|
||||
In a nutshell
|
||||
-------------
|
||||
@@ -143,7 +144,7 @@ Documentation should be:
|
||||
- Accessible. You should assume the reader to be moderately familiar with
|
||||
Python and Django, but not anything else. Link to documentation of libraries
|
||||
you use, for example, even if they are "obvious" to you. A brief
|
||||
description of what it does is also welcome.
|
||||
description of what it does is also welcome.
|
||||
|
||||
Pulling of documentation is pretty fast and painless. Usually somebody goes
|
||||
over your text and merges it, since there are no "breaks" and that github
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
django-sekizai Support
|
||||
======================
|
||||
|
||||
Django Compressor comes with support for _django-sekizai via an extension.
|
||||
_django-sekizai provides the ability to include template code, from within
|
||||
Django Compressor comes with support for django-sekizai_ via an extension.
|
||||
django-sekizai provides the ability to include template code, from within
|
||||
any block, to a parent block. It is primarily used to include js/css from
|
||||
included templates to the master template.
|
||||
|
||||
It requires _django-sekizai to installed. Refer to the _django-sekizai _docs
|
||||
It requires django-sekizai to be installed. Refer to the `django-sekizai docs`_
|
||||
for how to use ``render_block``
|
||||
|
||||
Usage
|
||||
@@ -21,4 +21,4 @@ Usage
|
||||
|
||||
|
||||
.. _django-sekizai: https://github.com/ojii/django-sekizai
|
||||
.. _docs: http://django-sekizai.readthedocs.org/en/latest/
|
||||
.. _django-sekizai docs: http://django-sekizai.readthedocs.org/en/latest/
|
||||
|
||||
@@ -42,13 +42,13 @@ Jinja2 Offline Compression Support
|
||||
==================================
|
||||
You'd need to configure ``COMPRESS_JINJA2_GET_ENVIRONMENT`` so that
|
||||
Compressor can retrieve the Jinja2 environment for rendering.
|
||||
This can be a lamda or function that returns a Jinja2 environment.
|
||||
This can be a lambda or function that returns a Jinja2 environment.
|
||||
|
||||
Usage
|
||||
-----
|
||||
Run the following compress command along with an ``-engine`` parameter. The
|
||||
Run the following compress command along with an ``--engine`` parameter. The
|
||||
parameter can be either jinja2 or django (default). For example,
|
||||
"./manage.py compress -engine jinja2".
|
||||
``./manage.py compress --engine jinja2``.
|
||||
|
||||
Using both Django and Jinja2 templates
|
||||
--------------------------------------
|
||||
@@ -60,9 +60,9 @@ template safely. (Vice versa for Django parser).
|
||||
|
||||
A typical usage could be :
|
||||
|
||||
- "./manage.py compress" for processing Django templates first, skipping
|
||||
- ``./manage.py compress`` for processing Django templates first, skipping
|
||||
Jinja2 templates.
|
||||
- "./manage.py compress -engine jinja2" for processing Jinja2 templates,
|
||||
- ``./manage.py compress --engine jinja2`` for processing Jinja2 templates,
|
||||
skipping Django templates.
|
||||
|
||||
However, it is still recommended that you do not mix Django and Jinja2
|
||||
@@ -172,4 +172,3 @@ Jinja2 alone (with custom loader) are tested and work on Python 2.6, 2.7 and
|
||||
.. _Jinja2: http://jinja.pocoo.org/docs/
|
||||
.. _Coffin: http://pypi.python.org/pypi/Coffin
|
||||
.. _Jingo: https://jingo.readthedocs.org/en/latest/
|
||||
|
||||
|
||||
@@ -18,10 +18,8 @@ Installation
|
||||
* See the list of :ref:`settings` to modify Django Compressor's
|
||||
default behaviour and make adjustments for your website.
|
||||
|
||||
* In case you use Django's staticfiles_ contrib app (or its standalone
|
||||
counterpart django-staticfiles_) you have to add Django Compressor's file
|
||||
finder to the ``STATICFILES_FINDERS`` setting, for example with
|
||||
``django.contrib.staticfiles``:
|
||||
* In case you use Django's staticfiles_ contrib app you have to add Django
|
||||
Compressor's file finder to the ``STATICFILES_FINDERS`` setting, like this:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@@ -95,6 +93,6 @@ Optional
|
||||
.. _lxml: http://codespeak.net/lxml/
|
||||
.. _libxml2: http://xmlsoft.org/
|
||||
.. _html5lib: http://code.google.com/p/html5lib/
|
||||
.. _`Slim It`: http://slimit.org/
|
||||
.. _`Slim It`: https://github.com/rspivak/slimit
|
||||
.. _django-appconf: http://pypi.python.org/pypi/django-appconf/
|
||||
.. _versiontools: http://pypi.python.org/pypi/versiontools/
|
||||
|
||||
@@ -39,12 +39,11 @@ The storage backend to save the compressed files needs to be changed, too::
|
||||
Using staticfiles
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you are using Django's staticfiles_ contrib app or the standalone
|
||||
app django-staticfiles_, you'll need to use a temporary filesystem cache
|
||||
for Django Compressor to know which files to compress. Since staticfiles
|
||||
provides a management command to collect static files from various
|
||||
locations which uses a storage backend, this is where both apps can be
|
||||
integrated.
|
||||
If you are using Django's staticfiles_ contrib app, you'll need to use a
|
||||
temporary filesystem cache for Django Compressor to know which files to
|
||||
compress. Since staticfiles provides a management command to collect static
|
||||
files from various locations which uses a storage backend, this is where both
|
||||
apps can be integrated.
|
||||
|
||||
#. Make sure the :attr:`~django.conf.settings.COMPRESS_ROOT` and STATIC_ROOT_
|
||||
settings are equal since both apps need to look at the same directories
|
||||
@@ -84,7 +83,6 @@ integrated.
|
||||
.. _Amazon S3: https://s3.amazonaws.com/
|
||||
.. _boto: http://boto.cloudhackers.com/
|
||||
.. _django-storages: http://code.welldev.org/django-storages/
|
||||
.. _django-staticfiles: http://github.com/jezdez/django-staticfiles/
|
||||
.. _staticfiles: http://docs.djangoproject.com/en/dev/howto/static-files/
|
||||
.. _STATIC_ROOT: http://docs.djangoproject.com/en/dev/ref/settings/#static-root
|
||||
.. _STATIC_URL: http://docs.djangoproject.com/en/dev/ref/settings/#static-url
|
||||
|
||||
@@ -81,10 +81,11 @@ Backend settings
|
||||
|
||||
.. attribute:: COMPRESS_CSS_HASHING_METHOD
|
||||
|
||||
The method to use when calculating the hash to append to
|
||||
processed URLs. Either ``'mtime'`` (default) or ``'content'``.
|
||||
Use the latter in case you're using multiple server to serve your
|
||||
static files.
|
||||
The method to use when calculating the suffix to append to URLs in
|
||||
your processed CSS files. Either ``None``, ``'mtime'`` (default) or
|
||||
``'content'``. Use the ``None`` if you want to completely disable that
|
||||
feature, and the ``'content'`` in case you're using multiple servers
|
||||
to serve your content.
|
||||
|
||||
- ``compressor.filters.csstidy.CSSTidyFilter``
|
||||
|
||||
@@ -136,9 +137,24 @@ Backend settings
|
||||
A filter that uses Zachary Voase's Python port of the YUI CSS compression
|
||||
algorithm cssmin_.
|
||||
|
||||
- ``compressor.filters.cleancss.CleanCSSFilter``
|
||||
|
||||
A filter that passes the CSS content to the `clean-css`_ tool.
|
||||
|
||||
.. attribute:: CLEAN_CSS_BINARY
|
||||
|
||||
The clean-css binary filesystem path.
|
||||
|
||||
.. attribute:: CLEAN_CSS_ARGUMENTS
|
||||
|
||||
The arguments passed to clean-css.
|
||||
|
||||
|
||||
.. _CSSTidy: http://csstidy.sourceforge.net/
|
||||
.. _`data: URIs`: http://en.wikipedia.org/wiki/Data_URI_scheme
|
||||
.. _cssmin: http://pypi.python.org/pypi/cssmin/
|
||||
.. _`clean-css`: https://github.com/GoalSmashers/clean-css/
|
||||
|
||||
|
||||
- ``compressor.filters.template.TemplateFilter``
|
||||
|
||||
@@ -220,7 +236,7 @@ Backend settings
|
||||
.. _`Google Closure compiler`: http://code.google.com/closure/compiler/
|
||||
.. _`YUI compressor`: http://developer.yahoo.com/yui/compressor/
|
||||
.. _`yUglify compressor`: https://github.com/yui/yuglify
|
||||
.. _`Slim It`: http://slimit.org/
|
||||
.. _`Slim It`: https://github.com/rspivak/slimit
|
||||
|
||||
.. attribute:: COMPRESS_PRECOMPILERS
|
||||
|
||||
@@ -305,7 +321,7 @@ Backend settings
|
||||
<link rel="stylesheet" href="/static/CACHE/css/8ccf8d877f18.css" type="text/css" charset="utf-8">
|
||||
|
||||
.. _less: http://lesscss.org/
|
||||
.. _CoffeeScript: http://jashkenas.github.com/coffee-script/
|
||||
.. _CoffeeScript: http://coffeescript.org/
|
||||
|
||||
.. attribute:: COMPRESS_STORAGE
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ Which would be rendered something like:
|
||||
|
||||
.. note::
|
||||
|
||||
Remember that django-compressor will try to :ref:`group ouputs by media <css_notes>`.
|
||||
Remember that django-compressor will try to :ref:`group outputs by media <css_notes>`.
|
||||
|
||||
Linked files **must** be accessible via
|
||||
:attr:`~django.conf.settings.COMPRESS_URL`.
|
||||
|
||||
97
tox.ini
97
tox.ini
@@ -33,89 +33,34 @@ three_two =
|
||||
|
||||
[tox]
|
||||
envlist =
|
||||
py33-1.6.X,
|
||||
py32-1.6.X,
|
||||
py27-1.6.X,
|
||||
py26-1.6.X,
|
||||
py33-1.5.X,
|
||||
py32-1.5.X,
|
||||
py27-1.5.X,
|
||||
py26-1.5.X,
|
||||
py27-1.4.X,
|
||||
py26-1.4.X
|
||||
|
||||
{py26,py27}-{1.4.X,1.5.X},
|
||||
{py26,py27,py32,py33}-{1.6.X},
|
||||
{py27,py32,py33,py34}-{1.7.X},
|
||||
{py27,py32,py33,py34}-{1.8.X}
|
||||
[testenv]
|
||||
basepython =
|
||||
py26: python2.6
|
||||
py27: python2.7
|
||||
py32: python3.2
|
||||
py33: python3.3
|
||||
py34: python3.4
|
||||
usedevelop = true
|
||||
setenv =
|
||||
CPPFLAGS=-O0
|
||||
usedevelop = true
|
||||
whitelist_externals = /usr/bin/make
|
||||
downloadcache = {toxworkdir}/_download/
|
||||
commands =
|
||||
django-admin.py --version
|
||||
make test
|
||||
|
||||
[testenv:py33-1.6.X]
|
||||
basepython = python3.3
|
||||
deps =
|
||||
Django>=1.6,<1.7
|
||||
{[deps]three}
|
||||
|
||||
[testenv:py32-1.6.X]
|
||||
basepython = python3.2
|
||||
deps =
|
||||
Django>=1.6,<1.7
|
||||
{[deps]three_two}
|
||||
|
||||
[testenv:py27-1.6.X]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
Django>=1.6,<1.7
|
||||
{[deps]two}
|
||||
|
||||
[testenv:py26-1.6.X]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
Django>=1.6,<1.7
|
||||
{[deps]two}
|
||||
|
||||
[testenv:py33-1.5.X]
|
||||
basepython = python3.3
|
||||
deps =
|
||||
Django>=1.5,<1.6
|
||||
1.4.X: Django>=1.4,<1.5
|
||||
1.5.X: Django>=1.5,<1.6
|
||||
1.6.X: Django>=1.6,<1.7
|
||||
1.7.X: Django>=1.7,<1.8
|
||||
1.8.X: Django>=1.8,<1.9
|
||||
py26: {[deps]two}
|
||||
py27: {[deps]two}
|
||||
py32: {[deps]three_two}
|
||||
py33: {[deps]three}
|
||||
py34: {[deps]three}
|
||||
django-discover-runner
|
||||
{[deps]three}
|
||||
|
||||
[testenv:py32-1.5.X]
|
||||
basepython = python3.2
|
||||
deps =
|
||||
Django>=1.5,<1.6
|
||||
django-discover-runner
|
||||
{[deps]three_two}
|
||||
|
||||
[testenv:py27-1.5.X]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
Django>=1.5,<1.6
|
||||
django-discover-runner
|
||||
{[deps]two}
|
||||
|
||||
[testenv:py26-1.5.X]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
Django>=1.5,<1.6
|
||||
django-discover-runner
|
||||
{[deps]two}
|
||||
|
||||
[testenv:py27-1.4.X]
|
||||
basepython = python2.7
|
||||
deps =
|
||||
Django>=1.4,<1.5
|
||||
django-discover-runner
|
||||
{[deps]two}
|
||||
|
||||
[testenv:py26-1.4.X]
|
||||
basepython = python2.6
|
||||
deps =
|
||||
Django>=1.4,<1.5
|
||||
django-discover-runner
|
||||
{[deps]two}
|
||||
|
||||
Reference in New Issue
Block a user