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())