diff --git a/compressor/cache.py b/compressor/cache.py
index 9db3625..62986b6 100644
--- a/compressor/cache.py
+++ b/compressor/cache.py
@@ -101,6 +101,19 @@ def get_hashed_mtime(filename, length=12):
return get_hexdigest(mtime, length)
+def get_hashed_content(filename, length=12):
+ try:
+ filename = os.path.realpath(filename)
+ except OSError:
+ return None
+ hash_file = open(filename)
+ try:
+ content = hash_file.read()
+ finally:
+ hash_file.close()
+ return get_hexdigest(content, length)
+
+
def cache_get(key):
packed_val = cache.get(key)
if packed_val is None:
diff --git a/compressor/filters/css_default.py b/compressor/filters/css_default.py
index 3cba152..8a05f87 100644
--- a/compressor/filters/css_default.py
+++ b/compressor/filters/css_default.py
@@ -2,7 +2,8 @@ import os
import re
import posixpath
-from compressor.cache import get_hexdigest, get_hashed_mtime
+from compressor.cache import (get_hexdigest, get_hashed_mtime,
+ get_hashed_content)
from compressor.conf import settings
from compressor.filters import FilterBase, FilterError
from compressor.utils import staticfiles
@@ -61,12 +62,8 @@ class CssAbsoluteFilter(FilterBase):
if filename:
if settings.COMPRESS_CSS_HASHING_METHOD == "mtime":
suffix = get_hashed_mtime(filename)
- elif settings.COMPRESS_CSS_HASHING_METHOD == "hash":
- hash_file = open(filename)
- try:
- suffix = get_hexdigest(hash_file.read(), 12)
- finally:
- hash_file.close()
+ elif settings.COMPRESS_CSS_HASHING_METHOD in ("hash", "content"):
+ suffix = get_hashed_content(filename)
else:
raise FilterError('COMPRESS_CSS_HASHING_METHOD is configured '
'with an unknown method (%s).')
diff --git a/docs/changelog.txt b/docs/changelog.txt
index a0b8f75..d67639f 100644
--- a/docs/changelog.txt
+++ b/docs/changelog.txt
@@ -45,6 +45,10 @@ v1.2.0
- 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'``.
+
v1.1.1
------
diff --git a/docs/settings.txt b/docs/settings.txt
index 7bf98fe..db39c57 100644
--- a/docs/settings.txt
+++ b/docs/settings.txt
@@ -97,8 +97,8 @@ A filter that normalizes the URLs used in ``url()`` CSS statements.
- ``COMPRESS_CSS_HASHING_METHOD`` -- The method to use when calculating
the hash to append to processed URLs. Either ``'mtime'`` (default) or
- ``'hash'``. Use the latter in case you're using multiple server to serve
- your static files.
+ ``'content'``. Use the latter in case you're using multiple server to
+ serve your static files.
``compressor.filters.csstidy.CSSTidyFilter``
""""""""""""""""""""""""""""""""""""""""""""
diff --git a/tests/tests/__init__.py b/tests/tests/__init__.py
index 7212692..dfaadab 100644
--- a/tests/tests/__init__.py
+++ b/tests/tests/__init__.py
@@ -1,8 +1,12 @@
-from .base import CompressorTestCase, CssMediaTestCase, VerboseTestCase, CacheBackendTestCase
-from .filters import CssTidyTestCase, PrecompilerTestCase, CssMinTestCase, CssAbsolutizingTestCase, CssDataUriTestCase
+from .base import (CompressorTestCase, CssMediaTestCase, VerboseTestCase,
+ CacheBackendTestCase)
+from .filters import (CssTidyTestCase, PrecompilerTestCase, CssMinTestCase,
+ CssAbsolutizingTestCase, CssAbsolutizingTestCaseWithHash,
+ CssDataUriTestCase)
from .jinja2ext import TestJinja2CompressorExtension
from .offline import OfflineGenerationTestCase
-from .parsers import LxmlParserTests, Html5LibParserTests, BeautifulSoupParserTests, HtmlParserTests
+from .parsers import (LxmlParserTests, Html5LibParserTests,
+ BeautifulSoupParserTests, HtmlParserTests)
from .signals import PostCompressSignalTestCase
from .storages import StorageTestCase
from .templatetags import TemplatetagTestCase, PrecompilerTemplatetagTestCase
diff --git a/tests/tests/filters.py b/tests/tests/filters.py
index 1645fa6..abf74ed 100644
--- a/tests/tests/filters.py
+++ b/tests/tests/filters.py
@@ -5,14 +5,15 @@ from unittest2 import skipIf
from django.test import TestCase
-from compressor.cache import get_hashed_mtime
+from compressor.cache import get_hashed_mtime, get_hashed_content
from compressor.conf import settings
from compressor.css import CssCompressor
from compressor.utils import find_command
from compressor.filters.base import CompilerFilter
+from compressor.filters.cssmin import CSSMinFilter
+from compressor.filters.css_default import CssAbsoluteFilter
-from .templatetags import render
-from .base import css_tag, test_dir
+from .base import test_dir
class CssTidyTestCase(TestCase):
@@ -39,7 +40,7 @@ class PrecompilerTestCase(TestCase):
self.filename = os.path.join(test_dir, 'media/css/one.css')
with open(self.filename) as f:
self.content = f.read()
- self.test_precompiler = os.path.join(test_dir, 'precompiler.py')
+ 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)
@@ -47,29 +48,28 @@ class PrecompilerTestCase(TestCase):
self.assertEqual(u"body { color:#990; }", compiler.input())
def test_precompiler_infile_stdout(self):
- command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler)
+ command = '%s %s -f {infile}' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=None, command=command)
self.assertEqual(u"body { color:#990; }%s" % os.linesep, compiler.input())
def test_precompiler_stdin_outfile(self):
- command = '%s %s -o {outfile}' % (sys.executable, self.test_precompiler)
+ command = '%s %s -o {outfile}' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=None, command=command)
self.assertEqual(u"body { color:#990; }", compiler.input())
def test_precompiler_stdin_stdout(self):
- command = '%s %s' % (sys.executable, self.test_precompiler)
+ command = '%s %s' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=None, command=command)
self.assertEqual(u"body { color:#990; }%s" % os.linesep, compiler.input())
def test_precompiler_stdin_stdout_filename(self):
- command = '%s %s' % (sys.executable, self.test_precompiler)
+ command = '%s %s' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
self.assertEqual(u"body { color:#990; }%s" % os.linesep, compiler.input())
class CssMinTestCase(TestCase):
def test_cssmin_filter(self):
- from compressor.filters.cssmin import CSSMinFilter
content = """p {
@@ -78,78 +78,88 @@ class CssMinTestCase(TestCase):
}
"""
- output = "p{background:#369 url('../../images/image.gif')}"
+ output = "p{background:#369 url('../../images/image.gif')}"
self.assertEqual(output, CSSMinFilter(content).output())
class CssAbsolutizingTestCase(TestCase):
+ hashing_method = 'mtime'
+ hashing_func = staticmethod(get_hashed_mtime)
+ content = "p { background: url('../../img/python.png') }"
+
def setUp(self):
+ self.old_enabled = settings.COMPRESS_ENABLED
+ self.old_url = settings.COMPRESS_URL
+ self.old_hashing_method = settings.COMPRESS_CSS_HASHING_METHOD
settings.COMPRESS_ENABLED = True
settings.COMPRESS_URL = '/media/'
- settings.COMPRESS_CSS_HASHING_METHOD = 'mtime'
+ settings.COMPRESS_CSS_HASHING_METHOD = self.hashing_method
self.css = """
"""
self.css_node = CssCompressor(self.css)
- def suffix_method(self, filename):
- return get_hashed_mtime(filename)
+ def tearDown(self):
+ settings.COMPRESS_ENABLED = self.old_enabled
+ settings.COMPRESS_URL = self.old_url
+ settings.COMPRESS_CSS_HASHING_METHOD = self.old_hashing_method
def test_css_absolute_filter(self):
- from compressor.filters.css_default import CssAbsoluteFilter
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
- content = "p { background: url('../../img/python.png') }"
- output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
- filter = CssAbsoluteFilter(content)
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.hashing_func(imagefilename))
+ filter = CssAbsoluteFilter(self.content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = 'http://media.example.com/'
- filter = CssAbsoluteFilter(content)
+ filter = CssAbsoluteFilter(self.content)
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
- output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.hashing_func(imagefilename))
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter_https(self):
- from compressor.filters.css_default import CssAbsoluteFilter
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
- content = "p { background: url('../../img/python.png') }"
- output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
- filter = CssAbsoluteFilter(content)
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.hashing_func(imagefilename))
+ filter = CssAbsoluteFilter(self.content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = 'https://media.example.com/'
- filter = CssAbsoluteFilter(content)
+ filter = CssAbsoluteFilter(self.content)
filename = os.path.join(settings.COMPRESS_ROOT, 'css/url/test.css')
- output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.hashing_func(imagefilename))
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_absolute_filter_relative_path(self):
- from compressor.filters.css_default import CssAbsoluteFilter
filename = os.path.join(settings.TEST_DIR, 'whatever', '..', 'media', 'whatever/../css/url/test.css')
imagefilename = os.path.join(settings.COMPRESS_ROOT, 'img/python.png')
- content = "p { background: url('../../img/python.png') }"
- output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
- filter = CssAbsoluteFilter(content)
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.hashing_func(imagefilename))
+ filter = CssAbsoluteFilter(self.content)
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
settings.COMPRESS_URL = 'https://media.example.com/'
- filter = CssAbsoluteFilter(content)
- output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.suffix_method(imagefilename))
+ filter = CssAbsoluteFilter(self.content)
+ output = "p { background: url('%simg/python.png?%s') }" % (settings.COMPRESS_URL, self.hashing_func(imagefilename))
self.assertEqual(output, filter.input(filename=filename, basename='css/url/test.css'))
def test_css_hunks(self):
hash_dict = {
- 'hash1': self.suffix_method(os.path.join(settings.COMPRESS_ROOT, 'img/python.png')),
- 'hash2': self.suffix_method(os.path.join(settings.COMPRESS_ROOT, 'img/add.png')),
+ 'hash1': self.hashing_func(os.path.join(settings.COMPRESS_ROOT, 'img/python.png')),
+ 'hash2': self.hashing_func(os.path.join(settings.COMPRESS_ROOT, 'img/add.png')),
}
- out = [u"p { background: url('/media/img/python.png?%(hash1)s'); }\np { background: url('/media/img/python.png?%(hash1)s'); }\np { background: url('/media/img/python.png?%(hash1)s'); }\np { background: url('/media/img/python.png?%(hash1)s'); }\n" % hash_dict,
- u"p { background: url('/media/img/add.png?%(hash2)s'); }\np { background: url('/media/img/add.png?%(hash2)s'); }\np { background: url('/media/img/add.png?%(hash2)s'); }\np { background: url('/media/img/add.png?%(hash2)s'); }\n" % hash_dict]
hunks = [h for m, h in self.css_node.hunks()]
- self.assertEqual(out, hunks)
+ self.assertEqual([u"""\
+p { background: url('/media/img/python.png?%(hash1)s'); }
+p { background: url('/media/img/python.png?%(hash1)s'); }
+p { background: url('/media/img/python.png?%(hash1)s'); }
+p { background: url('/media/img/python.png?%(hash1)s'); }
+""" % hash_dict,
+ u"""\
+p { background: url('/media/img/add.png?%(hash2)s'); }
+p { background: url('/media/img/add.png?%(hash2)s'); }
+p { background: url('/media/img/add.png?%(hash2)s'); }
+p { background: url('/media/img/add.png?%(hash2)s'); }
+""" % hash_dict], hunks)
def test_guess_filename(self):
- import urllib
- from compressor.filters.css_default import CssAbsoluteFilter
for base_url in ('/media/', 'http://media.example.com/'):
settings.COMPRESS_URL = base_url
url = '%s/img/python.png' % settings.COMPRESS_URL.rstrip('/')
@@ -158,24 +168,19 @@ class CssAbsolutizingTestCase(TestCase):
filter = CssAbsoluteFilter(content)
self.assertEqual(path, filter.guess_filename(url))
+
class CssAbsolutizingTestCaseWithHash(CssAbsolutizingTestCase):
+ hashing_method = 'content'
+ hashing_func = staticmethod(get_hashed_content)
def setUp(self):
- settings.COMPRESS_ENABLED = True
- settings.COMPRESS_URL = '/media/'
- settings.COMPRESS_CSS_HASHING_METHOD = 'hash'
+ super(CssAbsolutizingTestCaseWithHash, self).setUp()
self.css = """
"""
self.css_node = CssCompressor(self.css)
- def suffix_method(self, filename):
- f = open(filename)
- suffix = "H%s" % (get_hexdigest(f.read(), 12), )
- f.close()
- return suffix
-
class CssDataUriTestCase(TestCase):
def setUp(self):
@@ -196,6 +201,3 @@ class CssDataUriTestCase(TestCase):
out = [u'.add { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJvSURBVDjLpZPrS5NhGIf9W7YvBYOkhlkoqCklWChv2WyKik7blnNris72bi6dus0DLZ0TDxW1odtopDs4D8MDZuLU0kXq61CijSIIasOvv94VTUfLiB74fXngup7nvrnvJABJ/5PfLnTTdcwOj4RsdYmo5glBWP6iOtzwvIKSWstI0Wgx80SBblpKtE9KQs/We7EaWoT/8wbWP61gMmCH0lMDvokT4j25TiQU/ITFkek9Ow6+7WH2gwsmahCPdwyw75uw9HEO2gUZSkfyI9zBPCJOoJ2SMmg46N61YO/rNoa39Xi41oFuXysMfh36/Fp0b7bAfWAH6RGi0HglWNCbzYgJaFjRv6zGuy+b9It96N3SQvNKiV9HvSaDfFEIxXItnPs23BzJQd6DDEVM0OKsoVwBG/1VMzpXVWhbkUM2K4oJBDYuGmbKIJ0qxsAbHfRLzbjcnUbFBIpx/qH3vQv9b3U03IQ/HfFkERTzfFj8w8jSpR7GBE123uFEYAzaDRIqX/2JAtJbDat/COkd7CNBva2cMvq0MGxp0PRSCPF8BXjWG3FgNHc9XPT71Ojy3sMFdfJRCeKxEsVtKwFHwALZfCUk3tIfNR8XiJwc1LmL4dg141JPKtj3WUdNFJqLGFVPC4OkR4BxajTWsChY64wmCnMxsWPCHcutKBxMVp5mxA1S+aMComToaqTRUQknLTH62kHOVEE+VQnjahscNCy0cMBWsSI0TCQcZc5ALkEYckL5A5noWSBhfm2AecMAjbcRWV0pUTh0HE64TNf0mczcnnQyu/MilaFJCae1nw2fbz1DnVOxyGTlKeZft/Ff8x1BRssfACjTwQAAAABJRU5ErkJggg=="); }\n.python { background-image: url("/media/img/python.png?%s"); }\n.datauri { background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9YGARc5KB0XV+IAAAAddEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIFRoZSBHSU1Q72QlbgAAAF1JREFUGNO9zL0NglAAxPEfdLTs4BZM4DIO4C7OwQg2JoQ9LE1exdlYvBBeZ7jqch9//q1uH4TLzw4d6+ErXMMcXuHWxId3KOETnnXXV6MJpcq2MLaI97CER3N0 vr4MkhoXe0rZigAAAABJRU5ErkJggg=="); }\n' % datauri_hash]
hunks = [h for m, h in self.css_node.hunks()]
self.assertEqual(out, hunks)
-
-
-