From 931374a67da7980839a0dc2ef3f3b188c7569900 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Thu, 7 Apr 2011 22:36:24 +0200 Subject: [PATCH] Refactored precompilation to use mimetypes only (no filename matching anymore) and new-style string formatting for the commands. --- compressor/base.py | 57 ++++++++++-------------------------- compressor/filters/base.py | 22 +++++++------- compressor/settings.py | 34 ++++++--------------- compressor/tests/tests.py | 3 ++ docs/index.txt | 60 ++++++++++++++++---------------------- 5 files changed, 65 insertions(+), 111 deletions(-) diff --git a/compressor/base.py b/compressor/base.py index d3f9e9d..f17159a 100644 --- a/compressor/base.py +++ b/compressor/base.py @@ -26,7 +26,6 @@ class Compressor(object): self.content = content or "" 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 = {} @@ -78,19 +77,18 @@ class Compressor(object): if kind == "hunk": # Let's cast BeautifulSoup element to unicode here since # it will try to encode using ascii internally later - yield unicode( - self.filter(value, method="input", elem=elem, kind=kind)) + yield unicode(self.filter( + value, method="input", elem=elem, kind=kind)) elif kind == "file": content = "" + fd = open(value, 'rb') try: - fd = open(value, 'rb') - try: - content = fd.read() - finally: - fd.close() + 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, elem=elem, kind=kind) attribs = self.parser.elem_attribs(elem) @@ -101,41 +99,18 @@ class Compressor(object): def concat(self): return '\n'.join((hunk.encode(self.charset) for hunk in self.hunks)) - def matches_patterns(self, path, patterns=[]): - """ - Return True or False depending on whether the ``path`` matches the - list of give the given patterns. - """ - if not isinstance(patterns, (list, tuple)): - patterns = (patterns,) - for pattern in patterns: - if fnmatch.fnmatchcase(path, pattern): - return True - return False - - def compiler_options(self, kind, filename, elem): - if kind == "file" and filename: - for patterns, options in self.precompilers.items(): - if self.matches_patterns(filename, patterns): - yield options - elif kind == "hunk" and elem is not None: - # get the mimetype of the file and handle "text/" cases - attrs = self.parser.elem_attribs(elem) - mimetype = attrs.get("type", "").split("/")[-1] - for options in self.precompilers.values(): - if (mimetype and - mimetype == options.get("mimetype", "").split("/")[-1]): - yield options - def precompile(self, content, kind=None, elem=None, filename=None, **kwargs): if not kind: return content - for options in self.compiler_options(kind, filename, elem): - command = options.get("command") - if command is None: - continue - content = CompilerFilter(content, - filter_type=self.type, command=command).output(**kwargs) + attrs = self.parser.elem_attribs(elem) + mimetype = attrs.get("type", None) + if mimetype is not None: + for mimetypes, command in settings.COMPRESS_PRECOMPILERS: + if not isinstance(mimetypes, (list, tuple)): + mimetypes = (mimetypes,) + if mimetype in mimetypes: + content = CompilerFilter(content, filter_type=self.type, + command=command).output(**kwargs) return content def filter(self, content, method, **kwargs): @@ -174,7 +149,7 @@ class Compressor(object): # including precompilation (or if it's forced) if settings.COMPRESS_ENABLED or forced: content = self.combined - elif self.precompilers: + elif settings.COMPRESS_PRECOMPILERS: # or concatting it, if pre-compilation is enabled content = self.concat else: diff --git a/compressor/filters/base.py b/compressor/filters/base.py index f5df8f5..0123ead 100644 --- a/compressor/filters/base.py +++ b/compressor/filters/base.py @@ -5,7 +5,7 @@ import tempfile from compressor.conf import settings from compressor.exceptions import FilterError -from compressor.utils import cmd_split +from compressor.utils import cmd_split, FormattableString logger = logging.getLogger("compressor.filters") @@ -38,26 +38,28 @@ class CompilerFilter(FilterBase): self.command = command if self.command is None: raise FilterError("Required command attribute not set") - self.options = {} self.stdout = subprocess.PIPE self.stdin = subprocess.PIPE self.stderr = subprocess.PIPE def output(self, **kwargs): - infile = outfile = "" + infile = None + outfile = None + options = {} try: - if "%(infile)s" in self.command: + if "{infile}" in self.command: infile = tempfile.NamedTemporaryFile(mode='w') infile.write(self.content) infile.flush() - self.options["infile"] = infile.name - if "%(outfile)s" in self.command: + options["infile"] = infile.name + if "{outfile}" in self.command: ext = ".%s" % self.type and self.type or "" outfile = tempfile.NamedTemporaryFile(mode='w', suffix=ext) - self.options["outfile"] = outfile.name - proc = subprocess.Popen(cmd_split(self.command % self.options), + options["outfile"] = outfile.name + cmd = FormattableString(self.command).format(**options) + proc = subprocess.Popen(cmd_split(cmd), stdout=self.stdout, stdin=self.stdin, stderr=self.stderr) - if infile: + if infile is not None: filtered, err = proc.communicate() else: filtered, err = proc.communicate(self.content) @@ -74,7 +76,7 @@ class CompilerFilter(FilterBase): raise FilterError(err) if self.verbose: self.logger.debug(err) - if outfile: + if outfile is not None: try: outfile_obj = open(outfile.name) filtered = outfile_obj.read() diff --git a/compressor/settings.py b/compressor/settings.py index bfb9fd5..3fd0a79 100644 --- a/compressor/settings.py +++ b/compressor/settings.py @@ -22,20 +22,12 @@ class CompressorSettings(AppSettings): CSS_FILTERS = ['compressor.filters.css_default.CssAbsoluteFilter'] JS_FILTERS = ['compressor.filters.jsmin.JSMinFilter'] - PRECOMPILERS = { - # "*.coffee": { - # "command": "coffee --compile --stdio", - # "mimetype": "text/coffeescript", - # }, - # "*.less": { - # "command": "lessc %(infile)s %(outfile)s", - # "mimetype": "text/less", - # }, - # ("*.sass", "*.scss"): { - # "command": "sass %(infile)s %(outfile)s", - # "mimetype": "sass", - # }, - } + PRECOMPILERS = ( + # ('text/coffeescript', 'coffee --compile --stdio'), + # ('text/less', 'lessc {infile} {outfile}'), + # ('text/x-sass', 'sass {infile} {outfile}'), + # ('text/x-scss', 'sass --scss {infile} {outfile}'), + ) CLOSURE_COMPILER_BINARY = 'java -jar compiler.jar' CLOSURE_COMPILER_ARGUMENTS = '' CSSTIDY_BINARY = 'csstidy' @@ -124,15 +116,7 @@ class CompressorSettings(AppSettings): return value def configure_precompilers(self, value): - for patterns, options in value.items(): - if options.get("command", None) is None: - raise ImproperlyConfigured("Please specify a command " - "for compiler with the pattern %r." % patterns) - mimetype = options.get("mimetype", None) - if mimetype is None: - raise ImproperlyConfigured("Please specify a mimetype " - "for compiler with the pattern %r." % patterns) - if mimetype.startswith("text/"): - options["mimetype"] = mimetype[5:] - value[patterns].update(options) + if not isinstance(value, (list, tuple)): + raise ImproperlyConfigured("The COMPRESS_PRECOMPILERS setting " + "must be a list or tuple. Check for missing commas.") return value diff --git a/compressor/tests/tests.py b/compressor/tests/tests.py index 7b37a3c..49d21a9 100644 --- a/compressor/tests/tests.py +++ b/compressor/tests/tests.py @@ -101,10 +101,13 @@ class CompressorTestCase(TestCase): def test_js_return_if_off(self): try: enabled = settings.COMPRESS_ENABLED + precompilers = settings.COMPRESS_PRECOMPILERS settings.COMPRESS_ENABLED = False + settings.COMPRESS_PRECOMPILERS = {} self.assertEqual(self.js, self.js_node.output()) finally: settings.COMPRESS_ENABLED = enabled + settings.COMPRESS_PRECOMPILERS = precompilers def test_js_return_if_on(self): output = u'' diff --git a/docs/index.txt b/docs/index.txt index 9085d49..db38e7d 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -288,38 +288,30 @@ A list of filters that will be applied to javascript. COMPRESS_PRECOMPILERS ^^^^^^^^^^^^^^^^^^^^^ -:Default: ``{}`` +:Default: ``()`` -A mapping of file pattern(s) to compiler options to be used on the -matching files. The options dictionary requires two items: - -* command - The command to call on each of the files. Standard Python string - formatting will be provided for the two variables ``%(infile)s`` and - ``%(outfile)s`` and will also trigger the actual creation of those - temporary files. If not given in the command string, django_compressor - will use ``stdin`` and ``stdout`` respectively instead. +An iterable of two-tuples whose first item is the mimetype of the files or +hunks you want to compile with the command specified as the second item: * mimetype - The mimetype of the file in case inline code should be compiled - (see below). + The mimetype of the file or inline code should that should be compiled. + +* command + The command to call on each of the files. Modern Python string + formatting will be provided for the two placeholders ``{infile}`` and + ``{outfile}`` whose existence in the command string also triggers the + actual creation of those temporary files. If not given in the command + string, django_compressor will use ``stdin`` and ``stdout`` respectively + instead. Example:: - COMPRESS_PRECOMPILERS = { - "*.coffee": { - "command": "coffee --compile --stdio", - "mimetype": "text/coffeescript", - }, - "*.less": { - "command": "lessc %(infile)s %(outfile)s", - "mimetype": "text/less", - }, - ("*.sass", "*.scss"): { - "command": "sass %(infile)s %(outfile)s", - "mimetype": "sass", - }, - } + COMPRESS_PRECOMPILERS = ( + ('text/coffeescript', 'coffee --compile --stdio'), + ('text/less', 'lessc {infile} {outfile}'), + ('text/x-sass', 'sass {infile} {outfile}'), + ('text/x-scss', 'sass --scss {infile} {outfile}'), + ) With that setting (and CoffeeScript_ installed), you could add the following code to your templates: @@ -347,7 +339,7 @@ The same works for less_, too: {% load compress %} {% compress css %} - +