Added CompassFilter.

This commit is contained in:
Jannis Leidel
2011-05-25 17:10:33 +02:00
parent be0922a9f2
commit d0411eba02
18 changed files with 284 additions and 70 deletions

3
.gitignore vendored
View File

@@ -10,4 +10,5 @@ MANIFEST
*.egg
docs/_build/
.coverage
htmlcov
htmlcov
.sass-cache

View File

@@ -130,7 +130,7 @@ class Compressor(object):
"mimetype '%s'." % mimetype)
else:
return CompilerFilter(content, filter_type=self.type,
command=command, filename=filename).output(**kwargs)
command=command, filename=filename).input(**kwargs)
return content
def filter(self, content, method, **kwargs):

View File

@@ -40,6 +40,10 @@ class CompressorSettings(AppSettings):
YUI_JS_ARGUMENTS = ''
DATA_URI_MIN_SIZE = 1024
COMPASS_BINARY = 'compass'
COMPASS_ARGUMENTS = ' --no-line-comments --output-style expanded'
COMPASS_PLUGINS = []
# the cache backend to use
CACHE_BACKEND = None
# rebuilds the cache every 30 days if nothing has changed.

View File

@@ -1,21 +1,25 @@
import os
import logging
import subprocess
import tempfile
from django.utils.datastructures import SortedDict
from compressor.conf import settings
from compressor.exceptions import FilterError
from compressor.utils import cmd_split, stringformat
from compressor.utils import cmd_split
from compressor.utils.stringformat import FormattableString as fstr
logger = logging.getLogger("compressor.filters")
class FilterBase(object):
def __init__(self, content, filter_type=None, verbose=0):
def __init__(self, content, filter_type=None, filename=None, verbose=0):
self.type = filter_type
self.content = content
self.verbose = verbose or settings.COMPRESS_VERBOSE
self.logger = logger
self.filename = filename
def input(self, **kwargs):
raise NotImplementedError
@@ -30,40 +34,53 @@ class CompilerFilter(FilterBase):
external commands.
"""
command = None
filename = None
options = {}
options = ()
def __init__(self, content, command=None, filename=None, *args, **kwargs):
def __init__(self, content, command=None, *args, **kwargs):
super(CompilerFilter, self).__init__(content, *args, **kwargs)
self.cwd = None
if command:
self.command = command
if self.command is None:
raise FilterError("Required attribute 'command' not given")
self.filename = filename
if isinstance(self.options, dict):
new_options = ()
for item in kwargs.iteritems():
new_options += (item,)
self.options = new_options
for item in kwargs.iteritems():
self.options += (item,)
self.stdout = subprocess.PIPE
self.stdin = subprocess.PIPE
self.stderr = subprocess.PIPE
self.infile, self.outfile = None, None
def output(self, **kwargs):
infile = None
outfile = None
try:
def input(self, **kwargs):
options = dict(self.options)
if self.infile is None:
if "{infile}" in self.command:
infile = tempfile.NamedTemporaryFile(mode='w')
infile.write(self.content)
infile.flush()
self.options["infile"] = self.filename or infile.name
if "{outfile}" in self.command:
ext = ".%s" % self.type and self.type or ""
outfile = tempfile.NamedTemporaryFile(mode='rw', suffix=ext)
self.options["outfile"] = outfile.name
command = stringformat.FormattableString(self.command)
proc = subprocess.Popen(cmd_split(command.format(**self.options)),
stdout=self.stdout, stdin=self.stdin, stderr=self.stderr)
if infile is not None:
filtered, err = proc.communicate()
else:
if self.filename is None:
self.infile = tempfile.NamedTemporaryFile(mode="w")
self.infile.write(self.content)
self.infile.flush()
os.fsync(self.infile)
options["infile"] = self.infile.name
else:
self.infile = open(self.filename)
options["infile"] = self.filename
if "{outfile}" in self.command and not "outfile" in options:
ext = ".%s" % self.type and self.type or ""
self.outfile = tempfile.NamedTemporaryFile(mode='r+', suffix=ext)
options["outfile"] = self.outfile.name
try:
command = fstr(self.command).format(**options)
proc = subprocess.Popen(cmd_split(command), shell=os.name=='nt',
stdout=self.stdout, stdin=self.stdin, stderr=self.stderr, cwd=self.cwd)
if self.infile is None:
filtered, err = proc.communicate(self.content)
else:
filtered, err = proc.communicate()
except (IOError, OSError), e:
raise FilterError('Unable to apply %s (%r): %s' %
(self.__class__.__name__, self.command, e))
@@ -75,11 +92,13 @@ class CompilerFilter(FilterBase):
raise FilterError(err)
if self.verbose:
self.logger.debug(err)
if outfile is not None:
filtered = outfile.read()
outfile_path = options.get('outfile')
if outfile_path:
self.outfile = open(outfile_path, 'r')
finally:
if infile is not None:
infile.close()
if outfile is not None:
outfile.close()
if self.infile is not None:
self.infile.close()
if self.outfile is not None:
filtered = self.outfile.read()
self.outfile.close()
return filtered

View File

@@ -4,7 +4,7 @@ from compressor.filters import CompilerFilter
class ClosureCompilerFilter(CompilerFilter):
command = "{binary} {args}"
options = {
"binary": settings.COMPRESS_CLOSURE_COMPILER_BINARY,
"args": settings.COMPRESS_CLOSURE_COMPILER_ARGUMENTS,
}
options = (
("binary", settings.COMPRESS_CLOSURE_COMPILER_BINARY),
("args", settings.COMPRESS_CLOSURE_COMPILER_ARGUMENTS),
)

View File

@@ -0,0 +1,37 @@
import tempfile
from os import path
from compressor.conf import settings
from compressor.filters import CompilerFilter
class CompassFilter(CompilerFilter):
"""
Converts Compass files to css.
"""
command = "{binary} compile --force --quiet --boring {args} "
options = (
("binary", settings.COMPRESS_COMPASS_BINARY),
("args", settings.COMPRESS_COMPASS_ARGUMENTS),
)
def input(self, *args, **kwargs):
if self.filename is None:
self.filename = kwargs.pop('filename')
tmpdir = tempfile.mkdtemp()
parentdir = path.abspath(path.dirname(self.filename))
self.cwd = path.dirname(parentdir)
self.infile = open(self.filename)
outfile_name = path.splitext(path.split(self.filename)[1])[0] + '.css'
self.options += (
('infile', self.filename),
('tmpdir', tmpdir),
('sassdir', parentdir),
('outfile', path.join(tmpdir, outfile_name)),
('imagedir', settings.COMPRESS_URL),
)
for plugin in settings.COMPRESS_COMPASS_PLUGINS:
self.command += ' --require %s'% plugin
self.command += (' --sass-dir {sassdir} --css-dir {tmpdir}'
' --image-dir {imagedir} {infile}')
return super(CompassFilter, self).input(*args, **kwargs)

View File

@@ -4,7 +4,7 @@ from compressor.filters import CompilerFilter
class CSSTidyFilter(CompilerFilter):
command = "{binary} {infile} {args} {outfile}"
options = {
"binary": settings.COMPRESS_CSSTIDY_BINARY,
"args": settings.COMPRESS_CSSTIDY_ARGUMENTS,
}
options = (
("binary", settings.COMPRESS_CSSTIDY_BINARY),
("args", settings.COMPRESS_CSSTIDY_ARGUMENTS),
)

View File

@@ -14,15 +14,15 @@ class YUICompressorFilter(CompilerFilter):
class YUICSSFilter(YUICompressorFilter):
type = 'css'
options = {
"binary": settings.COMPRESS_YUI_BINARY,
"args": settings.COMPRESS_YUI_CSS_ARGUMENTS,
}
options = (
("binary", settings.COMPRESS_YUI_BINARY),
("args", settings.COMPRESS_YUI_CSS_ARGUMENTS),
)
class YUIJSFilter(YUICompressorFilter):
type = 'js'
options = {
"binary": settings.COMPRESS_YUI_BINARY,
"args": settings.COMPRESS_YUI_JS_ARGUMENTS,
}
options = (
("binary", settings.COMPRESS_YUI_BINARY),
("args", settings.COMPRESS_YUI_JS_ARGUMENTS),
)

View File

@@ -71,6 +71,7 @@ class CompressorNode(template.Node):
return cache_content
# 4. call compressor output method and handle exceptions
rendered_output = compressor.output(self.mode, forced=forced)
try:
rendered_output = compressor.output(self.mode, forced=forced)
if cache_key:

View File

@@ -0,0 +1,24 @@
# Require any additional compass plugins here.
# Set this to the root of your project when deployed:
http_path = "/"
css_dir = "stylesheets"
sass_dir = "sass"
images_dir = "images"
javascripts_dir = "javascripts"
# You can select your preferred output style here (can be overridden via the command line):
# output_style = :expanded or :nested or :compact or :compressed
# To enable relative paths to assets via compass helper functions. Uncomment:
# relative_assets = true
# To disable debugging comments that display the original location of your selectors. Uncomment:
# line_comments = false
# If you prefer the indented syntax, you might want to regenerate this
# project again passing --syntax sass, or you can uncomment this:
# preferred_syntax = :sass
# and then run:
# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass

View File

@@ -0,0 +1,5 @@
/* Welcome to Compass. Use this file to write IE specific override styles.
* Import this file using the following HTML or equivalent:
* <!--[if IE]>
* <link href="/stylesheets/ie.css" media="screen, projection" rel="stylesheet" type="text/css" />
* <![endif]--> */

View File

@@ -0,0 +1,3 @@
/* Welcome to Compass. Use this file to define print styles.
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/print.css" media="print" rel="stylesheet" type="text/css" /> */

View File

@@ -0,0 +1,6 @@
/* Welcome to Compass.
* In this file you should write your main styles. (or centralize your imports)
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */
@import "compass/reset";

View File

@@ -0,0 +1,5 @@
/* Welcome to Compass. Use this file to write IE specific override styles.
* Import this file using the following HTML or equivalent:
* <!--[if IE]>
* <link href="/stylesheets/ie.css" media="screen, projection" rel="stylesheet" type="text/css" />
* <![endif]--> */

View File

@@ -0,0 +1,3 @@
/* Welcome to Compass. Use this file to define print styles.
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/print.css" media="print" rel="stylesheet" type="text/css" /> */

View File

@@ -0,0 +1,69 @@
/* Welcome to Compass.
* In this file you should write your main styles. (or centralize your imports)
* Import this file using the following HTML or equivalent:
* <link href="/stylesheets/screen.css" media="screen, projection" rel="stylesheet" type="text/css" /> */
/* line 17, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* line 20, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
body {
line-height: 1;
}
/* line 22, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
ol, ul {
list-style: none;
}
/* line 24, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
table {
border-collapse: collapse;
border-spacing: 0;
}
/* line 26, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
caption, th, td {
text-align: left;
font-weight: normal;
vertical-align: middle;
}
/* line 28, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
q, blockquote {
quotes: none;
}
/* line 101, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
q:before, q:after, blockquote:before, blockquote:after {
content: "";
content: none;
}
/* line 30, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
a img {
border: none;
}
/* line 115, ../../../../../usr/local/Cellar/gems/1.8/gems/compass-0.11.1/frameworks/compass/stylesheets/compass/reset/_utilities.scss */
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
display: block;
}

View File

@@ -23,9 +23,8 @@ def main():
content = content.replace('background:', 'color:')
if options.outfile:
f = open(options.outfile, 'w')
f.write(content)
f.close()
with open(options.outfile, 'w') as f:
f.write(content)
else:
print content

View File

@@ -483,43 +483,81 @@ color: black;
"""
from compressor.filters.csstidy import CSSTidyFilter
self.assertEqual(
"font,th,td,p{color:#000;}", CSSTidyFilter(content).output())
"font,th,td,p{color:#000;}", CSSTidyFilter(content).input())
CssTidyTestCase = skipIf(
find_command(settings.COMPRESS_CSSTIDY_BINARY) is None,
'CSStidy binary %r not found' % settings.COMPRESS_CSSTIDY_BINARY
)(CssTidyTestCase)
class CompassTestCase(TestCase):
def setUp(self):
self.old_debug = settings.DEBUG
self.old_compress_css_filters = settings.COMPRESS_CSS_FILTERS
self.old_compress_url = settings.COMPRESS_URL
self.old_enabled = settings.COMPRESS_ENABLED
settings.DEBUG = True
settings.COMPRESS_ENABLED = True
settings.COMPRESS_CSS_FILTERS = [
'compressor.filters.compass.CompassFilter',
'compressor.filters.css_default.CssAbsoluteFilter',
]
settings.COMPRESS_URL = '/media/'
def tearDown(self):
settings.DEBUG = self.old_debug
settings.COMPRESS_URL = self.old_compress_url
settings.COMPRESS_ENABLED = self.old_enabled
settings.COMPRESS_CSS_FILTERS = self.old_compress_css_filters
def test_compass(self):
template = u"""{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ MEDIA_URL }}sass/screen.scss" type="text/css" charset="utf-8">
<link rel="stylesheet" href="{{ MEDIA_URL }}sass/print.scss" type="text/css" charset="utf-8">
{% endcompress %}
"""
context = {'MEDIA_URL': settings.COMPRESS_URL}
out = u'<link rel="stylesheet" href="/media/CACHE/css/3f807af2259c.css" type="text/css">'
self.assertEqual(out, render(template, context))
CompassTestCase = skipIf(
find_command(settings.COMPRESS_COMPASS_BINARY) is None,
'Compass binary %r not found' % settings.COMPRESS_COMPASS_BINARY
)(CompassTestCase)
class PrecompilerTestCase(TestCase):
def setUp(self):
self.this_dir = os.path.dirname(__file__)
self.filename = os.path.join(self.this_dir, 'media/css/one.css')
self.test_precompiler = os.path.join(self.this_dir, 'precompiler.py')
with open(self.filename) as f:
self.content = f.read()
self.test_precompiler = os.path.join(self.this_dir, 'precompiler.py')
def test_precompiler_infile_outfile(self):
command = '%s %s -f {infile} -o {outfile}' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
self.assertEqual(u"body { color:#990; }", compiler.output())
def test_precompiler_stdin_outfile(self):
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.output())
def test_precompiler_stdin_stdout(self):
command = '%s %s' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=None, command=command)
self.assertEqual(u"body { color:#990; }\n", compiler.output())
def test_precompiler_stdin_stdout_filename(self):
command = '%s %s' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
self.assertEqual(u"body { color:#990; }\n", compiler.output())
self.assertEqual(u"body { color:#990; }", compiler.input())
def test_precompiler_infile_stdout(self):
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; }\n", compiler.output())
self.assertEqual(u"body { color:#990; }\n", compiler.input())
def test_precompiler_stdin_outfile(self):
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)
compiler = CompilerFilter(content=self.content, filename=None, command=command)
self.assertEqual(u"body { color:#990; }\n", compiler.input())
def test_precompiler_stdin_stdout_filename(self):
command = '%s %s' % (sys.executable, self.test_precompiler)
compiler = CompilerFilter(content=self.content, filename=self.filename, command=command)
self.assertEqual(u"body { color:#990; }\n", compiler.input())