Refactored precompilation to use mimetypes only (no filename matching anymore) and new-style string formatting for the commands.
This commit is contained in:
@@ -26,7 +26,6 @@ class Compressor(object):
|
|||||||
self.content = content or ""
|
self.content = content or ""
|
||||||
self.output_prefix = output_prefix
|
self.output_prefix = output_prefix
|
||||||
self.charset = settings.DEFAULT_CHARSET
|
self.charset = settings.DEFAULT_CHARSET
|
||||||
self.precompilers = settings.COMPRESS_PRECOMPILERS
|
|
||||||
self.storage = default_storage
|
self.storage = default_storage
|
||||||
self.split_content = []
|
self.split_content = []
|
||||||
self.extra_context = {}
|
self.extra_context = {}
|
||||||
@@ -78,19 +77,18 @@ class Compressor(object):
|
|||||||
if kind == "hunk":
|
if kind == "hunk":
|
||||||
# Let's cast BeautifulSoup element to unicode here since
|
# Let's cast BeautifulSoup element to unicode here since
|
||||||
# it will try to encode using ascii internally later
|
# it will try to encode using ascii internally later
|
||||||
yield unicode(
|
yield unicode(self.filter(
|
||||||
self.filter(value, method="input", elem=elem, kind=kind))
|
value, method="input", elem=elem, kind=kind))
|
||||||
elif kind == "file":
|
elif kind == "file":
|
||||||
content = ""
|
content = ""
|
||||||
try:
|
|
||||||
fd = open(value, 'rb')
|
fd = open(value, 'rb')
|
||||||
try:
|
try:
|
||||||
content = fd.read()
|
content = fd.read()
|
||||||
finally:
|
|
||||||
fd.close()
|
|
||||||
except IOError, e:
|
except IOError, e:
|
||||||
raise UncompressableFileError(
|
raise UncompressableFileError(
|
||||||
"IOError while processing '%s': %s" % (value, e))
|
"IOError while processing '%s': %s" % (value, e))
|
||||||
|
finally:
|
||||||
|
fd.close()
|
||||||
content = self.filter(content,
|
content = self.filter(content,
|
||||||
method="input", filename=value, elem=elem, kind=kind)
|
method="input", filename=value, elem=elem, kind=kind)
|
||||||
attribs = self.parser.elem_attribs(elem)
|
attribs = self.parser.elem_attribs(elem)
|
||||||
@@ -101,41 +99,18 @@ class Compressor(object):
|
|||||||
def concat(self):
|
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=[]):
|
|
||||||
"""
|
|
||||||
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/<type>" 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):
|
def precompile(self, content, kind=None, elem=None, filename=None, **kwargs):
|
||||||
if not kind:
|
if not kind:
|
||||||
return content
|
return content
|
||||||
for options in self.compiler_options(kind, filename, elem):
|
attrs = self.parser.elem_attribs(elem)
|
||||||
command = options.get("command")
|
mimetype = attrs.get("type", None)
|
||||||
if command is None:
|
if mimetype is not None:
|
||||||
continue
|
for mimetypes, command in settings.COMPRESS_PRECOMPILERS:
|
||||||
content = CompilerFilter(content,
|
if not isinstance(mimetypes, (list, tuple)):
|
||||||
filter_type=self.type, command=command).output(**kwargs)
|
mimetypes = (mimetypes,)
|
||||||
|
if mimetype in mimetypes:
|
||||||
|
content = CompilerFilter(content, filter_type=self.type,
|
||||||
|
command=command).output(**kwargs)
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def filter(self, content, method, **kwargs):
|
def filter(self, content, method, **kwargs):
|
||||||
@@ -174,7 +149,7 @@ class Compressor(object):
|
|||||||
# including precompilation (or if it's forced)
|
# including precompilation (or if it's forced)
|
||||||
if settings.COMPRESS_ENABLED or forced:
|
if settings.COMPRESS_ENABLED or forced:
|
||||||
content = self.combined
|
content = self.combined
|
||||||
elif self.precompilers:
|
elif settings.COMPRESS_PRECOMPILERS:
|
||||||
# or concatting it, if pre-compilation is enabled
|
# or concatting it, if pre-compilation is enabled
|
||||||
content = self.concat
|
content = self.concat
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import tempfile
|
|||||||
|
|
||||||
from compressor.conf import settings
|
from compressor.conf import settings
|
||||||
from compressor.exceptions import FilterError
|
from compressor.exceptions import FilterError
|
||||||
from compressor.utils import cmd_split
|
from compressor.utils import cmd_split, FormattableString
|
||||||
|
|
||||||
logger = logging.getLogger("compressor.filters")
|
logger = logging.getLogger("compressor.filters")
|
||||||
|
|
||||||
@@ -38,26 +38,28 @@ class CompilerFilter(FilterBase):
|
|||||||
self.command = command
|
self.command = command
|
||||||
if self.command is None:
|
if self.command is None:
|
||||||
raise FilterError("Required command attribute not set")
|
raise FilterError("Required command attribute not set")
|
||||||
self.options = {}
|
|
||||||
self.stdout = subprocess.PIPE
|
self.stdout = subprocess.PIPE
|
||||||
self.stdin = subprocess.PIPE
|
self.stdin = subprocess.PIPE
|
||||||
self.stderr = subprocess.PIPE
|
self.stderr = subprocess.PIPE
|
||||||
|
|
||||||
def output(self, **kwargs):
|
def output(self, **kwargs):
|
||||||
infile = outfile = ""
|
infile = None
|
||||||
|
outfile = None
|
||||||
|
options = {}
|
||||||
try:
|
try:
|
||||||
if "%(infile)s" in self.command:
|
if "{infile}" in self.command:
|
||||||
infile = tempfile.NamedTemporaryFile(mode='w')
|
infile = tempfile.NamedTemporaryFile(mode='w')
|
||||||
infile.write(self.content)
|
infile.write(self.content)
|
||||||
infile.flush()
|
infile.flush()
|
||||||
self.options["infile"] = infile.name
|
options["infile"] = infile.name
|
||||||
if "%(outfile)s" in self.command:
|
if "{outfile}" in self.command:
|
||||||
ext = ".%s" % self.type and self.type or ""
|
ext = ".%s" % self.type and self.type or ""
|
||||||
outfile = tempfile.NamedTemporaryFile(mode='w', suffix=ext)
|
outfile = tempfile.NamedTemporaryFile(mode='w', suffix=ext)
|
||||||
self.options["outfile"] = outfile.name
|
options["outfile"] = outfile.name
|
||||||
proc = subprocess.Popen(cmd_split(self.command % self.options),
|
cmd = FormattableString(self.command).format(**options)
|
||||||
|
proc = subprocess.Popen(cmd_split(cmd),
|
||||||
stdout=self.stdout, stdin=self.stdin, stderr=self.stderr)
|
stdout=self.stdout, stdin=self.stdin, stderr=self.stderr)
|
||||||
if infile:
|
if infile is not None:
|
||||||
filtered, err = proc.communicate()
|
filtered, err = proc.communicate()
|
||||||
else:
|
else:
|
||||||
filtered, err = proc.communicate(self.content)
|
filtered, err = proc.communicate(self.content)
|
||||||
@@ -74,7 +76,7 @@ class CompilerFilter(FilterBase):
|
|||||||
raise FilterError(err)
|
raise FilterError(err)
|
||||||
if self.verbose:
|
if self.verbose:
|
||||||
self.logger.debug(err)
|
self.logger.debug(err)
|
||||||
if outfile:
|
if outfile is not None:
|
||||||
try:
|
try:
|
||||||
outfile_obj = open(outfile.name)
|
outfile_obj = open(outfile.name)
|
||||||
filtered = outfile_obj.read()
|
filtered = outfile_obj.read()
|
||||||
|
|||||||
@@ -22,20 +22,12 @@ class CompressorSettings(AppSettings):
|
|||||||
|
|
||||||
CSS_FILTERS = ['compressor.filters.css_default.CssAbsoluteFilter']
|
CSS_FILTERS = ['compressor.filters.css_default.CssAbsoluteFilter']
|
||||||
JS_FILTERS = ['compressor.filters.jsmin.JSMinFilter']
|
JS_FILTERS = ['compressor.filters.jsmin.JSMinFilter']
|
||||||
PRECOMPILERS = {
|
PRECOMPILERS = (
|
||||||
# "*.coffee": {
|
# ('text/coffeescript', 'coffee --compile --stdio'),
|
||||||
# "command": "coffee --compile --stdio",
|
# ('text/less', 'lessc {infile} {outfile}'),
|
||||||
# "mimetype": "text/coffeescript",
|
# ('text/x-sass', 'sass {infile} {outfile}'),
|
||||||
# },
|
# ('text/x-scss', 'sass --scss {infile} {outfile}'),
|
||||||
# "*.less": {
|
)
|
||||||
# "command": "lessc %(infile)s %(outfile)s",
|
|
||||||
# "mimetype": "text/less",
|
|
||||||
# },
|
|
||||||
# ("*.sass", "*.scss"): {
|
|
||||||
# "command": "sass %(infile)s %(outfile)s",
|
|
||||||
# "mimetype": "sass",
|
|
||||||
# },
|
|
||||||
}
|
|
||||||
CLOSURE_COMPILER_BINARY = 'java -jar compiler.jar'
|
CLOSURE_COMPILER_BINARY = 'java -jar compiler.jar'
|
||||||
CLOSURE_COMPILER_ARGUMENTS = ''
|
CLOSURE_COMPILER_ARGUMENTS = ''
|
||||||
CSSTIDY_BINARY = 'csstidy'
|
CSSTIDY_BINARY = 'csstidy'
|
||||||
@@ -124,15 +116,7 @@ class CompressorSettings(AppSettings):
|
|||||||
return value
|
return value
|
||||||
|
|
||||||
def configure_precompilers(self, value):
|
def configure_precompilers(self, value):
|
||||||
for patterns, options in value.items():
|
if not isinstance(value, (list, tuple)):
|
||||||
if options.get("command", None) is None:
|
raise ImproperlyConfigured("The COMPRESS_PRECOMPILERS setting "
|
||||||
raise ImproperlyConfigured("Please specify a command "
|
"must be a list or tuple. Check for missing commas.")
|
||||||
"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)
|
|
||||||
return value
|
return value
|
||||||
|
|||||||
@@ -101,10 +101,13 @@ class CompressorTestCase(TestCase):
|
|||||||
def test_js_return_if_off(self):
|
def test_js_return_if_off(self):
|
||||||
try:
|
try:
|
||||||
enabled = settings.COMPRESS_ENABLED
|
enabled = settings.COMPRESS_ENABLED
|
||||||
|
precompilers = settings.COMPRESS_PRECOMPILERS
|
||||||
settings.COMPRESS_ENABLED = False
|
settings.COMPRESS_ENABLED = False
|
||||||
|
settings.COMPRESS_PRECOMPILERS = {}
|
||||||
self.assertEqual(self.js, self.js_node.output())
|
self.assertEqual(self.js, self.js_node.output())
|
||||||
finally:
|
finally:
|
||||||
settings.COMPRESS_ENABLED = enabled
|
settings.COMPRESS_ENABLED = enabled
|
||||||
|
settings.COMPRESS_PRECOMPILERS = precompilers
|
||||||
|
|
||||||
def test_js_return_if_on(self):
|
def test_js_return_if_on(self):
|
||||||
output = u'<script type="text/javascript" src="/media/CACHE/js/3f33b9146e12.js" charset="utf-8"></script>'
|
output = u'<script type="text/javascript" src="/media/CACHE/js/3f33b9146e12.js" charset="utf-8"></script>'
|
||||||
|
|||||||
@@ -288,38 +288,30 @@ A list of filters that will be applied to javascript.
|
|||||||
COMPRESS_PRECOMPILERS
|
COMPRESS_PRECOMPILERS
|
||||||
^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
:Default: ``{}``
|
:Default: ``()``
|
||||||
|
|
||||||
A mapping of file pattern(s) to compiler options to be used on the
|
An iterable of two-tuples whose first item is the mimetype of the files or
|
||||||
matching files. The options dictionary requires two items:
|
hunks you want to compile with the command specified as the second item:
|
||||||
|
|
||||||
* 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.
|
|
||||||
|
|
||||||
* mimetype
|
* mimetype
|
||||||
The mimetype of the file in case inline code should be compiled
|
The mimetype of the file or inline code should that should be compiled.
|
||||||
(see below).
|
|
||||||
|
* 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::
|
Example::
|
||||||
|
|
||||||
COMPRESS_PRECOMPILERS = {
|
COMPRESS_PRECOMPILERS = (
|
||||||
"*.coffee": {
|
('text/coffeescript', 'coffee --compile --stdio'),
|
||||||
"command": "coffee --compile --stdio",
|
('text/less', 'lessc {infile} {outfile}'),
|
||||||
"mimetype": "text/coffeescript",
|
('text/x-sass', 'sass {infile} {outfile}'),
|
||||||
},
|
('text/x-scss', 'sass --scss {infile} {outfile}'),
|
||||||
"*.less": {
|
)
|
||||||
"command": "lessc %(infile)s %(outfile)s",
|
|
||||||
"mimetype": "text/less",
|
|
||||||
},
|
|
||||||
("*.sass", "*.scss"): {
|
|
||||||
"command": "sass %(infile)s %(outfile)s",
|
|
||||||
"mimetype": "sass",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
With that setting (and CoffeeScript_ installed), you could add the following
|
With that setting (and CoffeeScript_ installed), you could add the following
|
||||||
code to your templates:
|
code to your templates:
|
||||||
@@ -347,7 +339,7 @@ The same works for less_, too:
|
|||||||
{% load compress %}
|
{% load compress %}
|
||||||
|
|
||||||
{% compress css %}
|
{% compress css %}
|
||||||
<link rel="stylesheet" href="/static/css/styles.less" charset="utf-8">
|
<link type="text/less" rel="stylesheet" href="/static/css/styles.less" charset="utf-8">
|
||||||
<style type="text/less">
|
<style type="text/less">
|
||||||
@color: #4D926F;
|
@color: #4D926F;
|
||||||
|
|
||||||
@@ -524,12 +516,10 @@ This section lists features and settings that are deprecated or removed
|
|||||||
in newer versions of django_compressor.
|
in newer versions of django_compressor.
|
||||||
|
|
||||||
* ``COMPRESS_LESSC_BINARY``
|
* ``COMPRESS_LESSC_BINARY``
|
||||||
Superseded by the COMPRESS_PRECOMPILERS_ setting. Just add the following
|
Superseded by the COMPRESS_PRECOMPILERS_ setting. Just make sure to
|
||||||
to your settings::
|
use the correct mimetype when linking to less files or adding inline
|
||||||
|
code and add the following to your settings::
|
||||||
|
|
||||||
COMPRESS_PRECOMPILERS = {
|
COMPRESS_PRECOMPILERS = (
|
||||||
"*.less": {
|
('text/less', 'lessc {infile} {outfile}'),
|
||||||
"command": "lessc %(infile)s %(outfile)s",
|
)
|
||||||
"mimetype": "text/less",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user