From 202d65768921379d519c4b133a10040a8dea837f Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 30 Jan 2015 13:24:53 -0800 Subject: [PATCH 1/2] Upgrade to libsass==3.1.0. Resolves #36. Resolves #38. --- libsass | 2 +- pysass.cpp | 122 +++++++++++++++++++++------------------------------ sass.py | 46 +++++++++++++++---- sasstests.py | 73 +++++++++++++++++++++++++++--- setup.py | 4 +- tox.ini | 2 +- 6 files changed, 161 insertions(+), 88 deletions(-) diff --git a/libsass b/libsass index 030e267e..31521ef3 160000 --- a/libsass +++ b/libsass @@ -1 +1 @@ -Subproject commit 030e267efc50bf829bbb6fda025f5ca265248742 +Subproject commit 31521ef3ece636892f395a80392448ceae449b90 diff --git a/pysass.cpp b/pysass.cpp index 30f0e6e2..480f6a99 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -2,7 +2,7 @@ #include #include #include -#include "sass_interface.h" +#include "sass_context.h" #if PY_MAJOR_VERSION >= 3 #define PySass_IF_PY3(three, two) (three) @@ -37,9 +37,13 @@ static struct PySass_Pair PySass_output_style_enum[] = { static PyObject * PySass_compile_string(PyObject *self, PyObject *args) { - struct sass_context *context; + struct Sass_Context *ctx; + struct Sass_Data_Context *context; + struct Sass_Options *options; char *string, *include_paths, *image_path; - int output_style, source_comments, precision; + const char *error_message, *output_string; + Sass_Output_Style output_style; + int source_comments, error_status, precision; PyObject *result; if (!PyArg_ParseTuple(args, @@ -49,30 +53,38 @@ PySass_compile_string(PyObject *self, PyObject *args) { return NULL; } - context = sass_new_context(); - context->source_string = string; - context->options.output_style = output_style; - context->options.source_comments = source_comments; - context->options.include_paths = include_paths; - context->options.image_path = image_path; - context->options.precision = precision; + context = sass_make_data_context(string); + options = sass_data_context_get_options(context); + sass_option_set_output_style(options, output_style); + sass_option_set_source_comments(options, source_comments); + sass_option_set_include_path(options, include_paths); + sass_option_set_image_path(options, image_path); + sass_option_set_precision(options, precision); - sass_compile(context); + sass_compile_data_context(context); + ctx = sass_data_context_get_context(context); + error_status = sass_context_get_error_status(ctx); + error_message = sass_context_get_error_message(ctx); + output_string = sass_context_get_output_string(ctx); result = Py_BuildValue( PySass_IF_PY3("hy", "hs"), - (short int) !context->error_status, - context->error_status ? context->error_message : context->output_string + (short int) !error_status, + error_status ? error_message : output_string ); - sass_free_context(context); + sass_delete_data_context(context); return result; } static PyObject * PySass_compile_filename(PyObject *self, PyObject *args) { - struct sass_file_context *context; + struct Sass_Context *ctx; + struct Sass_File_Context *context; + struct Sass_Options *options; char *filename, *include_paths, *image_path; - int output_style, source_comments, error_status, precision; + const char *error_message, *output_string, *source_map_string; + Sass_Output_Style output_style; + int source_comments, error_status, precision; PyObject *source_map_filename, *result; if (!PyArg_ParseTuple(args, @@ -82,73 +94,41 @@ PySass_compile_filename(PyObject *self, PyObject *args) { return NULL; } - context = sass_new_file_context(); - context->input_path = filename; + context = sass_make_file_context(filename); + options = sass_file_context_get_options(context); + if (source_comments && PySass_Bytes_Check(source_map_filename)) { size_t source_map_file_len = PySass_Bytes_GET_SIZE(source_map_filename); if (source_map_file_len) { char *source_map_file = (char *) malloc(source_map_file_len + 1); strncpy( - source_map_file, + source_map_file, PySass_Bytes_AS_STRING(source_map_filename), source_map_file_len + 1 ); - context->options.source_map_file = source_map_file; + sass_option_set_source_map_file(options, source_map_file); } } - context->options.output_style = output_style; - context->options.source_comments = source_comments; - context->options.include_paths = include_paths; - context->options.image_path = image_path; - context->options.precision = precision; - - sass_compile_file(context); - - error_status = context->error_status; + sass_option_set_output_style(options, output_style); + sass_option_set_source_comments(options, source_comments); + sass_option_set_include_path(options, include_paths); + sass_option_set_image_path(options, image_path); + sass_option_set_precision(options, precision); + + sass_compile_file_context(context); + + ctx = sass_file_context_get_context(context); + error_status = sass_context_get_error_status(ctx); + error_message = sass_context_get_error_message(ctx); + output_string = sass_context_get_output_string(ctx); + source_map_string = sass_context_get_source_map_string(ctx); result = Py_BuildValue( PySass_IF_PY3("hyy", "hss"), - (short int) !context->error_status, - error_status ? context->error_message : context->output_string, - error_status || context->source_map_string == NULL - ? "" - : context->source_map_string - ); - sass_free_file_context(context); - return result; -} - -static PyObject * -PySass_compile_dirname(PyObject *self, PyObject *args) { - struct sass_folder_context *context; - char *search_path, *output_path, *include_paths, *image_path; - int output_style, source_comments, precision; - PyObject *result; - - if (!PyArg_ParseTuple(args, - PySass_IF_PY3("yyiiyyi", "ssiissi"), - &search_path, &output_path, - &output_style, &source_comments, - &include_paths, &image_path, precision)) { - return NULL; - } - - context = sass_new_folder_context(); - context->search_path = search_path; - context->output_path = output_path; - context->options.output_style = output_style; - context->options.source_comments = source_comments; - context->options.include_paths = include_paths; - context->options.image_path = image_path; - context->options.precision = precision; - - sass_compile_folder(context); - - result = Py_BuildValue( - PySass_IF_PY3("hy", "hs"), - (short int) !context->error_status, - context->error_status ? context->error_message : NULL + (short int) !error_status, + error_status ? error_message : output_string, + error_status || source_map_string == NULL ? "" : source_map_string ); - sass_free_folder_context(context); + sass_delete_file_context(context); return result; } @@ -157,8 +137,6 @@ static PyMethodDef PySass_methods[] = { "Compile a SASS string."}, {"compile_filename", PySass_compile_filename, METH_VARARGS, "Compile a SASS file."}, - {"compile_dirname", PySass_compile_dirname, METH_VARARGS, - "Compile several SASS files."}, {NULL, NULL, 0, NULL} }; diff --git a/sass.py b/sass.py index 0b60784e..3676a240 100644 --- a/sass.py +++ b/sass.py @@ -19,8 +19,7 @@ from six import string_types, text_type -from _sass import (OUTPUT_STYLES, compile_dirname, - compile_filename, compile_string) +from _sass import OUTPUT_STYLES, compile_filename, compile_string __all__ = ('MODES', 'OUTPUT_STYLES', 'SOURCE_COMMENTS', 'CompileError', 'and_join', 'compile') @@ -46,10 +45,46 @@ class CompileError(ValueError): """The exception type that is raised by :func:`compile()`. It is a subtype of :exc:`exceptions.ValueError`. - """ +def mkdirp(path): + try: + os.makedirs(path) + except OSError: + if os.path.isdir(path): + return + raise + + +def compile_dirname( + search_path, output_path, output_style, source_comments, include_paths, + image_path, precision, +): + fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() + for dirpath, _, filenames in os.walk(search_path): + filenames = [ + filename for filename in filenames if filename.endswith('.scss') + ] + for filename in filenames: + input_filename = os.path.join(dirpath, filename) + relpath_to_file = os.path.relpath(input_filename, search_path) + output_filename = os.path.join(output_path, relpath_to_file) + output_filename = re.sub('.scss$', '.css', output_filename) + input_filename = input_filename.encode(fs_encoding) + s, v, _ = compile_filename( + input_filename, output_style, source_comments, include_paths, + image_path, precision, None, + ) + if s: + v = v.decode('UTF-8') + mkdirp(os.path.dirname(output_filename)) + with open(output_filename, 'w') as output_file: + output_file.write(v) + else: + return False, v + return True, None + def compile(**kwargs): """There are three modes of parameters :func:`compile()` can take: ``string``, ``filename``, and ``dirname``. @@ -302,11 +337,6 @@ def compile(**kwargs): except ValueError: raise ValueError('dirname must be a pair of (source_dir, ' 'output_dir)') - else: - if isinstance(search_path, text_type): - search_path = search_path.encode(fs_encoding) - if isinstance(output_path, text_type): - output_path = output_path.encode(fs_encoding) s, v = compile_dirname(search_path, output_path, output_style, source_comments, include_paths, image_path, precision) diff --git a/sasstests.py b/sasstests.py index b3f11aff..e78432d3 100644 --- a/sasstests.py +++ b/sasstests.py @@ -2,6 +2,7 @@ from __future__ import with_statement import collections +import contextlib import glob import json import os @@ -56,7 +57,7 @@ def normalize_path(path): 'sources': ['test/a.scss'], 'sourcesContent': [], 'names': [], - 'mappings': ';AAKA;EAHE,kBAAkB;;EAIpB,KAAK;IAED,OAAO' + 'mappings': ';AAKA;EAHE,AAAkB;;EAKpB,AAAK;IACD,AAAO', } B_EXPECTED_CSS = '''\ @@ -82,6 +83,7 @@ def normalize_path(path): ''' D_EXPECTED_CSS = '''\ +@charset "UTF-8"; body { background-color: green; } body a { @@ -89,6 +91,7 @@ def normalize_path(path): ''' D_EXPECTED_CSS_WITH_MAP = '''\ +@charset "UTF-8"; /* line 6, SOURCE */ body { background-color: green; } @@ -240,7 +243,8 @@ def test_compile_string(self): ''' actual = sass.compile(string=u'a { color: blue; } /* 유니코드 */') self.assertEqual( - u'''a { + u'''@charset "UTF-8"; +a { color: blue; } /* 유니코드 */''', @@ -458,7 +462,7 @@ def test_build_one(self): 'sources': ['../test/b.scss'], 'sourcesContent': [], 'names': [], - 'mappings': ';AAAA,EAAE;EAEE,WAAW' + 'mappings': ';AACA,AAAE;EACE,AAAW', }, os.path.join(d, 'css', 'b.scss.css.map') ) @@ -476,7 +480,7 @@ def test_build_one(self): 'sources': ['../test/d.scss'], 'sourcesContent': [], 'names': [], - 'mappings': ';AAKA;EAHE,kBAAkB;;EAIpB,KAAK;IAED,MAAM' + 'mappings': ';AAKA;EAHE,AAAkB;;EAKpB,AAAK;IACD,AAAM', }, os.path.join(d, 'css', 'd.scss.css.map') ) @@ -673,6 +677,64 @@ def test_sassc_sourcemap(self): shutil.rmtree(tmp_dir) +@contextlib.contextmanager +def tempdir(): + tmpdir = tempfile.mkdtemp() + try: + yield tmpdir + finally: + shutil.rmtree(tmpdir) + + +def write_file(filename, contents): + with open(filename, 'w') as f: + f.write(contents) + + +class CompileDirectoriesTest(unittest.TestCase): + + def test_successful(self): + with tempdir() as tmpdir: + input_dir = os.path.join(tmpdir, 'input') + output_dir = os.path.join(tmpdir, 'output') + os.makedirs(os.path.join(input_dir, 'foo')) + write_file(os.path.join(input_dir, 'f1.scss'), 'a { b { width: 100%; } }') + write_file(os.path.join(input_dir, 'foo/f2.scss'), 'foo { width: 100%; }') + # Make sure we don't compile non-scss files + write_file(os.path.join(input_dir, 'baz.txt'), 'Hello der') + + # the api for this is weird, why does it need source? + sass.compile(dirname=(input_dir, output_dir)) + assert os.path.exists(output_dir) + assert os.path.exists(os.path.join(output_dir, 'foo')) + assert os.path.exists(os.path.join(output_dir, 'f1.css')) + assert os.path.exists(os.path.join(output_dir, 'foo/f2.css')) + assert not os.path.exists(os.path.join(output_dir, 'baz.txt')) + + contentsf1 = open(os.path.join(output_dir, 'f1.css')).read() + contentsf2 = open(os.path.join(output_dir, 'foo/f2.css')).read() + self.assertEqual(contentsf1, 'a b {\n width: 100%; }\n') + self.assertEqual(contentsf2, 'foo {\n width: 100%; }\n') + + def test_error(self): + with tempdir() as tmpdir: + input_dir = os.path.join(tmpdir, 'input') + os.makedirs(input_dir) + write_file(os.path.join(input_dir, 'bad.scss'), 'a {') + + try: + sass.compile(dirname=(input_dir, os.path.join(tmpdir, 'output'))) + assert False, 'Expected to raise' + except sass.CompileError as e: + msg, = e.args + assert msg.decode('UTF-8').endswith( + 'bad.scss:1: invalid property name\n' + ), msg + return + except Exception as e: + assert False, 'Expected to raise CompileError but got {0!r}'.format(e) + + test_cases = [ SassTestCase, CompileTestCase, @@ -680,7 +742,8 @@ def test_sassc_sourcemap(self): ManifestTestCase, WsgiTestCase, DistutilsTestCase, - SasscTestCase + SasscTestCase, + CompileDirectoriesTest, ] loader = unittest.defaultTestLoader suite = unittest.TestSuite() diff --git a/setup.py b/setup.py index 590d9833..7a310230 100644 --- a/setup.py +++ b/setup.py @@ -46,7 +46,9 @@ sources_match = MAKEFILE_SOURCES_LIST_RE.search(makefile.read()) sources_list = sources_match.group('sources').replace('\\\n', ' ') libsass_sources.update(sources_list.split()) -libsass_sources = list(libsass_sources) +libsass_sources = set( + x for x in libsass_sources if not x.endswith('.hpp') and not x.endswith('.h') +) libsass_headers = [ os.path.join(LIBSASS_DIR, 'sass_interface.h'), diff --git a/tox.ini b/tox.ini index adfeea84..eaf45db0 100644 --- a/tox.ini +++ b/tox.ini @@ -5,4 +5,4 @@ envlist = pypy, py26, py27, py33, py34 deps = Werkzeug >= 0.9 commands = python -c 'from shutil import *; rmtree("build", True)' - python setup.py test + python -m unittest sasstests From 15df8fcceadaf642b29c242ca2b05e1b58cda2c0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 30 Jan 2015 13:31:07 -0800 Subject: [PATCH 2/2] Point at sass/libsass --- .gitmodules | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index f378a73a..975b971f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "libsass"] path = libsass -url = git://github.com/dahlia/libsass-python.git +url = git://github.com/sass/libsass.git