Permalink
Browse files

Merge branch 'release/1.0'

  • Loading branch information...
jezdez committed Sep 7, 2011
2 parents 9717eab + 2fbfc9d commit bb3185b0b46773e5e4ed5b1457a685fb36cdbd3c
Showing with 1,640 additions and 1,019 deletions.
  1. +5 −4 .gitignore
  2. +17 −1 AUTHORS
  3. +1 −1 MANIFEST.in
  4. +2 −16 compressor/__init__.py
  5. +123 −71 compressor/base.py
  6. +29 −7 compressor/cache.py
  7. +35 −31 compressor/conf.py
  8. +11 −13 compressor/css.py
  9. +2 −1 compressor/filters/__init__.py
  10. +89 −29 compressor/filters/base.py
  11. +4 −4 compressor/filters/closure.py
  12. +21 −10 compressor/filters/css_default.py
  13. +3 −5 compressor/filters/cssmin/__init__.py
  14. +4 −4 compressor/filters/csstidy.py
  15. +2 −2 compressor/filters/datauri.py
  16. +7 −6 compressor/filters/jsmin/__init__.py
  17. +2 −1 compressor/filters/jsmin/rjsmin.py
  18. +10 −0 compressor/filters/jsmin/slimit.py
  19. +8 −8 compressor/filters/yui.py
  20. +3 −0 compressor/finders.py
  21. +7 −11 compressor/js.py
  22. +65 −13 compressor/management/commands/compress.py
  23. +1 −1 compressor/management/commands/mtime_cache.py
  24. 0 compressor/{tests → }/models.py
  25. +1 −1 compressor/parser/__init__.py
  26. +1 −1 compressor/parser/{htmlparser.py → default_htmlparser.py}
  27. +14 −10 compressor/parser/lxml.py
  28. +4 −0 compressor/signals.py
  29. +0 −3 compressor/storage.py
  30. +1 −1 compressor/templates/compressor/css_file.html
  31. +1 −1 compressor/templates/compressor/css_inline.html
  32. +1 −1 compressor/templates/compressor/js_file.html
  33. +28 −26 compressor/templatetags/compress.py
  34. +0 −4 compressor/tests/media/css/url/2/url2.css
  35. +0 −4 compressor/tests/media/css/url/url1.css
  36. +0 −14 compressor/tests/templates/test_compressor_offline.html
  37. +0 −525 compressor/tests/tests.py
  38. +0 −1 compressor/utils/__init__.py
  39. +26 −0 compressor/utils/decorators.py
  40. +0 −107 compressor/utils/settings.py
  41. +5 −6 compressor/utils/staticfiles.py
  42. +1 −1 docs/behind-the-scenes.txt
  43. +50 −0 docs/changelog.txt
  44. +2 −2 docs/conf.py
  45. +10 −1 docs/installation.txt
  46. +3 −0 docs/remote-storages.txt
  47. +103 −6 docs/settings.txt
  48. +62 −10 docs/usage.txt
  49. +16 −9 setup.py
  50. 0 {compressor → }/tests/__init__.py
  51. 0 {compressor → }/tests/media/css/datauri.css
  52. 0 {compressor → }/tests/media/css/nonasc.css
  53. 0 {compressor → }/tests/media/css/one.css
  54. 0 {compressor → }/tests/media/css/two.css
  55. +4 −0 tests/media/css/url/2/url2.css
  56. 0 {compressor → }/tests/media/css/url/nonasc.css
  57. 0 {compressor → }/tests/media/css/url/test.css
  58. +4 −0 tests/media/css/url/url1.css
  59. BIN {compressor → }/tests/media/img/add.png
  60. BIN {compressor → }/tests/media/img/python.png
  61. 0 {compressor → }/tests/media/js/nonasc-latin1.js
  62. 0 {compressor → }/tests/media/js/nonasc.js
  63. 0 {compressor → }/tests/media/js/one.js
  64. 0 tests/models.py
  65. +3 −3 {compressor → }/tests/precompiler.py
  66. +3 −4 {compressor → }/tests/runtests.py
  67. +24 −0 tests/settings.py
  68. +16 −0 tests/templates/base.html
  69. +42 −0 tests/templates/test_compressor_offline.html
  70. +7 −0 tests/tests/__init__.py
  71. +164 −0 tests/tests/base.py
  72. +192 −0 tests/tests/filters.py
  73. +82 −0 tests/tests/offline.py
  74. +78 −0 tests/tests/parsers.py
  75. +67 −0 tests/tests/signals.py
  76. +33 −0 tests/tests/storages.py
  77. +115 −0 tests/tests/templatetags.py
  78. +26 −39 { → tests}/tox.ini
