Commit aaa23876 authored by Thomas Goirand's avatar Thomas Goirand

Merge tag '2.1.1' into debian/pike

parents eb26054a e2b7a3ef
...@@ -13,3 +13,4 @@ docs/_build/ ...@@ -13,3 +13,4 @@ docs/_build/
.sass-cache .sass-cache
.coverage .coverage
.tox .tox
.eggs
language: python language: python
sudo: false sudo: false
install: install:
- if [[ $TOXENV == py32-1.8.X ]]; then pip install pip\<8.0.0 virtualenv\<14.0.0; fi
- pip install tox - pip install tox
script: script:
- tox - tox
...@@ -11,15 +12,17 @@ env: ...@@ -11,15 +12,17 @@ env:
- TOXENV=py34-1.8.X - TOXENV=py34-1.8.X
- TOXENV=py27-1.9.X - TOXENV=py27-1.9.X
- TOXENV=py34-1.9.X - TOXENV=py34-1.9.X
- TOXENV=py27-1.10.X
- TOXENV=py34-1.10.X
# https://github.com/travis-ci/travis-ci/issues/4794 # https://github.com/travis-ci/travis-ci/issues/4794
matrix: matrix:
include: include:
- python: 3.5 - python: 3.5
env: env: TOXENV=py35-1.8.X
- TOXENV=py35-1.8.X
- python: 3.5 - python: 3.5
env: env: TOXENV=py35-1.9.X
- TOXENV=py35-1.9.X - python: 3.5
env: TOXENV=py35-1.10.X
notifications: notifications:
irc: "irc.freenode.org#django-compressor" irc: "irc.freenode.org#django-compressor"
after_success: after_success:
......
Django Compressor Django Compressor
================= =================
.. image:: http://codecov.io/github/django-compressor/django-compressor/coverage.svg?branch=develop .. image:: https://codecov.io/github/django-compressor/django-compressor/coverage.svg?branch=develop
:target: http://codecov.io/github/django-compressor/django-compressor?branch=develop :target: https://codecov.io/github/django-compressor/django-compressor?branch=develop
.. image:: https://pypip.in/v/django_compressor/badge.svg .. image:: https://img.shields.io/pypi/v/django_compressor.svg
:target: https://pypi.python.org/pypi/django_compressor
.. image:: https://pypip.in/d/django_compressor/badge.svg
:target: https://pypi.python.org/pypi/django_compressor :target: https://pypi.python.org/pypi/django_compressor
.. image:: https://secure.travis-ci.org/django-compressor/django-compressor.svg?branch=develop .. image:: https://secure.travis-ci.org/django-compressor/django-compressor.svg?branch=develop
...@@ -17,34 +14,39 @@ Django Compressor ...@@ -17,34 +14,39 @@ Django Compressor
.. image:: https://caniusepython3.com/project/django_compressor.svg .. image:: https://caniusepython3.com/project/django_compressor.svg
:target: https://caniusepython3.com/project/django_compressor :target: https://caniusepython3.com/project/django_compressor
Django Compressor combines and compresses linked and inline Javascript Django Compressor processes, combines and minifies linked and inline
or CSS in a Django template into cacheable static files by using the Javascript or CSS in a Django template into cacheable static files.
``compress`` template tag.
It supports compilers such as coffeescript, LESS and SASS and is
extensible by custom processing steps.
Django Compressor is compatible with Django 1.8 and newer.
HTML in between ``{% compress js/css %}`` and ``{% endcompress %}`` is How it works
parsed and searched for CSS or JS. These styles and scripts are subsequently ------------
processed with optional, configurable compilers and filters. In your templates, all HTML code between the tags ``{% compress js/css %}`` and
``{% endcompress %}`` is parsed and searched for CSS or JS. These styles and
scripts are subsequently processed with optional, configurable compilers and
filters.
The default filter for CSS rewrites paths to static files to be absolute The default filter for CSS rewrites paths to static files to be absolute.
and adds a cache busting timestamp. For Javascript the default filter Both Javascript and CSS files are by default concatenated and minified.
compresses it using ``jsmin``.
As the final result the template tag outputs a ``<script>`` or ``<link>`` As the final step the template tag outputs a ``<script>`` or ``<link>``
tag pointing to the optimized file. These files are stored inside a folder tag pointing to the optimized file. Alternatively it can also
and given a unique name based on their content. Alternatively it can also inline the resulting content into the original template directly.
return the resulting content to the original template directly.
Since the file name is dependent on the content these files can be given Since the file name is dependent on the content, these files can be given
a far future expiration date without worrying about stale browser caches. a far future expiration date without worrying about stale browser caches.
The concatenation and compressing process can also be jump started outside For increased performance, the concatenation and compressing process
of the request/response cycle by using the Django management command can also be run once manually outside of the request/response cycle by using
``manage.py compress``. the Django management command ``manage.py compress``.
Configurability & Extendibility Configurability & Extensibility
------------------------------- -------------------------------
Django Compressor is highly configurable and extendible. The HTML parsing Django Compressor is highly configurable and extensible. The HTML parsing
is done using lxml_ or if it's not available Python's built-in HTMLParser by is done using lxml_ or if it's not available Python's built-in HTMLParser by
default. As an alternative Django Compressor provides a BeautifulSoup_ and a default. As an alternative Django Compressor provides a BeautifulSoup_ and a
html5lib_ based parser, as well as an abstract base class that makes it easy to html5lib_ based parser, as well as an abstract base class that makes it easy to
...@@ -78,5 +80,5 @@ The in-development version of Django Compressor can be installed with ...@@ -78,5 +80,5 @@ The in-development version of Django Compressor can be installed with
.. _JSMin: http://www.crockford.com/javascript/jsmin.html .. _JSMin: http://www.crockford.com/javascript/jsmin.html
.. _csscompressor: https://github.com/sprymix/csscompressor .. _csscompressor: https://github.com/sprymix/csscompressor
.. _data URIs: http://en.wikipedia.org/wiki/Data_URI_scheme .. _data URIs: http://en.wikipedia.org/wiki/Data_URI_scheme
.. _django-compressor.readthedocs.org: http://django-compressor.readthedocs.org/en/latest/ .. _django-compressor.readthedocs.org: https://django-compressor.readthedocs.io/en/latest/
.. _github.com/django-compressor/django-compressor: https://github.com/django-compressor/django-compressor .. _github.com/django-compressor/django-compressor: https://github.com/django-compressor/django-compressor
# following PEP 386 # following PEP 386
__version__ = "2.0" __version__ = "2.1.1"
...@@ -73,13 +73,10 @@ class CompressorConf(AppConf): ...@@ -73,13 +73,10 @@ class CompressorConf(AppConf):
# Returns the Jinja2 environment to use in offline compression. # Returns the Jinja2 environment to use in offline compression.
def JINJA2_GET_ENVIRONMENT(): def JINJA2_GET_ENVIRONMENT():
alias = 'Jinja2' alias = 'jinja2'
try: try:
from django.template.loader import _engine_list from django.template import engines
engines = _engine_list(alias) return engines[alias].env
if engines:
engine = engines[0]
return engine.env
except InvalidTemplateEngineError: except InvalidTemplateEngineError:
raise InvalidTemplateEngineError( raise InvalidTemplateEngineError(
"Could not find config for '{}' " "Could not find config for '{}' "
......
...@@ -126,7 +126,7 @@ class CompilerFilter(FilterBase): ...@@ -126,7 +126,7 @@ class CompilerFilter(FilterBase):
if isinstance(self.options, dict): if isinstance(self.options, dict):
# turn dict into a tuple # turn dict into a tuple
new_options = () new_options = ()
for item in kwargs.items(): for item in self.options.items():
new_options += (item,) new_options += (item,)
self.options = new_options self.options = new_options
...@@ -221,7 +221,7 @@ class CachedCompilerFilter(CompilerFilter): ...@@ -221,7 +221,7 @@ class CachedCompilerFilter(CompilerFilter):
key = self.get_cache_key() key = self.get_cache_key()
data = cache.get(key) data = cache.get(key)
if data is not None: if data is not None:
return data return smart_text(data)
filtered = super(CachedCompilerFilter, self).input(**kwargs) filtered = super(CachedCompilerFilter, self).input(**kwargs)
cache.set(key, filtered, settings.COMPRESS_REBUILD_TIMEOUT) cache.set(key, filtered, settings.COMPRESS_REBUILD_TIMEOUT)
return filtered return filtered
......
...@@ -84,19 +84,23 @@ class CssAbsoluteFilter(FilterBase): ...@@ -84,19 +84,23 @@ class CssAbsoluteFilter(FilterBase):
def _converter(self, matchobj, group, template): def _converter(self, matchobj, group, template):
url = matchobj.group(group) url = matchobj.group(group)
url = url.strip(' \'"')
url = url.strip()
wrap = '"' if url[0] == '"' else "'"
url = url.strip('\'"')
if url.startswith('#'): if url.startswith('#'):
return "url('%s')" % url return template % (wrap, url, wrap)
elif url.startswith(SCHEMES): elif url.startswith(SCHEMES):
return "url('%s')" % self.add_suffix(url) return template % (wrap, self.add_suffix(url), wrap)
full_url = posixpath.normpath('/'.join([str(self.directory_name), full_url = posixpath.normpath('/'.join([str(self.directory_name),
url])) url]))
if self.has_scheme: if self.has_scheme:
full_url = "%s%s" % (self.protocol, full_url) full_url = "%s%s" % (self.protocol, full_url)
return template % self.add_suffix(full_url) return template % (wrap, self.add_suffix(full_url), wrap)
def url_converter(self, matchobj): def url_converter(self, matchobj):
return self._converter(matchobj, 1, "url('%s')") return self._converter(matchobj, 1, "url(%s%s%s)")
def src_converter(self, matchobj): def src_converter(self, matchobj):
return self._converter(matchobj, 2, "src='%s'") return self._converter(matchobj, 2, "src=%s%s%s")
...@@ -2,9 +2,8 @@ ...@@ -2,9 +2,8 @@
import os import os
import sys import sys
from collections import OrderedDict from collections import OrderedDict, defaultdict
from fnmatch import fnmatch from fnmatch import fnmatch
from optparse import make_option
from importlib import import_module from importlib import import_module
import django import django
...@@ -13,14 +12,12 @@ import django.template ...@@ -13,14 +12,12 @@ import django.template
from django.template import Context from django.template import Context
from django.utils import six from django.utils import six
from django.template.loader import get_template # noqa Leave this in to preload template locations from django.template.loader import get_template # noqa Leave this in to preload template locations
from django.template.utils import InvalidTemplateEngineError
from django.template import engines from django.template import engines
from compressor.cache import get_offline_hexdigest, write_offline_manifest from compressor.cache import get_offline_hexdigest, write_offline_manifest
from compressor.conf import settings from compressor.conf import settings
from compressor.exceptions import (OfflineGenerationError, TemplateSyntaxError, from compressor.exceptions import (OfflineGenerationError, TemplateSyntaxError,
TemplateDoesNotExist) TemplateDoesNotExist)
from compressor.templatetags.compress import CompressorNode
from compressor.utils import get_mod_func from compressor.utils import get_mod_func
if six.PY3: if six.PY3:
...@@ -36,23 +33,23 @@ else: ...@@ -36,23 +33,23 @@ else:
class Command(BaseCommand): class Command(BaseCommand):
help = "Compress content outside of the request/response cycle" help = "Compress content outside of the request/response cycle"
option_list = BaseCommand.option_list + (
make_option('--extension', '-e', action='append', dest='extensions', def add_arguments(self, parser):
help='The file extension(s) to examine (default: ".html", ' parser.add_argument('--extension', '-e', action='append', dest='extensions',
'separate multiple extensions with commas, or use -e ' help='The file extension(s) to examine (default: ".html", '
'multiple times)'), 'separate multiple extensions with commas, or use -e '
make_option('-f', '--force', default=False, action='store_true', 'multiple times)')
help="Force the generation of compressed content even if the " parser.add_argument('-f', '--force', default=False, action='store_true',
"COMPRESS_ENABLED setting is not True.", dest='force'), help="Force the generation of compressed content even if the "
make_option('--follow-links', default=False, action='store_true', "COMPRESS_ENABLED setting is not True.", dest='force')
help="Follow symlinks when traversing the COMPRESS_ROOT " parser.add_argument('--follow-links', default=False, action='store_true',
"(which defaults to STATIC_ROOT). Be aware that using this " help="Follow symlinks when traversing the COMPRESS_ROOT "
"can lead to infinite recursion if a link points to a parent " "(which defaults to STATIC_ROOT). Be aware that using this "
"directory of itself.", dest='follow_links'), "can lead to infinite recursion if a link points to a parent "
make_option('--engine', default="django", action="store", "directory of itself.", dest='follow_links')
help="Specifies the templating engine. jinja2 or django", parser.add_argument('--engine', default="django", action="store",
dest="engine"), help="Specifies the templating engine. jinja2 or django",
) dest="engine")
def get_loaders(self): def get_loaders(self):
template_source_loaders = [] template_source_loaders = []
...@@ -107,10 +104,11 @@ class Command(BaseCommand): ...@@ -107,10 +104,11 @@ class Command(BaseCommand):
verbosity = int(options.get("verbosity", 0)) verbosity = int(options.get("verbosity", 0))
if not log: if not log:
log = StringIO() log = StringIO()
if not settings.TEMPLATE_LOADERS: if not self.get_loaders():
raise OfflineGenerationError("No template loaders defined. You " raise OfflineGenerationError("No template loaders defined. You "
"must set TEMPLATE_LOADERS in your " "must set TEMPLATE_LOADERS in your "
"settings.") "settings or set 'loaders' in your "
"TEMPLATES dictionary.")
templates = set() templates = set()
if engine == 'django': if engine == 'django':
paths = set() paths = set()
...@@ -156,6 +154,18 @@ class Command(BaseCommand): ...@@ -156,6 +154,18 @@ class Command(BaseCommand):
if verbosity > 1: if verbosity > 1:
log.write("Found templates:\n\t" + "\n\t".join(templates) + "\n") log.write("Found templates:\n\t" + "\n\t".join(templates) + "\n")
contexts = settings.COMPRESS_OFFLINE_CONTEXT
if isinstance(contexts, six.string_types):
try:
module, function = get_mod_func(contexts)
contexts = getattr(import_module(module), function)()
except (AttributeError, ImportError, TypeError) as e:
raise ImportError("Couldn't import offline context function %s: %s" %
(settings.COMPRESS_OFFLINE_CONTEXT, e))
elif not isinstance(contexts, (list, tuple)):
contexts = [contexts]
contexts = list(contexts) # evaluate generator
parser = self.__get_parser(engine) parser = self.__get_parser(engine)
compressor_nodes = OrderedDict() compressor_nodes = OrderedDict()
for template_name in templates: for template_name in templates:
...@@ -177,16 +187,23 @@ class Command(BaseCommand): ...@@ -177,16 +187,23 @@ class Command(BaseCommand):
if verbosity > 0: if verbosity > 0:
log.write("UnicodeDecodeError while trying to read " log.write("UnicodeDecodeError while trying to read "
"template %s\n" % template_name) "template %s\n" % template_name)
try:
nodes = list(parser.walk_nodes(template))
except (TemplateDoesNotExist, TemplateSyntaxError) as e:
# Could be an error in some base template
if verbosity > 0:
log.write("Error parsing template %s: %s\n" % (template_name, e))
continue continue
if nodes:
template.template_name = template_name for context_dict in contexts:
compressor_nodes.setdefault(template, []).extend(nodes) context = parser.get_init_context(context_dict)
context = Context(context)
try:
nodes = list(parser.walk_nodes(template, context=context))
except (TemplateDoesNotExist, TemplateSyntaxError) as e:
# Could be an error in some base template
if verbosity > 0:
log.write("Error parsing template %s: %s\n" % (template_name, e))
continue
if nodes:
template.template_name = template_name
template_nodes = compressor_nodes.setdefault(template, OrderedDict())
for node in nodes:
template_nodes.setdefault(node, []).append(context)
if not compressor_nodes: if not compressor_nodes:
raise OfflineGenerationError( raise OfflineGenerationError(
...@@ -199,36 +216,23 @@ class Command(BaseCommand): ...@@ -199,36 +216,23 @@ class Command(BaseCommand):
"\n\t".join((t.template_name "\n\t".join((t.template_name
for t in compressor_nodes.keys())) + "\n") for t in compressor_nodes.keys())) + "\n")
contexts = settings.COMPRESS_OFFLINE_CONTEXT
if isinstance(contexts, six.string_types):
try:
module, function = get_mod_func(contexts)
contexts = getattr(import_module(module), function)()
except (AttributeError, ImportError, TypeError) as e:
raise ImportError("Couldn't import offline context function %s: %s" %
(settings.COMPRESS_OFFLINE_CONTEXT, e))
elif not isinstance(contexts, (list, tuple)):
contexts = [contexts]
log.write("Compressing... ") log.write("Compressing... ")
block_count = context_count = 0 block_count = 0
compressed_contexts = []
results = [] results = []
offline_manifest = OrderedDict() offline_manifest = OrderedDict()
for template, nodes in compressor_nodes.items():
for context_dict in contexts: template._log = log
context_count += 1 template._log_verbosity = verbosity
init_context = parser.get_init_context(context_dict)
for node, contexts in nodes.items():
for template, nodes in compressor_nodes.items(): for context in contexts:
context = Context(init_context) if context not in compressed_contexts:
template._log = log compressed_contexts.append(context)
template._log_verbosity = verbosity
if not parser.process_template(template, context):
continue
for node in nodes:
context.push() context.push()
if not parser.process_template(template, context):
continue
parser.process_node(template, context, node) parser.process_node(template, context, node)
rendered = parser.render_nodelist(template, context, node) rendered = parser.render_nodelist(template, context, node)
key = get_offline_hexdigest(rendered) key = get_offline_hexdigest(rendered)
...@@ -248,6 +252,7 @@ class Command(BaseCommand): ...@@ -248,6 +252,7 @@ class Command(BaseCommand):
write_offline_manifest(offline_manifest) write_offline_manifest(offline_manifest)
context_count = len(compressed_contexts)
log.write("done\nCompressed %d block(s) from %d template(s) for %d context(s).\n" % log.write("done\nCompressed %d block(s) from %d template(s) for %d context(s).\n" %
(block_count, len(compressor_nodes), context_count)) (block_count, len(compressor_nodes), context_count))
return block_count, results return block_count, results
......
...@@ -6,31 +6,25 @@ from django.template import Context ...@@ -6,31 +6,25 @@ from django.template import Context
from django.template.base import Node, VariableNode, TextNode, NodeList from django.template.base import Node, VariableNode, TextNode, NodeList
from django.template.defaulttags import IfNode from django.template.defaulttags import IfNode
from django.template.loader import get_template from django.template.loader import get_template
from django.template.loader_tags import ExtendsNode, BlockNode, BlockContext from django.template.loader_tags import BLOCK_CONTEXT_KEY, ExtendsNode, BlockNode, BlockContext
from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist from compressor.exceptions import TemplateSyntaxError, TemplateDoesNotExist
from compressor.templatetags.compress import CompressorNode from compressor.templatetags.compress import CompressorNode
def handle_extendsnode(extendsnode, block_context=None, original=None): def handle_extendsnode(extendsnode, context):
"""Create a copy of Node tree of a derived template replacing """Create a copy of Node tree of a derived template replacing
all blocks tags with the nodes of appropriate blocks. all blocks tags with the nodes of appropriate blocks.
Also handles {{ block.super }} tags. Also handles {{ block.super }} tags.
""" """
if block_context is None: if BLOCK_CONTEXT_KEY not in context.render_context:
block_context = BlockContext() context.render_context[BLOCK_CONTEXT_KEY] = BlockContext()
block_context = context.render_context[BLOCK_CONTEXT_KEY]
blocks = dict((n.name, n) for n in blocks = dict((n.name, n) for n in
extendsnode.nodelist.get_nodes_by_type(BlockNode)) extendsnode.nodelist.get_nodes_by_type(BlockNode))
block_context.add_blocks(blocks) block_context.add_blocks(blocks)
# Note: we pass an empty context when we find the parent, this breaks
# inheritance using variables ({% extends template_var %}) but a refactor
# will be needed to support that use-case with multiple offline contexts.
context = Context()
if original is not None:
context.template = original
compiled_parent = extendsnode.get_parent(context) compiled_parent = extendsnode.get_parent(context)
parent_nodelist = compiled_parent.nodelist parent_nodelist = compiled_parent.nodelist
# If the parent template has an ExtendsNode it is not the root. # If the parent template has an ExtendsNode it is not the root.
...@@ -38,7 +32,7 @@ def handle_extendsnode(extendsnode, block_context=None, original=None): ...@@ -38,7 +32,7 @@ def handle_extendsnode(extendsnode, block_context=None, original=None):
# The ExtendsNode has to be the first non-text node. # The ExtendsNode has to be the first non-text node.
if not isinstance(node, TextNode): if not isinstance(node, TextNode):
if isinstance(node, ExtendsNode): if isinstance(node, ExtendsNode):
return handle_extendsnode(node, block_context, original) return handle_extendsnode(node, context)
break break
# Add blocks of the root template to block context. # Add blocks of the root template to block context.
blocks = dict((n.name, n) for n in blocks = dict((n.name, n) for n in
...@@ -122,10 +116,13 @@ class DjangoParser(object): ...@@ -122,10 +116,13 @@ class DjangoParser(object):
def render_node(self, template, context, node): def render_node(self, template, context, node):
return node.render(context, forced=True) return node.render(context, forced=True)
def get_nodelist(self, node, original=None): def get_nodelist(self, node, original, context):
if isinstance(node, ExtendsNode): if isinstance(node, ExtendsNode):
try: try:
return handle_extendsnode(node, block_context=None, original=original) if context is None:
context = Context()
context.template = original
return handle_extendsnode(node, context)
except template.TemplateSyntaxError as e: except template.TemplateSyntaxError as e:
raise TemplateSyntaxError(str(e)) raise TemplateSyntaxError(str(e))
except template.TemplateDoesNotExist as e: except template.TemplateDoesNotExist as e:
...@@ -140,12 +137,13 @@ class DjangoParser(object): ...@@ -140,12 +137,13 @@ class DjangoParser(object):
nodelist = getattr(node, 'nodelist', []) nodelist = getattr(node, 'nodelist', [])
return nodelist return nodelist
def walk_nodes(self, node, original=None): def walk_nodes(self, node, original=None, context=None):
if original is None: if original is None:
original = node original = node
for node in self.get_nodelist(node, original): for node in self.get_nodelist(node, original, context):
if isinstance(node, CompressorNode) and node.is_offline_compression_enabled(forced=True): if isinstance(node, CompressorNode) \
and node.is_offline_compression_enabled(forced=True):
yield node yield node
else: else:
for node in self.walk_nodes(node, original): for node in self.walk_nodes(node, original, context):
yield node yield node
...@@ -112,7 +112,7 @@ class Jinja2Parser(object): ...@@ -112,7 +112,7 @@ class Jinja2Parser(object):
return body return body
def walk_nodes(self, node, block_name=None): def walk_nodes(self, node, block_name=None, context=None):
for node in self.get_nodelist(node): for node in self.get_nodelist(node):
if (isinstance(node, CallBlock) and if (isinstance(node, CallBlock) and
isinstance(node.call, Call) and isinstance(node.call, Call) and
......
...@@ -70,21 +70,17 @@ class GzipCompressorFileStorage(CompressorFileStorage): ...@@ -70,21 +70,17 @@ class GzipCompressorFileStorage(CompressorFileStorage):
orig_path = self.path(filename) orig_path = self.path(filename)
compressed_path = '%s.gz' % orig_path compressed_path = '%s.gz' % orig_path
f_in = open(orig_path, 'rb') with open(orig_path, 'rb') as f_in, open(compressed_path, 'wb') as f_out:
f_out = open(compressed_path, 'wb') with gzip.GzipFile(fileobj=f_out) as gz_out:
try: gz_out.write(f_in.read())
f_out = gzip.GzipFile(fileobj=f_out)
f_out.write(f_in.read()) # Ensure the file timestamps match.
finally: # os.stat() returns nanosecond resolution on Linux, but os.utime()
f_out.close() # only sets microsecond resolution. Set times on both files to
f_in.close() # ensure they are equal.
# Ensure the file timestamps match. stamp = time.time()
# os.stat() returns nanosecond resolution on Linux, but os.utime() os.utime(orig_path, (stamp, stamp))
# only sets microsecond resolution. Set times on both files to os.utime(compressed_path, (stamp, stamp))
# ensure they are equal.
stamp = time.time()
os.utime(orig_path, (stamp, stamp))
os.utime(compressed_path, (stamp, stamp))
return filename return filename
......
...@@ -16,9 +16,8 @@ def main(): ...@@ -16,9 +16,8 @@ def main():
options, arguments = p.parse_args() options, arguments = p.parse_args()
if options.filename: if options.filename:
f = open(options.filename) with open(options.filename) as f:
content = f.read() content = f.read()
f.close()
else: else:
content = sys.stdin.read() content = sys.stdin.read()
......
...@@ -405,7 +405,8 @@ class CompressorInDebugModeTestCase(SimpleTestCase): ...@@ -405,7 +405,8 @@ class CompressorInDebugModeTestCase(SimpleTestCase):
# files can be outdated # files can be outdated
css_filename = os.path.join(settings.COMPRESS_ROOT, "css", "one.css") css_filename = os.path.join(settings.COMPRESS_ROOT, "css", "one.css")
# Store the hash of the original file's content # Store the hash of the original file's content
css_content = open(css_filename).read() with open(css_filename) as f:
css_content = f.read()
hashed = get_hexdigest(css_content, 12) hashed = get_hexdigest(css_content, 12)
# Now modify the file in the STATIC_ROOT # Now modify the file in the STATIC_ROOT
test_css_content = "p { font-family: 'test' }" test_css_content = "p { font-family: 'test' }"
...@@ -419,6 +420,7 @@ class CompressorInDebugModeTestCase(SimpleTestCase): ...@@ -419,6 +420,7 @@ class CompressorInDebugModeTestCase(SimpleTestCase):
compressor.storage = DefaultStorage() compressor.storage = DefaultStorage()
output = compressor.output() output = compressor.output()
self.assertEqual(expected, output) self.assertEqual(expected, output)
result = open(os.path.join(settings.COMPRESS_ROOT, "CACHE", "css", with open(os.path.join(settings.COMPRESS_ROOT, "CACHE", "css",
"%s.css" % hashed), "r").read() "%s.css" % hashed), "r") as f:
result = f.read()
self.assertTrue(test_css_content not in result) self.assertTrue(test_css_content not in result)
...@@ -3,8 +3,10 @@ from collections import defaultdict ...@@ -3,8 +3,10 @@ from collections import defaultdict
import io import io
import os import os
import sys import sys
import mock
from django.utils import six from django.utils import six
from django.utils.encoding import smart_text
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings from django.test.utils import override_settings
...@@ -41,6 +43,15 @@ class PrecompilerTestCase(TestCase): ...@@ -41,6 +43,15 @@ class PrecompilerTestCase(TestCase):
with io.open(self.filename, encoding=settings.FILE_CHARSET) as file: with io.open(self.filename, encoding=settings.FILE_CHARSET) as file:
self.content = file.read() self.content = file.read()
def test_precompiler_d