From fc50651260b1efb777f3db96f4728a262b39acd0 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Thu, 7 Apr 2011 16:00:34 +0200 Subject: [PATCH] Made Compressor.concat a cached property instead of a method and use it in the hash and combined properties. Refactored Compressor.outline method to proxy to mode specific methods. --- .gitignore | 1 + compressor/base.py | 94 ++++++++++++------- compressor/css.py | 18 ++-- compressor/exceptions.py | 6 ++ compressor/templates/compressor/css.html | 2 +- compressor/templates/compressor/css_file.html | 1 + compressor/templates/compressor/js.html | 2 +- compressor/templates/compressor/js_file.html | 1 + compressor/templatetags/compress.py | 7 +- compressor/tests/tests.py | 13 +-- 10 files changed, 89 insertions(+), 56 deletions(-) create mode 100644 compressor/templates/compressor/css_file.html create mode 100644 compressor/templates/compressor/js_file.html diff --git a/.gitignore b/.gitignore index 9b59a6a..9d802fb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ build compressor/tests/media/CACHE compressor/tests/media/custom +compressor/tests/media/js/3f33b9146e12.js dist MANIFEST *.pyc diff --git a/compressor/base.py b/compressor/base.py index 6cf04a0..d0213fe 100644 --- a/compressor/base.py +++ b/compressor/base.py @@ -9,7 +9,7 @@ from django.template.loader import render_to_string from compressor.cache import get_hexdigest, get_mtime from compressor.conf import settings -from compressor.exceptions import UncompressableFileError +from compressor.exceptions import CompressorError, UncompressableFileError from compressor.filters import CompilerFilter from compressor.storage import default_storage from compressor.utils import get_class, cached_property @@ -27,6 +27,7 @@ class Compressor(object): self.output_prefix = output_prefix self.charset = settings.DEFAULT_CHARSET self.precompilers = settings.COMPRESS_PRECOMPILERS + self.storage = default_storage self.split_content = [] self.extra_context = {} @@ -73,10 +74,6 @@ class Compressor(object): return "django_compressor.%s.%s" % (socket.gethostname(), get_hexdigest(cachestr)[:12]) - @cached_property - def storage(self): - return default_storage - @cached_property def hunks(self): for kind, value, elem in self.split_contents(): @@ -102,8 +99,9 @@ class Compressor(object): charset = attribs.get("charset", self.charset) yield unicode(content, charset) + @cached_property def concat(self): - return "\n".join((hunk.encode(self.charset) for hunk in self.hunks)) + return '\n'.join((hunk.encode(self.charset) for hunk in self.hunks)) def matches_patterns(self, path, patterns=[]): """ @@ -159,37 +157,65 @@ class Compressor(object): @cached_property def combined(self): - return self.filter(self.concat(), method="output") + return self.filter(self.concat, method="output") - @cached_property - def hash(self): - return get_hexdigest(self.concat())[:12] + def hash(self, content): + return get_hexdigest(content)[:12] - @cached_property - def new_filepath(self): + def filepath(self, content): return os.path.join(settings.COMPRESS_OUTPUT_DIR.strip(os.sep), - self.output_prefix, "%s.%s" % (self.hash, self.type)) + self.output_prefix, "%s.%s" % (self.hash(content), self.type)) - def save_file(self): - if self.storage.exists(self.new_filepath): - return False - self.storage.save(self.new_filepath, ContentFile(self.combined)) - return True - - def output(self, forced=False): - if not settings.COMPRESS_ENABLED and not forced: - return self.content - context = { - "saved": self.save_file(), - "url": self.storage.url(self.new_filepath), - } - context.update(self.extra_context) - return render_to_string(self.template_name, context) - - def output_inline(self): - if settings.COMPRESS_ENABLED: + def output(self, mode='file', forced=False): + """ + The general output method, override in subclass if you need to do + any custom modification. Calls other mode specific methods or simply + returns the content directly. + """ + # First check whether we should do the full compression, + # including precompilation (or if it's forced) + if settings.COMPRESS_ENABLED or forced: content = self.combined + elif self.precompilers: + # or concatting it, if pre-compilation is enabled + content = self.concat else: - content = self.concat() - context = dict(content=content, **self.extra_context) - return render_to_string(self.template_name_inline, context) + # or just doing nothing, when neither + # compression nor compilation is enabled + return self.content + # Then check for the appropriate output method and call it + output_func = getattr(self, "output_%s" % mode, None) + if callable(output_func): + return output_func(mode, content) + # Total failure, raise a general exception + raise CompressorError( + "Couldn't find output method for mode '%s'" % mode) + + def output_file(self, mode, content): + """ + The output method that saves the content to a file and renders + the appropriate template with the file's URL. + """ + new_filepath = self.filepath(content) + if not self.storage.exists(new_filepath): + self.storage.save(new_filepath, ContentFile(content)) + url = self.storage.url(new_filepath) + return self.render_output(mode, {"url": url}) + + def output_inline(self, mode, content): + """ + The output method that directly returns the content for inline + display. + """ + return self.render_output(mode, {"content": content}) + + def render_output(self, mode, context=None): + """ + Renders the compressor output with the appropriate template for + the given mode and template context. + """ + if context is None: + context = {} + context.update(self.extra_context) + return render_to_string( + "compressor/%s_%s.html" % (self.type, mode), context) diff --git a/compressor/css.py b/compressor/css.py index 3fab0cd..c88e473 100644 --- a/compressor/css.py +++ b/compressor/css.py @@ -42,14 +42,14 @@ class CssCompressor(Compressor): self.media_nodes.append((media, node)) return self.split_content - def output(self, forced=False): + def output(self, *args, **kwargs): self.split_contents() if not hasattr(self, 'media_nodes'): - return super(CssCompressor, self).output(forced=forced) - if not settings.COMPRESS_ENABLED and not forced: - return self.content - ret = [] - for media, subnode in self.media_nodes: - subnode.extra_context.update({'media': media}) - ret.append(subnode.output(forced=forced)) - return "".join(ret) + return super(CssCompressor, self).output(*args, **kwargs) + if settings.COMPRESS_ENABLED or kwargs.get('forced', False): + ret = [] + for media, subnode in self.media_nodes: + subnode.extra_context.update({'media': media}) + ret.append(subnode.output(*args, **kwargs)) + return "".join(ret) + return self.content diff --git a/compressor/exceptions.py b/compressor/exceptions.py index 42ae7c4..407f525 100644 --- a/compressor/exceptions.py +++ b/compressor/exceptions.py @@ -1,3 +1,9 @@ +class CompressorError(Exception): + """ + A general error of the compressor + """ + pass + class UncompressableFileError(Exception): """ This exception is raised when a file cannot be compressed diff --git a/compressor/templates/compressor/css.html b/compressor/templates/compressor/css.html index 3d593f1..f4c5e38 100644 --- a/compressor/templates/compressor/css.html +++ b/compressor/templates/compressor/css.html @@ -1 +1 @@ - +{# left fot backwards compatibility #}{% include "compressor/css_file.html" %} \ No newline at end of file diff --git a/compressor/templates/compressor/css_file.html b/compressor/templates/compressor/css_file.html new file mode 100644 index 0000000..3d593f1 --- /dev/null +++ b/compressor/templates/compressor/css_file.html @@ -0,0 +1 @@ + diff --git a/compressor/templates/compressor/js.html b/compressor/templates/compressor/js.html index 8419c20..f1b198f 100644 --- a/compressor/templates/compressor/js.html +++ b/compressor/templates/compressor/js.html @@ -1 +1 @@ - \ No newline at end of file +{# left fot backwards compatibility #}{% include "compressor/js_file.html" %} \ No newline at end of file diff --git a/compressor/templates/compressor/js_file.html b/compressor/templates/compressor/js_file.html new file mode 100644 index 0000000..8419c20 --- /dev/null +++ b/compressor/templates/compressor/js_file.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/compressor/templatetags/compress.py b/compressor/templatetags/compress.py index 6f74ca7..de4a23a 100644 --- a/compressor/templatetags/compress.py +++ b/compressor/templatetags/compress.py @@ -51,8 +51,7 @@ class CompressorNode(template.Node): def render(self, context, forced=False): if (settings.COMPRESS_ENABLED and settings.COMPRESS_OFFLINE) and not forced: - key = get_offline_cachekey(self.nodelist) - content = cache.get(key) + content = cache.get(get_offline_cachekey(self.nodelist)) if content: return content content = self.nodelist.render(context) @@ -64,9 +63,7 @@ class CompressorNode(template.Node): output = self.cache_get(cachekey) if output is None or forced: try: - if self.mode == OUTPUT_INLINE: - return compressor.output_inline() - output = compressor.output(forced=forced) + output = compressor.output(self.mode, forced=forced) self.cache_set(cachekey, output) except: if settings.DEBUG: diff --git a/compressor/tests/tests.py b/compressor/tests/tests.py index e2c9e43..7b37a3c 100644 --- a/compressor/tests/tests.py +++ b/compressor/tests/tests.py @@ -25,6 +25,7 @@ class CompressorTestCase(TestCase): def setUp(self): settings.COMPRESS_ENABLED = True + settings.PRECOMPILERS = {} self.css = """ @@ -71,7 +72,7 @@ class CompressorTestCase(TestCase): self.assert_(is_cachekey.match(self.css_node.cachekey), "cachekey is returning something that doesn't look like r'django_compressor\.%s\.\w{12}'" % host_name) def test_css_hash(self): - self.assertEqual('f7c661b7a124', self.css_node.hash) + self.assertEqual('f7c661b7a124', self.css_node.hash(self.css_node.concat)) def test_css_return_if_on(self): output = u'' @@ -91,7 +92,7 @@ class CompressorTestCase(TestCase): def test_js_concat(self): out = u'obj = {};\nobj.value = "value";' - self.assertEqual(out, self.js_node.concat()) + self.assertEqual(out, self.js_node.concat) def test_js_output(self): out = u'obj={};obj.value="value";' @@ -384,10 +385,10 @@ class OfflineGenerationTestCase(TestCase): def test_offline(self): count, result = CompressCommand().compress() self.assertEqual(2, count) - self.assertEqual(result, [ + self.assertEqual([ u'\n', u'', - ]) + ], result) def test_offline_with_context(self): self._old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT @@ -396,8 +397,8 @@ class OfflineGenerationTestCase(TestCase): } count, result = CompressCommand().compress() self.assertEqual(2, count) - self.assertEqual(result, [ + self.assertEqual([ u'\n', u'', - ]) + ], result) settings.COMPRESS_OFFLINE_CONTEXT = self._old_offline_context