View
@@ -1,7 +1,7 @@
build
-compressor/tests/media/CACHE
-compressor/tests/media/custom
-compressor/tests/media/js/066cd253eada.js
+tests/media/CACHE
+tests/media/custom
+tests/media/js/066cd253eada.js
dist
MANIFEST
*.pyc
@@ -10,4 +10,5 @@ MANIFEST
*.egg
docs/_build/
.coverage
-htmlcov
+htmlcov
+.sass-cache
View
18 AUTHORS
@@ -1,4 +1,4 @@
-Christian Metts <xian@mintchaos.com>
+Christian Metts
Carl Meyer
Jannis Leidel
@@ -9,25 +9,41 @@ Contributors:
Aaron Godfrey
Atamert Ölçgen
+Aymeric Augustin
Ben Spaulding
Benjamin Wohlwend
Brad Whittington
+Bruno Renié
Chris Adams
+Chris Streeter
+David Medina
David Ziegler
Eugene Mirotin
Fenn Bailey
Gert Van Gool
Harro van der Klauw
Jaap Roes
+James Roe
Jason Davies
Jeremy Dunck
+John-Scott Atlakson
+Jonathan Lukens
+Julien Phalip
Justin Lilly
+Luis Nell
Maciek Szczesniak
+Maor Ben-Dayan
Mathieu Pillard
Mehmet S. Catalbas
+Michael van de Waeter
+Mike Yumatov
+Nicolas Charlot
Petar Radosevic
Philipp Bosch
Philipp Wollermann
+Selwin Ong
Shabda Raaj
Thom Linton
Ulrich Petri
+Wilson Júnior
+wesleyb
View
@@ -2,4 +2,4 @@ include AUTHORS
include README.rst
include LICENSE
recursive-include compressor/templates/compressor *.html
-recursive-include compressor/tests/media *.js *.css *.png
+recursive-include tests *.js *.css *.png *.py
View
@@ -1,16 +1,2 @@
-VERSION = (0, 9, 2, "f", 0) # following PEP 386
-DEV_N = None
-
-
-def get_version():
- version = "%s.%s" % (VERSION[0], VERSION[1])
- if VERSION[2]:
- version = "%s.%s" % (version, VERSION[2])
- if VERSION[3] != "f":
- version = "%s%s%s" % (version, VERSION[3], VERSION[4])
- if DEV_N:
- version = "%s.dev%s" % (version, DEV_N)
- return version
-
-
-__version__ = get_version()
+# following PEP 386, versiontools will pick it up
+__version__ = (1, 0, 0, "final", 0)
View
@@ -1,19 +1,24 @@
+from __future__ import with_statement
import os
+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.encoding import smart_unicode
from compressor.cache import get_hexdigest, get_mtime
+
from compressor.conf import settings
from compressor.exceptions import CompressorError, UncompressableFileError
from compressor.filters import CompilerFilter
from compressor.storage import default_storage
+from compressor.signals import post_compress
from compressor.utils import get_class, staticfiles
-from compressor.utils.decorators import cached_property
+from compressor.utils.decorators import cached_property, memoize
# Some constants for nicer handling.
-SOURCE_HUNK, SOURCE_FILE = 1, 2
+SOURCE_HUNK, SOURCE_FILE = 'inline', 'file'
METHOD_INPUT, METHOD_OUTPUT = 'input', 'output'
@@ -24,12 +29,14 @@ class Compressor(object):
"""
type = None
- def __init__(self, content=None, output_prefix="compressed"):
+ def __init__(self, content=None, output_prefix=None, context=None, *args, **kwargs):
self.content = content or ""
- self.output_prefix = output_prefix
+ self.output_prefix = output_prefix or "compressed"
+ self.output_dir = settings.COMPRESS_OUTPUT_DIR.strip('/')
self.charset = settings.DEFAULT_CHARSET
self.storage = default_storage
self.split_content = []
+ self.context = context or {}
self.extra_context = {}
self.all_mimetypes = dict(settings.COMPRESS_PRECOMPILERS)
self.finders = staticfiles.finders
@@ -47,28 +54,45 @@ def get_basename(self, url):
except AttributeError:
base_url = settings.COMPRESS_URL
if not url.startswith(base_url):
- raise UncompressableFileError(
- "'%s' isn't accesible via COMPRESS_URL ('%s') and can't be"
- " compressed" % (url, base_url))
+ raise UncompressableFileError("'%s' isn't accesible via "
+ "COMPRESS_URL ('%s') and can't be "
+ "compressed" % (url, base_url))
basename = url.replace(base_url, "", 1)
# drop the querystring, which is used for non-compressed cache-busting.
return basename.split("?", 1)[0]
+ def get_filepath(self, content):
+ filename = "%s.%s" % (get_hexdigest(content, 12), self.type)
+ return os.path.join(self.output_dir, self.output_prefix, filename)
+
def get_filename(self, basename):
# first try to find it with staticfiles (in debug mode)
filename = None
- if settings.DEBUG and self.finders:
- filename = self.finders.find(basename)
- # secondly try finding the file in the root
- elif self.storage.exists(basename):
+ if self.storage.exists(basename):
filename = self.storage.path(basename)
+ # secondly try finding the file in the root
+ elif self.finders:
+ filename = self.finders.find(basename)
if filename:
return filename
# or just raise an exception as the last resort
raise UncompressableFileError(
- "'%s' could not be found in the COMPRESS_ROOT '%s'%s" % (
- basename, settings.COMPRESS_ROOT,
- self.finders and " or with staticfiles." or "."))
+ "'%s' could not be found in the COMPRESS_ROOT '%s'%s" %
+ (basename, settings.COMPRESS_ROOT,
+ self.finders and " or with staticfiles." or "."))
+
+ def get_filecontent(self, filename, charset):
+ with codecs.open(filename, 'rb', charset) as fd:
+ try:
+ return fd.read()
+ except IOError, e:
+ raise UncompressableFileError("IOError while processing "
+ "'%s': %s" % (filename, e))
+ except UnicodeDecodeError, e:
+ raise UncompressableFileError("UnicodeDecodeError while "
+ "processing '%s' with "
+ "charset %s: %s" %
+ (filename, charset, e))
@cached_property
def parser(self):
@@ -89,36 +113,70 @@ def cachekey(self):
return get_hexdigest(''.join(
[self.content] + self.mtimes).encode(self.charset), 12)
- @cached_property
- def hunks(self):
+ @memoize
+ def hunks(self, mode='file'):
+ """
+ The heart of content parsing, iterates of the
+ list of split contents and looks at its kind
+ to decide what to do with it. Should yield a
+ bunch of precompiled and/or rendered hunks.
+ """
+ enabled = settings.COMPRESS_ENABLED
+
for kind, value, basename, elem in self.split_contents():
- if kind == SOURCE_HUNK:
- content = self.filter(value, METHOD_INPUT,
- elem=elem, kind=kind, basename=basename)
- yield smart_unicode(content)
- elif kind == SOURCE_FILE:
- content = ""
- fd = open(value, 'rb')
- try:
- content = fd.read()
- except IOError, e:
- raise UncompressableFileError(
- "IOError while processing '%s': %s" % (value, e))
- finally:
- fd.close()
- content = self.filter(content, METHOD_INPUT,
- filename=value, basename=basename, elem=elem, kind=kind)
- attribs = self.parser.elem_attribs(elem)
- charset = attribs.get("charset", self.charset)
- yield smart_unicode(content, charset.lower())
+ precompiled = False
+ attribs = self.parser.elem_attribs(elem)
+ charset = attribs.get("charset", self.charset)
+ options = {
+ 'method': METHOD_INPUT,
+ 'elem': elem,
+ 'kind': kind,
+ 'basename': basename,
+ }
- @cached_property
- def concat(self):
- return '\n'.join((hunk.encode(self.charset) for hunk in self.hunks))
+ if kind == SOURCE_FILE:
+ options = dict(options, filename=value)
+ value = self.get_filecontent(value, charset)
+
+ if self.all_mimetypes:
+ precompiled, value = self.precompile(value, **options)
+
+ if enabled:
+ value = self.filter(value, **options)
+ yield mode, smart_unicode(value, charset.lower())
+ else:
+ if precompiled:
+ value = self.handle_output(kind, value, forced=True)
+ yield "verbatim", smart_unicode(value, charset.lower())
+ else:
+ yield mode, self.parser.elem_str(elem)
+
+ @memoize
+ def filtered_output(self, content):
+ """
+ Passes the concatenated content to the 'output' methods
+ of the compressor filters.
+ """
+ return self.filter(content, method=METHOD_OUTPUT)
+
+ @memoize
+ def filtered_input(self, mode='file'):
+ """
+ Passes each hunk (file or code) to the 'input' methods
+ of the compressor filters.
+ """
+ verbatim_content = []
+ rendered_content = []
+ for mode, hunk in self.hunks(mode):
+ if mode == 'verbatim':
+ verbatim_content.append(hunk)
+ else:
+ rendered_content.append(hunk)
+ return verbatim_content, rendered_content
def precompile(self, content, kind=None, elem=None, filename=None, **kwargs):
if not kind:
- return content
+ return False, content
attrs = self.parser.elem_attribs(elem)
mimetype = attrs.get("type", None)
if mimetype:
@@ -129,15 +187,11 @@ def precompile(self, content, kind=None, elem=None, filename=None, **kwargs):
"COMPRESS_PRECOMPILERS setting for "
"mimetype '%s'." % mimetype)
else:
- return CompilerFilter(content, filter_type=self.type,
- command=command, filename=filename).output(**kwargs)
- return content
+ return True, CompilerFilter(content, filter_type=self.type,
+ command=command, filename=filename).input(**kwargs)
+ return False, content
def filter(self, content, method, **kwargs):
- # run compiler
- if method == METHOD_INPUT:
- content = self.precompile(content, **kwargs)
-
for filter_cls in self.cached_filters:
filter_func = getattr(
filter_cls(content, filter_type=self.type), method)
@@ -148,34 +202,28 @@ def filter(self, content, method, **kwargs):
pass
return content
- @cached_property
- def combined(self):
- return self.filter(self.concat, method=METHOD_OUTPUT)
-
- def filepath(self, content):
- return os.path.join(settings.COMPRESS_OUTPUT_DIR.strip(os.sep),
- self.output_prefix, "%s.%s" % (get_hexdigest(content, 12), self.type))
-
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 settings.COMPRESS_PRECOMPILERS:
- # or concatting it, if pre-compilation is enabled
- content = self.concat
- else:
- # or just doing nothing, when neither
- # compression nor compilation is enabled
- return self.content
- # Shortcurcuit in case the content is empty.
- if not content:
+ verbatim_content, rendered_content = self.filtered_input(mode)
+ if not verbatim_content and not rendered_content:
return ''
+
+ if settings.COMPRESS_ENABLED or forced:
+ filtered_content = self.filtered_output(
+ '\n'.join((c.encode(self.charset) for c in rendered_content)))
+ finished_content = self.handle_output(mode, filtered_content, forced)
+ verbatim_content.append(finished_content)
+
+ if verbatim_content:
+ return '\n'.join(verbatim_content)
+
+ return self.content
+
+ def handle_output(self, mode, content, forced):
# Then check for the appropriate output method and call it
output_func = getattr(self, "output_%s" % mode, None)
if callable(output_func):
@@ -189,7 +237,7 @@ def output_file(self, mode, content, forced=False):
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)
+ new_filepath = self.get_filepath(content)
if not self.storage.exists(new_filepath) or forced:
self.storage.save(new_filepath, ContentFile(content))
url = self.storage.url(new_filepath)
@@ -209,6 +257,10 @@ def render_output(self, mode, context=None):
"""
if context is None:
context = {}
- context.update(self.extra_context)
- return render_to_string(
- "compressor/%s_%s.html" % (self.type, mode), context)
+ final_context = Context()
+ final_context.update(context)
+ final_context.update(self.context)
+ final_context.update(self.extra_context)
+ post_compress.send(sender='django-compressor', type=self.type, mode=mode, context=final_context)
+ return render_to_string("compressor/%s_%s.html" %
+ (self.type, mode), final_context)
Oops, something went wrong.

0 comments on commit bb3185b

Please sign in to comment.