diff --git a/.gitignore b/.gitignore
index 4efafd4..b01b117 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,5 @@ MANIFEST
*.egg
docs/_build/
.coverage
-htmlcov
\ No newline at end of file
+htmlcov
+.sass-cache
\ No newline at end of file
diff --git a/compressor/base.py b/compressor/base.py
index 5562bba..d64fac3 100644
--- a/compressor/base.py
+++ b/compressor/base.py
@@ -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):
diff --git a/compressor/conf.py b/compressor/conf.py
index 2c28488..61d0006 100644
--- a/compressor/conf.py
+++ b/compressor/conf.py
@@ -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.
diff --git a/compressor/filters/base.py b/compressor/filters/base.py
index 1d5b634..26db734 100644
--- a/compressor/filters/base.py
+++ b/compressor/filters/base.py
@@ -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
diff --git a/compressor/filters/closure.py b/compressor/filters/closure.py
index e927d0d..d229bcb 100644
--- a/compressor/filters/closure.py
+++ b/compressor/filters/closure.py
@@ -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),
+ )
diff --git a/compressor/filters/compass.py b/compressor/filters/compass.py
new file mode 100644
index 0000000..ed7627d
--- /dev/null
+++ b/compressor/filters/compass.py
@@ -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)
diff --git a/compressor/filters/csstidy.py b/compressor/filters/csstidy.py
index a8428cc..4b7e4c7 100644
--- a/compressor/filters/csstidy.py
+++ b/compressor/filters/csstidy.py
@@ -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),
+ )
diff --git a/compressor/filters/yui.py b/compressor/filters/yui.py
index 9b9c82d..60fd1f7 100644
--- a/compressor/filters/yui.py
+++ b/compressor/filters/yui.py
@@ -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),
+ )
diff --git a/compressor/templatetags/compress.py b/compressor/templatetags/compress.py
index b51adb4..6ae3877 100644
--- a/compressor/templatetags/compress.py
+++ b/compressor/templatetags/compress.py
@@ -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:
diff --git a/compressor/tests/media/config.rb b/compressor/tests/media/config.rb
new file mode 100644
index 0000000..148088b
--- /dev/null
+++ b/compressor/tests/media/config.rb
@@ -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
diff --git a/compressor/tests/media/sass/ie.scss b/compressor/tests/media/sass/ie.scss
new file mode 100644
index 0000000..5cd5b6c
--- /dev/null
+++ b/compressor/tests/media/sass/ie.scss
@@ -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:
+ * */
diff --git a/compressor/tests/media/sass/print.scss b/compressor/tests/media/sass/print.scss
new file mode 100644
index 0000000..b0e9e45
--- /dev/null
+++ b/compressor/tests/media/sass/print.scss
@@ -0,0 +1,3 @@
+/* Welcome to Compass. Use this file to define print styles.
+ * Import this file using the following HTML or equivalent:
+ * */
diff --git a/compressor/tests/media/sass/screen.scss b/compressor/tests/media/sass/screen.scss
new file mode 100644
index 0000000..81de847
--- /dev/null
+++ b/compressor/tests/media/sass/screen.scss
@@ -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:
+ * */
+
+@import "compass/reset";
diff --git a/compressor/tests/media/stylesheets/ie.css b/compressor/tests/media/stylesheets/ie.css
new file mode 100644
index 0000000..5cd5b6c
--- /dev/null
+++ b/compressor/tests/media/stylesheets/ie.css
@@ -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:
+ * */
diff --git a/compressor/tests/media/stylesheets/print.css b/compressor/tests/media/stylesheets/print.css
new file mode 100644
index 0000000..b0e9e45
--- /dev/null
+++ b/compressor/tests/media/stylesheets/print.css
@@ -0,0 +1,3 @@
+/* Welcome to Compass. Use this file to define print styles.
+ * Import this file using the following HTML or equivalent:
+ * */
diff --git a/compressor/tests/media/stylesheets/screen.css b/compressor/tests/media/stylesheets/screen.css
new file mode 100644
index 0000000..669a832
--- /dev/null
+++ b/compressor/tests/media/stylesheets/screen.css
@@ -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:
+ * */
+/* 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;
+}
diff --git a/compressor/tests/precompiler.py b/compressor/tests/precompiler.py
index c15b103..c2d422f 100644
--- a/compressor/tests/precompiler.py
+++ b/compressor/tests/precompiler.py
@@ -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
diff --git a/compressor/tests/tests.py b/compressor/tests/tests.py
index 54a412e..0e3cc29 100644
--- a/compressor/tests/tests.py
+++ b/compressor/tests/tests.py
@@ -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 %}
+
+
+ {% endcompress %}
+ """
+ context = {'MEDIA_URL': settings.COMPRESS_URL}
+ out = u''
+ 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())