From 349c2113ade9d61dbb46e2735ac14418bcbfe8d9 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 31 Jan 2015 09:31:54 -0800 Subject: [PATCH 1/6] Implement custom functions. Resolves #13. --- Makefile | 37 ++++ pysass.cpp | 314 +++++++++++++++++++++++++++++++- sass.py | 41 ++++- sasstests.py | 389 ++++++++++++++++++++++++++++++++++++++++ sassutils/sass_types.py | 90 ++++++++++ 5 files changed, 859 insertions(+), 12 deletions(-) create mode 100644 Makefile create mode 100644 sassutils/sass_types.py diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..daaa3c93 --- /dev/null +++ b/Makefile @@ -0,0 +1,37 @@ +# This is to speed up development time. +# Usage: +# Needed once: +# $ virtualenv venv +# $ . venv/bin/activate +# $ pip install -e .` +# $ pip install werkzeug +# Once that is done, to rebuild simply: +# $ make -j 4 && python -m unittest sasstests + +PY_HEADERS := -I/usr/include/python2.7 +C_SOURCES := $(wildcard libsass/*.c) +C_OBJECTS = $(patsubst libsass/%.c,build2/libsass/c/%.o,$(C_SOURCES)) +CPP_SOURCES := $(wildcard libsass/*.cpp) +CPP_OBJECTS = $(patsubst libsass/%.cpp,build2/libsass/cpp/%.o,$(CPP_SOURCES)) + +all: _sass.so + +build2/libsass/c/%.o: libsass/%.c + @mkdir -p build2/libsass/c/ + gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fPIC -I./libsass $(PY_HEADERS) -c $^ -o $@ -c -O2 -fPIC -std=c++0x -Wall -Wno-parentheses + +build2/libsass/cpp/%.o: libsass/%.cpp + @mkdir -p build2/libsass/cpp/ + gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fPIC -I./libsass $(PY_HEADERS) -c $^ -o $@ -c -O2 -fPIC -std=c++0x -Wall -Wno-parentheses + +build2/pysass.o: pysass.cpp + @mkdir -p build2 + gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fPIC -I./libsass $(PY_HEADERS) -c $^ -o $@ -c -O2 -fPIC -std=c++0x -Wall -Wno-parentheses + +_sass.so: $(C_OBJECTS) $(CPP_OBJECTS) build2/pysass.o + g++ -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-Bsymbolic-functions -Wl,-z,relro $^ -L./libsass -o $@ -fPIC -lstdc++ + +.PHONY: clean +clean: + rm -rf build2 _sass.so + diff --git a/pysass.cpp b/pysass.cpp index 480f6a99..3218f4b8 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -35,6 +35,305 @@ static struct PySass_Pair PySass_output_style_enum[] = { {NULL} }; +static PyObject* _to_py_value(const union Sass_Value* value) { + PyObject* retv = NULL; + PyObject* types_mod = PyImport_ImportModule("sassutils.sass_types"); + PyObject* sass_comma = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_COMMA"); + PyObject* sass_space = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_SPACE"); + + switch (sass_value_get_tag(value)) { + case SASS_NULL: + retv = Py_None; + Py_INCREF(retv); + break; + case SASS_BOOLEAN: + retv = PyBool_FromLong(sass_boolean_get_value(value)); + break; + case SASS_STRING: + retv = PyUnicode_FromString(sass_string_get_value(value)); + break; + case SASS_NUMBER: + retv = PyObject_CallMethod( + types_mod, + "SassNumber", + PySass_IF_PY3("dy", "ds"), + sass_number_get_value(value), + sass_number_get_unit(value) + ); + break; + case SASS_COLOR: + retv = PyObject_CallMethod( + types_mod, + "SassColor", + "dddd", + sass_color_get_r(value), + sass_color_get_g(value), + sass_color_get_b(value), + sass_color_get_a(value) + ); + break; + case SASS_LIST: { + size_t i = 0; + PyObject* items = PyTuple_New(sass_list_get_length(value)); + PyObject* separator = sass_comma; + switch (sass_list_get_separator(value)) { + case SASS_COMMA: + separator = sass_comma; + break; + case SASS_SPACE: + separator = sass_space; + break; + } + for (i = 0; i < sass_list_get_length(value); i += 1) { + PyTuple_SetItem( + items, + i, + _to_py_value(sass_list_get_value(value, i)) + ); + } + retv = PyObject_CallMethod( + types_mod, "SassList", "OO", items, separator + ); + break; + } + case SASS_MAP: { + size_t i = 0; + PyObject* items = PyTuple_New(sass_map_get_length(value)); + for (i = 0; i < sass_map_get_length(value); i += 1) { + PyObject* kvp = PyTuple_New(2); + PyTuple_SetItem( + kvp, 0, _to_py_value(sass_map_get_key(value, i)) + ); + PyTuple_SetItem( + kvp, 1, _to_py_value(sass_map_get_value(value, i)) + ); + PyTuple_SetItem(items, i, kvp); + } + retv = PyObject_CallMethod(types_mod, "SassMap", "(O)", items); + Py_DECREF(items); + break; + } + case SASS_ERROR: + case SASS_WARNING: + /* @warning and @error cannot be passed */ + break; + } + + if (retv == NULL) { + PyErr_SetString(PyExc_TypeError, "Unexpected sass type"); + } + + Py_DECREF(types_mod); + Py_DECREF(sass_comma); + Py_DECREF(sass_space); + return retv; +} + +static union Sass_Value* _to_sass_value(PyObject* value) { + union Sass_Value* retv = NULL; + PyObject* types_mod = PyImport_ImportModule("sassutils.sass_types"); + PyObject* sass_number_t = PyObject_GetAttrString(types_mod, "SassNumber"); + PyObject* sass_color_t = PyObject_GetAttrString(types_mod, "SassColor"); + PyObject* sass_list_t = PyObject_GetAttrString(types_mod, "SassList"); + PyObject* sass_warning_t = PyObject_GetAttrString(types_mod, "SassWarning"); + PyObject* sass_error_t = PyObject_GetAttrString(types_mod, "SassError"); + PyObject* sass_comma = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_COMMA"); + PyObject* sass_space = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_SPACE"); + + if (value == Py_None) { + retv = sass_make_null(); + } else if (PyBool_Check(value)) { + retv = sass_make_boolean(value == Py_True); + } else if (PyUnicode_Check(value)) { + PyObject* bytes = PyUnicode_AsEncodedString(value, "UTF-8", "strict"); + retv = sass_make_string(PySass_Bytes_AS_STRING(bytes)); + Py_DECREF(bytes); + } else if (PySass_Bytes_Check(value)) { + retv = sass_make_string(PySass_Bytes_AS_STRING(value)); + } else if (PyDict_Check(value)) { + size_t i = 0; + Py_ssize_t pos = 0; + PyObject* d_key = NULL; + PyObject* d_value = NULL; + retv = sass_make_map(PyDict_Size(value)); + while (PyDict_Next(value, &pos, &d_key, &d_value)) { + sass_map_set_key(retv, i, _to_sass_value(d_key)); + sass_map_set_value(retv, i, _to_sass_value(d_value)); + i += 1; + } + } else if (PyObject_IsInstance(value, sass_number_t)) { + PyObject* d_value = PyObject_GetAttrString(value, "value"); + PyObject* unit = PyObject_GetAttrString(value, "unit"); + PyObject* bytes = PyUnicode_AsEncodedString(unit, "UTF-8", "strict"); + retv = sass_make_number( + PyFloat_AsDouble(d_value), PySass_Bytes_AS_STRING(bytes) + ); + Py_DECREF(d_value); + Py_DECREF(unit); + Py_DECREF(bytes); + } else if (PyObject_IsInstance(value, sass_color_t)) { + PyObject* r_value = PyObject_GetAttrString(value, "r"); + PyObject* g_value = PyObject_GetAttrString(value, "g"); + PyObject* b_value = PyObject_GetAttrString(value, "b"); + PyObject* a_value = PyObject_GetAttrString(value, "a"); + retv = sass_make_color( + PyFloat_AsDouble(r_value), + PyFloat_AsDouble(g_value), + PyFloat_AsDouble(b_value), + PyFloat_AsDouble(a_value) + ); + Py_DECREF(r_value); + Py_DECREF(g_value); + Py_DECREF(b_value); + Py_DECREF(a_value); + } else if (PyObject_IsInstance(value, sass_list_t)) { + Py_ssize_t i = 0; + PyObject* items = PyObject_GetAttrString(value, "items"); + PyObject* separator = PyObject_GetAttrString(value, "separator"); + /* TODO: I don't really like this, maybe move types to C */ + Sass_Separator sep = SASS_COMMA; + if (separator == sass_comma) { + sep = SASS_COMMA; + } else if (separator == sass_space) { + sep = SASS_SPACE; + } else { + assert(0); + } + retv = sass_make_list(PyTuple_Size(items), sep); + for (i = 0; i < PyTuple_Size(items); i += 1) { + sass_list_set_value( + retv, i, _to_sass_value(PyTuple_GET_ITEM(items, i)) + ); + } + Py_DECREF(items); + Py_DECREF(separator); + } else if (PyObject_IsInstance(value, sass_warning_t)) { + PyObject* msg = PyObject_GetAttrString(value, "msg"); + PyObject* bytes = PyUnicode_AsEncodedString(msg, "UTF-8", "strict"); + retv = sass_make_warning(PySass_Bytes_AS_STRING(bytes)); + Py_DECREF(msg); + Py_DECREF(bytes); + } else if (PyObject_IsInstance(value, sass_error_t)) { + PyObject* msg = PyObject_GetAttrString(value, "msg"); + PyObject* bytes = PyUnicode_AsEncodedString(msg, "UTF-8", "strict"); + retv = sass_make_error(PySass_Bytes_AS_STRING(bytes)); + Py_DECREF(msg); + Py_DECREF(bytes); + } + + if (retv == NULL) { + PyObject* type = PyObject_Type(value); + PyObject* type_name = PyObject_GetAttrString(type, "__name__"); + PyObject* fmt = PyUnicode_FromString( + "Unexpected type: `{0}`.\n" + "Expected one of:\n" + "- None\n" + "- bool\n" + "- str\n" + "- SassNumber\n" + "- SassColor\n" + "- SassList\n" + "- dict\n" + "- SassMap\n" + "- SassWarning\n" + "- SassError\n" + ); + PyObject* format_meth = PyObject_GetAttrString(fmt, "format"); + PyObject* result = PyObject_CallFunctionObjArgs( + format_meth, type_name, NULL + ); + PyObject* bytes = PyUnicode_AsEncodedString(result, "UTF-8", "strict"); + retv = sass_make_error(PySass_Bytes_AS_STRING(bytes)); + Py_DECREF(type); + Py_DECREF(type_name); + Py_DECREF(fmt); + Py_DECREF(format_meth); + Py_DECREF(result); + Py_DECREF(bytes); + } + + Py_DECREF(types_mod); + Py_DECREF(sass_number_t); + Py_DECREF(sass_color_t); + Py_DECREF(sass_list_t); + Py_DECREF(sass_warning_t); + Py_DECREF(sass_error_t); + Py_DECREF(sass_comma); + Py_DECREF(sass_space); + return retv; +} + +static union Sass_Value* _call_py_f( + const union Sass_Value* sass_args, void* cookie +) { + size_t i; + PyObject* pyfunc = (PyObject*)cookie; + PyObject* py_args = PyTuple_New(sass_list_get_length(sass_args)); + PyObject* py_result = NULL; + union Sass_Value* sass_result = NULL; + + for (i = 0; i < sass_list_get_length(sass_args); i += 1) { + union Sass_Value* sass_arg = sass_list_get_value(sass_args, i); + PyObject* py_arg = NULL; + if (!(py_arg = _to_py_value(sass_arg))) goto done; + PyTuple_SetItem(py_args, i, py_arg); + } + + if (!(py_result = PyObject_CallObject(pyfunc, py_args))) goto done; + sass_result = _to_sass_value(py_result); + +done: + if (sass_result == NULL) { + PyObject* etype = NULL; + PyObject* evalue = NULL; + PyObject* etb = NULL; + { + PyErr_Fetch(&etype, &evalue, &etb); + PyObject* traceback_mod = PyImport_ImportModule("traceback"); + PyObject* traceback_parts = PyObject_CallMethod( + traceback_mod, "format_exception", "OOO", etype, evalue, etb + ); + PyList_Insert(traceback_parts, 0, PyUnicode_FromString("\n")); + PyObject* joinstr = PyUnicode_FromString(""); + PyObject* result = PyUnicode_Join(joinstr, traceback_parts); + PyObject* bytes = PyUnicode_AsEncodedString( + result, "UTF-8", "strict" + ); + sass_result = sass_make_error(PySass_Bytes_AS_STRING(bytes)); + Py_DECREF(traceback_mod); + Py_DECREF(traceback_parts); + Py_DECREF(joinstr); + Py_DECREF(result); + Py_DECREF(bytes); + } + } + Py_XDECREF(py_args); + Py_XDECREF(py_result); + return sass_result; +} + + +static void _add_custom_functions( + struct Sass_Options* options, PyObject* custom_functions +) { + Py_ssize_t i; + Sass_C_Function_List fn_list = sass_make_function_list( + PyList_Size(custom_functions) + ); + for (i = 0; i < PyList_GET_SIZE(custom_functions); i += 1) { + PyObject* signature_and_func = PyList_GET_ITEM(custom_functions, i); + PyObject* signature = PyTuple_GET_ITEM(signature_and_func, 0); + PyObject* func = PyTuple_GET_ITEM(signature_and_func, 1); + Sass_C_Function_Callback fn = sass_make_function( + PySass_Bytes_AS_STRING(signature), + _call_py_f, + func + ); + sass_function_set_list_entry(fn_list, i, fn); + } + sass_option_set_c_functions(options, fn_list); +} + static PyObject * PySass_compile_string(PyObject *self, PyObject *args) { struct Sass_Context *ctx; @@ -44,12 +343,14 @@ PySass_compile_string(PyObject *self, PyObject *args) { const char *error_message, *output_string; Sass_Output_Style output_style; int source_comments, error_status, precision; + PyObject *custom_functions; PyObject *result; if (!PyArg_ParseTuple(args, - PySass_IF_PY3("yiiyyi", "siissi"), + PySass_IF_PY3("yiiyyiO", "siissiO"), &string, &output_style, &source_comments, - &include_paths, &image_path, &precision)) { + &include_paths, &image_path, &precision, + &custom_functions)) { return NULL; } @@ -60,6 +361,7 @@ PySass_compile_string(PyObject *self, PyObject *args) { sass_option_set_include_path(options, include_paths); sass_option_set_image_path(options, image_path); sass_option_set_precision(options, precision); + _add_custom_functions(options, custom_functions); sass_compile_data_context(context); @@ -85,12 +387,13 @@ PySass_compile_filename(PyObject *self, PyObject *args) { 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; + PyObject *source_map_filename, *custom_functions, *result; if (!PyArg_ParseTuple(args, - PySass_IF_PY3("yiiyyiO", "siissiO"), + PySass_IF_PY3("yiiyyiOO", "siissiOO"), &filename, &output_style, &source_comments, - &include_paths, &image_path, &precision, &source_map_filename)) { + &include_paths, &image_path, &precision, + &source_map_filename, &custom_functions)) { return NULL; } @@ -114,6 +417,7 @@ PySass_compile_filename(PyObject *self, PyObject *args) { sass_option_set_include_path(options, include_paths); sass_option_set_image_path(options, image_path); sass_option_set_precision(options, precision); + _add_custom_functions(options, custom_functions); sass_compile_file_context(context); diff --git a/sass.py b/sass.py index 3676a240..d63fdecb 100644 --- a/sass.py +++ b/sass.py @@ -11,6 +11,7 @@ """ import collections +import inspect import os import os.path import re @@ -57,9 +58,26 @@ def mkdirp(path): raise +def _prepare_custom_function_list(custom_functions): + # (signature, function_reference) + custom_function_list = [] + for func_name, func in sorted(custom_functions.items()): + argspec = inspect.getargspec(func) + if argspec.varargs or argspec.keywords or argspec.defaults: + raise TypeError( + 'Functions cannot have starargs or defaults: {0} {1}'.format( + func_name, func, + ) + ) + blinged_args = ['$' + arg for arg in argspec.args] + signature = '{0}({1})'.format(func_name, ', '.join(blinged_args)) + custom_function_list.append((signature.encode('UTF-8'), func)) + return custom_function_list + + def compile_dirname( search_path, output_path, output_style, source_comments, include_paths, - image_path, precision, + image_path, precision, custom_functions, ): fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() for dirpath, _, filenames in os.walk(search_path): @@ -74,7 +92,7 @@ def compile_dirname( input_filename = input_filename.encode(fs_encoding) s, v, _ = compile_filename( input_filename, output_style, source_comments, include_paths, - image_path, precision, None, + image_path, precision, None, custom_functions, ) if s: v = v.decode('UTF-8') @@ -274,13 +292,18 @@ def compile(**kwargs): repr(image_path)) elif isinstance(image_path, text_type): image_path = image_path.encode(fs_encoding) + + custom_functions = dict(kwargs.pop('custom_functions', {})) + custom_functions = _prepare_custom_function_list(custom_functions) + if 'string' in modes: string = kwargs.pop('string') if isinstance(string, text_type): string = string.encode('utf-8') s, v = compile_string(string, output_style, source_comments, - include_paths, image_path, precision) + include_paths, image_path, precision, + custom_functions) if s: return v.decode('utf-8') elif 'filename' in modes: @@ -294,7 +317,8 @@ def compile(**kwargs): s, v, source_map = compile_filename( filename, output_style, source_comments, - include_paths, image_path, precision, source_map_filename + include_paths, image_path, precision, source_map_filename, + custom_functions, ) if s: v = v.decode('utf-8') @@ -337,9 +361,12 @@ def compile(**kwargs): except ValueError: raise ValueError('dirname must be a pair of (source_dir, ' 'output_dir)') - s, v = compile_dirname(search_path, output_path, - output_style, source_comments, - include_paths, image_path, precision) + s, v = compile_dirname( + search_path, output_path, + output_style, source_comments, + include_paths, image_path, precision, + custom_functions, + ) if s: return else: diff --git a/sasstests.py b/sasstests.py index e78432d3..f2ec2e17 100644 --- a/sasstests.py +++ b/sasstests.py @@ -21,6 +21,7 @@ import sass import sassc +from sassutils import sass_types from sassutils.builder import Manifest, build_directory from sassutils.wsgi import SassMiddleware @@ -735,6 +736,391 @@ def test_error(self): assert False, 'Expected to raise CompileError but got {0!r}'.format(e) +class PrepareCustomFunctionListTest(unittest.TestCase): + def test_trivial(self): + self.assertEqual( + sass._prepare_custom_function_list({}), + [], + ) + + def test_noarg_functions(self): + func = lambda: 'bar' + self.assertEqual( + sass._prepare_custom_function_list({'foo': func}), + [(b'foo()', func)], + ) + + def test_functions_with_arguments(self): + func = lambda arg: 'baz' + self.assertEqual( + sass._prepare_custom_function_list({'foo': func}), + [(b'foo($arg)', func)], + ) + + def test_functions_many_arguments(self): + func = lambda foo, bar, baz: 'baz' + self.assertEqual( + sass._prepare_custom_function_list({'foo': func}), + [(b'foo($foo, $bar, $baz)', func)], + ) + + def test_raises_typeerror_kwargs(self): + self.assertRaises( + TypeError, + sass._prepare_custom_function_list, + {'foo': lambda bar='womp': 'baz'}, + ) + + def test_raises_typerror_star_kwargs(self): + self.assertRaises( + TypeError, + sass._prepare_custom_function_list, + {'foo': lambda *args: 'baz'}, + ) + + def test_raises_typeerror_star_kwargs(self): + self.assertRaises( + TypeError, + sass._prepare_custom_function_list, + {'foo': lambda *kwargs: 'baz'}, + ) + + +class SassTypesTest(unittest.TestCase): + def test_number_no_conversion(self): + num = sass_types.SassNumber(123., u'px') + assert type(num.value) is float, type(num.value) + assert type(num.unit) is text_type, type(num.unit) + + def test_number_conversion(self): + num = sass_types.SassNumber(123, b'px') + assert type(num.value) is float, type(num.value) + assert type(num.unit) is text_type, type(num.unit) + + def test_color_no_conversion(self): + color = sass_types.SassColor(1., 2., 3., .5) + assert type(color.r) is float, type(color.r) + assert type(color.g) is float, type(color.g) + assert type(color.b) is float, type(color.b) + assert type(color.a) is float, type(color.a) + + def test_color_conversion(self): + color = sass_types.SassColor(1, 2, 3, 1) + assert type(color.r) is float, type(color.r) + assert type(color.g) is float, type(color.g) + assert type(color.b) is float, type(color.b) + assert type(color.a) is float, type(color.a) + + def test_sass_list_no_conversion(self): + lst = sass_types.SassList( + ('foo', 'bar'), sass_types.SASS_SEPARATOR_COMMA, + ) + assert type(lst.items) is tuple, type(lst.items) + assert lst.separator is sass_types.SASS_SEPARATOR_COMMA, lst.separator + + def test_sass_list_conversion(self): + lst = sass_types.SassList( + ['foo', 'bar'], sass_types.SASS_SEPARATOR_SPACE, + ) + assert type(lst.items) is tuple, type(lst.items) + assert lst.separator is sass_types.SASS_SEPARATOR_SPACE, lst.separator + + def test_sass_warning_no_conversion(self): + warn = sass_types.SassWarning(u'error msg') + assert type(warn.msg) is text_type, type(warn.msg) + + def test_sass_warning_no_conversion(self): + warn = sass_types.SassWarning(b'error msg') + assert type(warn.msg) is text_type, type(warn.msg) + + def test_sass_error_no_conversion(self): + err = sass_types.SassError(u'error msg') + assert type(err.msg) is text_type, type(err.msg) + + def test_sass_error_conversion(self): + err = sass_types.SassError(b'error msg') + assert type(err.msg) is text_type, type(err.msg) + + +def raise_exc(x): + raise x + + +def identity(x): + # This has the side-effect of bubbling any exceptions we failed to process + # in C land + import sys + return x + + +custom_functions = { + 'raises': lambda: raise_exc(AssertionError('foo')), + 'returns_warning': lambda: sass_types.SassWarning('This is a warning'), + 'returns_error': lambda: sass_types.SassError('This is an error'), + # Tuples are a not-supported type. + 'returns_unknown': lambda: (1, 2, 3), + 'returns_true': lambda: True, + 'returns_false': lambda: False, + 'returns_none': lambda: None, + 'returns_unicode': lambda: u'☃', + 'returns_bytes': lambda: u'☃'.encode('UTF-8'), + 'returns_number': lambda: sass_types.SassNumber(5, 'px'), + 'returns_color': lambda: sass_types.SassColor(1, 2, 3, .5), + 'returns_comma_list': lambda: sass_types.SassList( + ('Arial', 'sans-serif'), sass_types.SASS_SEPARATOR_COMMA, + ), + 'returns_space_list': lambda: sass_types.SassList( + ('medium', 'none'), sass_types.SASS_SEPARATOR_SPACE, + ), + 'returns_py_dict': lambda: {'foo': 'bar'}, + 'returns_map': lambda: sass_types.SassMap((('foo', 'bar'),)), + # TODO: returns SassMap + 'identity': identity, +} + + +def compile_with_func(s): + return sass.compile( + string=s, + custom_functions=custom_functions, + output_style='compressed', + ) + + +@contextlib.contextmanager +def assert_raises_compile_error(expected): + try: + yield + assert False, 'Expected to raise!' + except sass.CompileError as e: + msg, = e.args + assert msg.decode('UTF-8') == expected, (msg, expected) + + +class RegexMatcher(object): + def __init__(self, reg, flags=None): + self.reg = re.compile(reg, re.MULTILINE | re.DOTALL) + + def __eq__(self, other): + return bool(self.reg.match(other)) + + +class CustomFunctionsTest(unittest.TestCase): + def test_raises(self): + with assert_raises_compile_error(RegexMatcher( + r'^stdin:1: error in C function raises: \n' + r'Traceback \(most recent call last\):\n' + r'.+' + r'AssertionError: foo\n\n' + r'Backtrace:\n' + r'\tstdin:1, in function `raises`\n' + r'\tstdin:1\n$', + )): + compile_with_func('a { content: raises(); }') + + def test_warning(self): + with assert_raises_compile_error( + 'stdin:1: warning in C function returns-warning: ' + 'This is a warning\n' + 'Backtrace:\n' + '\tstdin:1, in function `returns-warning`\n' + '\tstdin:1\n' + ): + compile_with_func('a { content: returns_warning(); }') + + def test_error(self): + with assert_raises_compile_error( + 'stdin:1: error in C function returns-error: ' + 'This is an error\n' + 'Backtrace:\n' + '\tstdin:1, in function `returns-error`\n' + '\tstdin:1\n', + ): + compile_with_func('a { content: returns_error(); }') + + def test_returns_unknown_object(self): + with assert_raises_compile_error( + 'stdin:1: error in C function returns-unknown: ' + 'Unexpected type: `tuple`.\n' + 'Expected one of:\n' + '- None\n' + '- bool\n' + '- str\n' + '- SassNumber\n' + '- SassColor\n' + '- SassList\n' + '- dict\n' + '- SassMap\n' + '- SassWarning\n' + '- SassError\n\n' + 'Backtrace:\n' + '\tstdin:1, in function `returns-unknown`\n' + '\tstdin:1\n', + ): + compile_with_func('a { content: returns_unknown(); }') + + def test_none(self): + self.assertEqual( + compile_with_func('a {color: #fff; content: returns_none();}'), + 'a{color:#fff}', + ) + + def test_true(self): + self.assertEqual( + compile_with_func('a { content: returns_true(); }'), + 'a{content:true}', + ) + + def test_false(self): + self.assertEqual( + compile_with_func('a { content: returns_false(); }'), + 'a{content:false}', + ) + + def test_unicode(self): + self.assertEqual( + compile_with_func('a { content: returns_unicode(); }'), + u'@charset "UTF-8";\n' + u'a{content:☃}', + ) + + def test_bytes(self): + self.assertEqual( + compile_with_func('a { content: returns_bytes(); }'), + u'@charset "UTF-8";\n' + u'a{content:☃}', + ) + + def test_number(self): + self.assertEqual( + compile_with_func('a { width: returns_number(); }'), + 'a{width:5px}', + ) + + def test_color(self): + self.assertEqual( + compile_with_func('a { color: returns_color(); }'), + 'a{color:rgba(1,2,3,0.5)}', + ) + + def test_comma_list(self): + self.assertEqual( + compile_with_func('a { font-family: returns_comma_list(); }'), + 'a{font-family:Arial,sans-serif}', + ) + + def test_space_list(self): + self.assertEqual( + compile_with_func('a { border-right: returns_space_list(); }'), + 'a{border-right:medium none}', + ) + + def test_py_dict(self): + self.assertEqual( + compile_with_func( + 'a { content: map-get(returns_py_dict(), foo); }', + ), + 'a{content:bar}', + ) + + def test_map(self): + self.assertEqual( + compile_with_func( + 'a { content: map-get(returns_map(), foo); }', + ), + 'a{content:bar}', + ) + + def test_identity_none(self): + self.assertEqual( + compile_with_func( + 'a {color: #fff; content: identity(returns_none());}', + ), + 'a{color:#fff}', + ) + + def test_identity_true(self): + self.assertEqual( + compile_with_func('a { content: identity(returns_true()); }'), + 'a{content:true}', + ) + + def test_identity_false(self): + self.assertEqual( + compile_with_func('a { content: identity(returns_false()); }'), + 'a{content:false}', + ) + + def test_identity_strings(self): + self.assertEqual( + compile_with_func('a { content: identity(returns_unicode()); }'), + u'@charset "UTF-8";\n' + u'a{content:☃}', + ) + + def test_identity_number(self): + self.assertEqual( + compile_with_func('a { width: identity(returns_number()); }'), + 'a{width:5px}', + ) + + def test_identity_color(self): + self.assertEqual( + compile_with_func('a { color: identity(returns_color()); }'), + 'a{color:rgba(1,2,3,0.5)}', + ) + + def test_identity_comma_list(self): + self.assertEqual( + compile_with_func( + 'a { font-family: identity(returns_comma_list()); }', + ), + 'a{font-family:Arial,sans-serif}', + ) + + def test_identity_space_list(self): + self.assertEqual( + compile_with_func( + 'a { border-right: identity(returns_space_list()); }', + ), + 'a{border-right:medium none}', + ) + + def test_identity_py_dict(self): + self.assertEqual( + compile_with_func( + 'a { content: map-get(identity(returns_py_dict()), foo); }', + ), + 'a{content:bar}', + ) + + def test_identity_map(self): + self.assertEqual( + compile_with_func( + 'a { content: map-get(identity(returns_map()), foo); }', + ), + 'a{content:bar}', + ) + + def test_list_with_map_item(self): + self.assertEqual( + compile_with_func( + 'a{content: ' + 'map-get(nth(identity(((foo: bar), (baz: womp))), 1), foo)' + '}' + ), + 'a{content:bar}' + ) + + def test_map_with_map_key(self): + self.assertEqual( + compile_with_func( + 'a{content: map-get(identity(((foo: bar): baz)), (foo: bar))}', + ), + 'a{content:baz}', + ) + + test_cases = [ SassTestCase, CompileTestCase, @@ -744,6 +1130,9 @@ def test_error(self): DistutilsTestCase, SasscTestCase, CompileDirectoriesTest, + PrepareCustomFunctionListTest, + SassTypesTest, + CustomFunctionsTest, ] loader = unittest.defaultTestLoader suite = unittest.TestSuite() diff --git a/sassutils/sass_types.py b/sassutils/sass_types.py new file mode 100644 index 00000000..f18997f0 --- /dev/null +++ b/sassutils/sass_types.py @@ -0,0 +1,90 @@ +""":mod:`sassutils.sass_types` --- Provides datatypes for custom functions. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This module provides datatypes to be used in custom sass functions. + +The following mappings from sass types to python types are used: + +SASS_NULL: ``None`` +SASS_BOOLEAN: ``True`` or ``False`` +SASS_STRING: class:`str` +SASS_NUMBER: class:`SassNumber` +SASS_COLOR: class:`SassColor` +SASS_LIST: class:`SassList` +SASS_MAP: class:`dict` or class:`SassMap` +SASS_ERROR: class:`SassError` +SASS_WARNING: class:`SassWarning` +""" +from __future__ import absolute_import +from __future__ import unicode_literals + +from collections import namedtuple + +from six import text_type + + +class SassNumber(namedtuple('SassNumber', ('value', 'unit'))): + def __new__(cls, value, unit): + value = float(value) + if not isinstance(unit, text_type): + unit = unit.decode('UTF-8') + return super(SassNumber, cls).__new__(cls, value, unit) + + +class SassColor(namedtuple('SassColor', ('r', 'g', 'b', 'a'))): + def __new__(cls, r, g, b, a): + r = float(r) + g = float(g) + b = float(b) + a = float(a) + return super(SassColor, cls).__new__(cls, r, g, b, a) + + +SASS_SEPARATOR_COMMA = namedtuple('SASS_SEPARATOR_COMMA', ())() +SASS_SEPARATOR_SPACE = namedtuple('SASS_SEPARATOR_SPACE', ())() +SEPARATORS = frozenset((SASS_SEPARATOR_COMMA, SASS_SEPARATOR_SPACE)) + + +class SassList(namedtuple('SassList', ('items', 'separator'))): + def __new__(cls, items, separator): + items = tuple(items) + assert separator in SEPARATORS + return super(SassList, cls).__new__(cls, items, separator) + + +class SassError(namedtuple('SassError', ('msg',))): + def __new__(cls, msg): + if not isinstance(msg, text_type): + msg = msg.decode('UTF-8') + return super(SassError, cls).__new__(cls, msg) + + +class SassWarning(namedtuple('SassError', ('msg',))): + def __new__(cls, msg): + if not isinstance(msg, text_type): + msg = msg.decode('UTF-8') + return super(SassWarning, cls).__new__(cls, msg) + + +class SassMap(dict): + """Because sass maps can have mapping types as keys, we need an immutable + hashable mapping type. + """ + __slots__ = ('_hash',) + + def __new__(cls, *args, **kwargs): + value = super(SassMap, cls).__new__(cls, *args, **kwargs) + # An assertion that all things are hashable + value._hash = hash(frozenset(value.items())) + return value + + def __repr__(self): + return '{0}({1})'.format(type(self).__name__, frozenset(self.items())) + + def __hash__(self): + return self._hash + + def _immutable(self, *_): + raise AssertionError('SassMaps are immutable') + + __setitem__ = __delitem__ = _immutable From 66f99cdc9c4180a4535988fc892238eeb8c25e83 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Feb 2015 06:51:37 -0800 Subject: [PATCH 2/6] Move sass types into sass module. --- pysass.cpp | 4 +- sass.py | 85 ++++++++++++++++++++++++++++++++++++++ sasstests.py | 47 +++++++++++---------- sassutils/sass_types.py | 90 ----------------------------------------- 4 files changed, 110 insertions(+), 116 deletions(-) delete mode 100644 sassutils/sass_types.py diff --git a/pysass.cpp b/pysass.cpp index 3218f4b8..0abf2300 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -37,7 +37,7 @@ static struct PySass_Pair PySass_output_style_enum[] = { static PyObject* _to_py_value(const union Sass_Value* value) { PyObject* retv = NULL; - PyObject* types_mod = PyImport_ImportModule("sassutils.sass_types"); + PyObject* types_mod = PyImport_ImportModule("sass"); PyObject* sass_comma = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_COMMA"); PyObject* sass_space = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_SPACE"); @@ -131,7 +131,7 @@ static PyObject* _to_py_value(const union Sass_Value* value) { static union Sass_Value* _to_sass_value(PyObject* value) { union Sass_Value* retv = NULL; - PyObject* types_mod = PyImport_ImportModule("sassutils.sass_types"); + PyObject* types_mod = PyImport_ImportModule("sass"); PyObject* sass_number_t = PyObject_GetAttrString(types_mod, "SassNumber"); PyObject* sass_color_t = PyObject_GetAttrString(types_mod, "SassColor"); PyObject* sass_list_t = PyObject_GetAttrString(types_mod, "SassList"); diff --git a/sass.py b/sass.py index d63fdecb..59462d28 100644 --- a/sass.py +++ b/sass.py @@ -10,6 +10,7 @@ 'a b {\n color: blue; }\n' """ +from __future__ import absolute_import import collections import inspect import os @@ -103,6 +104,7 @@ def compile_dirname( return False, v return True, None + def compile(**kwargs): """There are three modes of parameters :func:`compile()` can take: ``string``, ``filename``, and ``dirname``. @@ -394,3 +396,86 @@ def and_join(strings): return '' iterator = enumerate(strings) return ', '.join('and ' + s if i == last else s for i, s in iterator) + +""" +This module provides datatypes to be used in custom sass functions. + +The following mappings from sass types to python types are used: + +SASS_NULL: ``None`` +SASS_BOOLEAN: ``True`` or ``False`` +SASS_STRING: class:`str` +SASS_NUMBER: class:`SassNumber` +SASS_COLOR: class:`SassColor` +SASS_LIST: class:`SassList` +SASS_MAP: class:`dict` or class:`SassMap` +SASS_ERROR: class:`SassError` +SASS_WARNING: class:`SassWarning` +""" + + +class SassNumber(collections.namedtuple('SassNumber', ('value', 'unit'))): + def __new__(cls, value, unit): + value = float(value) + if not isinstance(unit, text_type): + unit = unit.decode('UTF-8') + return super(SassNumber, cls).__new__(cls, value, unit) + + +class SassColor(collections.namedtuple('SassColor', ('r', 'g', 'b', 'a'))): + def __new__(cls, r, g, b, a): + r = float(r) + g = float(g) + b = float(b) + a = float(a) + return super(SassColor, cls).__new__(cls, r, g, b, a) + + +SASS_SEPARATOR_COMMA = collections.namedtuple('SASS_SEPARATOR_COMMA', ())() +SASS_SEPARATOR_SPACE = collections.namedtuple('SASS_SEPARATOR_SPACE', ())() +SEPARATORS = frozenset((SASS_SEPARATOR_COMMA, SASS_SEPARATOR_SPACE)) + + +class SassList(collections.namedtuple('SassList', ('items', 'separator'))): + def __new__(cls, items, separator): + items = tuple(items) + assert separator in SEPARATORS + return super(SassList, cls).__new__(cls, items, separator) + + +class SassError(collections.namedtuple('SassError', ('msg',))): + def __new__(cls, msg): + if not isinstance(msg, text_type): + msg = msg.decode('UTF-8') + return super(SassError, cls).__new__(cls, msg) + + +class SassWarning(collections.namedtuple('SassError', ('msg',))): + def __new__(cls, msg): + if not isinstance(msg, text_type): + msg = msg.decode('UTF-8') + return super(SassWarning, cls).__new__(cls, msg) + + +class SassMap(dict): + """Because sass maps can have mapping types as keys, we need an immutable + hashable mapping type. + """ + __slots__ = ('_hash',) + + def __new__(cls, *args, **kwargs): + value = super(SassMap, cls).__new__(cls, *args, **kwargs) + # An assertion that all things are hashable + value._hash = hash(frozenset(value.items())) + return value + + def __repr__(self): + return '{0}({1})'.format(type(self).__name__, frozenset(self.items())) + + def __hash__(self): + return self._hash + + def _immutable(self, *_): + raise AssertionError('SassMaps are immutable') + + __setitem__ = __delitem__ = _immutable diff --git a/sasstests.py b/sasstests.py index f2ec2e17..42e48d99 100644 --- a/sasstests.py +++ b/sasstests.py @@ -21,7 +21,6 @@ import sass import sassc -from sassutils import sass_types from sassutils.builder import Manifest, build_directory from sassutils.wsgi import SassMiddleware @@ -788,57 +787,57 @@ def test_raises_typeerror_star_kwargs(self): class SassTypesTest(unittest.TestCase): def test_number_no_conversion(self): - num = sass_types.SassNumber(123., u'px') + num = sass.SassNumber(123., u'px') assert type(num.value) is float, type(num.value) assert type(num.unit) is text_type, type(num.unit) def test_number_conversion(self): - num = sass_types.SassNumber(123, b'px') + num = sass.SassNumber(123, b'px') assert type(num.value) is float, type(num.value) assert type(num.unit) is text_type, type(num.unit) def test_color_no_conversion(self): - color = sass_types.SassColor(1., 2., 3., .5) + color = sass.SassColor(1., 2., 3., .5) assert type(color.r) is float, type(color.r) assert type(color.g) is float, type(color.g) assert type(color.b) is float, type(color.b) assert type(color.a) is float, type(color.a) def test_color_conversion(self): - color = sass_types.SassColor(1, 2, 3, 1) + color = sass.SassColor(1, 2, 3, 1) assert type(color.r) is float, type(color.r) assert type(color.g) is float, type(color.g) assert type(color.b) is float, type(color.b) assert type(color.a) is float, type(color.a) def test_sass_list_no_conversion(self): - lst = sass_types.SassList( - ('foo', 'bar'), sass_types.SASS_SEPARATOR_COMMA, + lst = sass.SassList( + ('foo', 'bar'), sass.SASS_SEPARATOR_COMMA, ) assert type(lst.items) is tuple, type(lst.items) - assert lst.separator is sass_types.SASS_SEPARATOR_COMMA, lst.separator + assert lst.separator is sass.SASS_SEPARATOR_COMMA, lst.separator def test_sass_list_conversion(self): - lst = sass_types.SassList( - ['foo', 'bar'], sass_types.SASS_SEPARATOR_SPACE, + lst = sass.SassList( + ['foo', 'bar'], sass.SASS_SEPARATOR_SPACE, ) assert type(lst.items) is tuple, type(lst.items) - assert lst.separator is sass_types.SASS_SEPARATOR_SPACE, lst.separator + assert lst.separator is sass.SASS_SEPARATOR_SPACE, lst.separator def test_sass_warning_no_conversion(self): - warn = sass_types.SassWarning(u'error msg') + warn = sass.SassWarning(u'error msg') assert type(warn.msg) is text_type, type(warn.msg) def test_sass_warning_no_conversion(self): - warn = sass_types.SassWarning(b'error msg') + warn = sass.SassWarning(b'error msg') assert type(warn.msg) is text_type, type(warn.msg) def test_sass_error_no_conversion(self): - err = sass_types.SassError(u'error msg') + err = sass.SassError(u'error msg') assert type(err.msg) is text_type, type(err.msg) def test_sass_error_conversion(self): - err = sass_types.SassError(b'error msg') + err = sass.SassError(b'error msg') assert type(err.msg) is text_type, type(err.msg) @@ -855,8 +854,8 @@ def identity(x): custom_functions = { 'raises': lambda: raise_exc(AssertionError('foo')), - 'returns_warning': lambda: sass_types.SassWarning('This is a warning'), - 'returns_error': lambda: sass_types.SassError('This is an error'), + 'returns_warning': lambda: sass.SassWarning('This is a warning'), + 'returns_error': lambda: sass.SassError('This is an error'), # Tuples are a not-supported type. 'returns_unknown': lambda: (1, 2, 3), 'returns_true': lambda: True, @@ -864,16 +863,16 @@ def identity(x): 'returns_none': lambda: None, 'returns_unicode': lambda: u'☃', 'returns_bytes': lambda: u'☃'.encode('UTF-8'), - 'returns_number': lambda: sass_types.SassNumber(5, 'px'), - 'returns_color': lambda: sass_types.SassColor(1, 2, 3, .5), - 'returns_comma_list': lambda: sass_types.SassList( - ('Arial', 'sans-serif'), sass_types.SASS_SEPARATOR_COMMA, + 'returns_number': lambda: sass.SassNumber(5, 'px'), + 'returns_color': lambda: sass.SassColor(1, 2, 3, .5), + 'returns_comma_list': lambda: sass.SassList( + ('Arial', 'sans-serif'), sass.SASS_SEPARATOR_COMMA, ), - 'returns_space_list': lambda: sass_types.SassList( - ('medium', 'none'), sass_types.SASS_SEPARATOR_SPACE, + 'returns_space_list': lambda: sass.SassList( + ('medium', 'none'), sass.SASS_SEPARATOR_SPACE, ), 'returns_py_dict': lambda: {'foo': 'bar'}, - 'returns_map': lambda: sass_types.SassMap((('foo', 'bar'),)), + 'returns_map': lambda: sass.SassMap((('foo', 'bar'),)), # TODO: returns SassMap 'identity': identity, } diff --git a/sassutils/sass_types.py b/sassutils/sass_types.py deleted file mode 100644 index f18997f0..00000000 --- a/sassutils/sass_types.py +++ /dev/null @@ -1,90 +0,0 @@ -""":mod:`sassutils.sass_types` --- Provides datatypes for custom functions. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -This module provides datatypes to be used in custom sass functions. - -The following mappings from sass types to python types are used: - -SASS_NULL: ``None`` -SASS_BOOLEAN: ``True`` or ``False`` -SASS_STRING: class:`str` -SASS_NUMBER: class:`SassNumber` -SASS_COLOR: class:`SassColor` -SASS_LIST: class:`SassList` -SASS_MAP: class:`dict` or class:`SassMap` -SASS_ERROR: class:`SassError` -SASS_WARNING: class:`SassWarning` -""" -from __future__ import absolute_import -from __future__ import unicode_literals - -from collections import namedtuple - -from six import text_type - - -class SassNumber(namedtuple('SassNumber', ('value', 'unit'))): - def __new__(cls, value, unit): - value = float(value) - if not isinstance(unit, text_type): - unit = unit.decode('UTF-8') - return super(SassNumber, cls).__new__(cls, value, unit) - - -class SassColor(namedtuple('SassColor', ('r', 'g', 'b', 'a'))): - def __new__(cls, r, g, b, a): - r = float(r) - g = float(g) - b = float(b) - a = float(a) - return super(SassColor, cls).__new__(cls, r, g, b, a) - - -SASS_SEPARATOR_COMMA = namedtuple('SASS_SEPARATOR_COMMA', ())() -SASS_SEPARATOR_SPACE = namedtuple('SASS_SEPARATOR_SPACE', ())() -SEPARATORS = frozenset((SASS_SEPARATOR_COMMA, SASS_SEPARATOR_SPACE)) - - -class SassList(namedtuple('SassList', ('items', 'separator'))): - def __new__(cls, items, separator): - items = tuple(items) - assert separator in SEPARATORS - return super(SassList, cls).__new__(cls, items, separator) - - -class SassError(namedtuple('SassError', ('msg',))): - def __new__(cls, msg): - if not isinstance(msg, text_type): - msg = msg.decode('UTF-8') - return super(SassError, cls).__new__(cls, msg) - - -class SassWarning(namedtuple('SassError', ('msg',))): - def __new__(cls, msg): - if not isinstance(msg, text_type): - msg = msg.decode('UTF-8') - return super(SassWarning, cls).__new__(cls, msg) - - -class SassMap(dict): - """Because sass maps can have mapping types as keys, we need an immutable - hashable mapping type. - """ - __slots__ = ('_hash',) - - def __new__(cls, *args, **kwargs): - value = super(SassMap, cls).__new__(cls, *args, **kwargs) - # An assertion that all things are hashable - value._hash = hash(frozenset(value.items())) - return value - - def __repr__(self): - return '{0}({1})'.format(type(self).__name__, frozenset(self.items())) - - def __hash__(self): - return self._hash - - def _immutable(self, *_): - raise AssertionError('SassMaps are immutable') - - __setitem__ = __delitem__ = _immutable From 9702c1790df9cd9636f64f675de20999c15c14f5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Feb 2015 08:42:24 -0800 Subject: [PATCH 3/6] Use Mapping interface --- pysass.cpp | 15 +++++++++++---- sass.py | 26 +++++++++++++++++++------- 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/pysass.cpp b/pysass.cpp index 0abf2300..7dff951f 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -139,6 +139,8 @@ static union Sass_Value* _to_sass_value(PyObject* value) { PyObject* sass_error_t = PyObject_GetAttrString(types_mod, "SassError"); PyObject* sass_comma = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_COMMA"); PyObject* sass_space = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_SPACE"); + PyObject* collections_mod = PyImport_ImportModule("collections"); + PyObject* mapping_t = PyObject_GetAttrString(collections_mod, "Mapping"); if (value == Py_None) { retv = sass_make_null(); @@ -150,17 +152,21 @@ static union Sass_Value* _to_sass_value(PyObject* value) { Py_DECREF(bytes); } else if (PySass_Bytes_Check(value)) { retv = sass_make_string(PySass_Bytes_AS_STRING(value)); - } else if (PyDict_Check(value)) { + /* XXX: PyMapping_Check returns true for lists and tuples in python3 :( */ + } else if (PyObject_IsInstance(value, mapping_t)) { size_t i = 0; Py_ssize_t pos = 0; PyObject* d_key = NULL; PyObject* d_value = NULL; - retv = sass_make_map(PyDict_Size(value)); - while (PyDict_Next(value, &pos, &d_key, &d_value)) { + PyObject* dct = PyDict_New(); + PyDict_Update(dct, value); + retv = sass_make_map(PyDict_Size(dct)); + while (PyDict_Next(dct, &pos, &d_key, &d_value)) { sass_map_set_key(retv, i, _to_sass_value(d_key)); sass_map_set_value(retv, i, _to_sass_value(d_value)); i += 1; } + Py_DECREF(dct); } else if (PyObject_IsInstance(value, sass_number_t)) { PyObject* d_value = PyObject_GetAttrString(value, "value"); PyObject* unit = PyObject_GetAttrString(value, "unit"); @@ -190,7 +196,6 @@ static union Sass_Value* _to_sass_value(PyObject* value) { Py_ssize_t i = 0; PyObject* items = PyObject_GetAttrString(value, "items"); PyObject* separator = PyObject_GetAttrString(value, "separator"); - /* TODO: I don't really like this, maybe move types to C */ Sass_Separator sep = SASS_COMMA; if (separator == sass_comma) { sep = SASS_COMMA; @@ -260,6 +265,8 @@ static union Sass_Value* _to_sass_value(PyObject* value) { Py_DECREF(sass_error_t); Py_DECREF(sass_comma); Py_DECREF(sass_space); + Py_DECREF(collections_mod); + Py_DECREF(mapping_t); return retv; } diff --git a/sass.py b/sass.py index 59462d28..07753a17 100644 --- a/sass.py +++ b/sass.py @@ -457,17 +457,29 @@ def __new__(cls, msg): return super(SassWarning, cls).__new__(cls, msg) -class SassMap(dict): +class SassMap(collections.Mapping): """Because sass maps can have mapping types as keys, we need an immutable hashable mapping type. """ - __slots__ = ('_hash',) + __slots__ = ('_dict', '_hash',) - def __new__(cls, *args, **kwargs): - value = super(SassMap, cls).__new__(cls, *args, **kwargs) + def __init__(self, *args, **kwargs): + self._dict = dict(*args, **kwargs) # An assertion that all things are hashable - value._hash = hash(frozenset(value.items())) - return value + self._hash = hash(frozenset(self._dict.items())) + + # Mapping interface + + def __getitem__(self, key): + return self._dict[key] + + def __iter__(self): + return iter(self._dict) + + def __len__(self): + return len(self._dict) + + # Our interface def __repr__(self): return '{0}({1})'.format(type(self).__name__, frozenset(self.items())) @@ -476,6 +488,6 @@ def __hash__(self): return self._hash def _immutable(self, *_): - raise AssertionError('SassMaps are immutable') + raise TypeError('SassMaps are immutable.') __setitem__ = __delitem__ = _immutable From a5777f339ba2e8b064b52cd20c14f919b410bc52 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Feb 2015 09:14:47 -0800 Subject: [PATCH 4/6] Split things into functions --- pysass.cpp | 315 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 192 insertions(+), 123 deletions(-) diff --git a/pysass.cpp b/pysass.cpp index 7dff951f..62180b5c 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -22,6 +22,19 @@ extern "C" { #endif +static PyObject* _to_py_value(const union Sass_Value* value); +static union Sass_Value* _to_sass_value(PyObject* value); + +static union Sass_Value* _color_to_sass_value(PyObject* value); +static union Sass_Value* _number_to_sass_value(PyObject* value); +static union Sass_Value* _list_to_sass_value(PyObject* value); +static union Sass_Value* _mapping_to_sass_value(PyObject* value); +static union Sass_Value* _unicode_to_sass_value(PyObject* value); +static union Sass_Value* _warning_to_sass_value(PyObject* value); +static union Sass_Value* _error_to_sass_value(PyObject* value); +static union Sass_Value* _unknown_type_to_sass_error(PyObject* value); +static union Sass_Value* _exception_to_sass_error(); + struct PySass_Pair { char *label; int value; @@ -129,6 +142,175 @@ static PyObject* _to_py_value(const union Sass_Value* value) { return retv; } +static union Sass_Value* _color_to_sass_value(PyObject* value) { + union Sass_Value* retv = NULL; + PyObject* r_value = PyObject_GetAttrString(value, "r"); + PyObject* g_value = PyObject_GetAttrString(value, "g"); + PyObject* b_value = PyObject_GetAttrString(value, "b"); + PyObject* a_value = PyObject_GetAttrString(value, "a"); + retv = sass_make_color( + PyFloat_AsDouble(r_value), + PyFloat_AsDouble(g_value), + PyFloat_AsDouble(b_value), + PyFloat_AsDouble(a_value) + ); + Py_DECREF(r_value); + Py_DECREF(g_value); + Py_DECREF(b_value); + Py_DECREF(a_value); + return retv; +} + +static union Sass_Value* _list_to_sass_value(PyObject* value) { + PyObject* types_mod = PyImport_ImportModule("sass"); + PyObject* sass_comma = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_COMMA"); + PyObject* sass_space = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_SPACE"); + union Sass_Value* retv = NULL; + Py_ssize_t i = 0; + PyObject* items = PyObject_GetAttrString(value, "items"); + PyObject* separator = PyObject_GetAttrString(value, "separator"); + Sass_Separator sep = SASS_COMMA; + if (separator == sass_comma) { + sep = SASS_COMMA; + } else if (separator == sass_space) { + sep = SASS_SPACE; + } else { + assert(0); + } + retv = sass_make_list(PyTuple_Size(items), sep); + for (i = 0; i < PyTuple_Size(items); i += 1) { + sass_list_set_value( + retv, i, _to_sass_value(PyTuple_GET_ITEM(items, i)) + ); + } + Py_DECREF(types_mod); + Py_DECREF(sass_comma); + Py_DECREF(sass_space); + Py_DECREF(items); + Py_DECREF(separator); + return retv; +} + +static union Sass_Value* _mapping_to_sass_value(PyObject* value) { + union Sass_Value* retv = NULL; + size_t i = 0; + Py_ssize_t pos = 0; + PyObject* d_key = NULL; + PyObject* d_value = NULL; + PyObject* dct = PyDict_New(); + PyDict_Update(dct, value); + retv = sass_make_map(PyDict_Size(dct)); + while (PyDict_Next(dct, &pos, &d_key, &d_value)) { + sass_map_set_key(retv, i, _to_sass_value(d_key)); + sass_map_set_value(retv, i, _to_sass_value(d_value)); + i += 1; + } + Py_DECREF(dct); + return retv; +} + +static union Sass_Value* _number_to_sass_value(PyObject* value) { + union Sass_Value* retv = NULL; + PyObject* d_value = PyObject_GetAttrString(value, "value"); + PyObject* unit = PyObject_GetAttrString(value, "unit"); + PyObject* bytes = PyUnicode_AsEncodedString(unit, "UTF-8", "strict"); + retv = sass_make_number( + PyFloat_AsDouble(d_value), PySass_Bytes_AS_STRING(bytes) + ); + Py_DECREF(d_value); + Py_DECREF(unit); + Py_DECREF(bytes); + return retv; +} + +static union Sass_Value* _unicode_to_sass_value(PyObject* value) { + union Sass_Value* retv = NULL; + PyObject* bytes = PyUnicode_AsEncodedString(value, "UTF-8", "strict"); + retv = sass_make_string(PySass_Bytes_AS_STRING(bytes)); + Py_DECREF(bytes); + return retv; +} + +static union Sass_Value* _warning_to_sass_value(PyObject* value) { + union Sass_Value* retv = NULL; + PyObject* msg = PyObject_GetAttrString(value, "msg"); + PyObject* bytes = PyUnicode_AsEncodedString(msg, "UTF-8", "strict"); + retv = sass_make_warning(PySass_Bytes_AS_STRING(bytes)); + Py_DECREF(msg); + Py_DECREF(bytes); + return retv; +} + +static union Sass_Value* _error_to_sass_value(PyObject* value) { + union Sass_Value* retv = NULL; + PyObject* msg = PyObject_GetAttrString(value, "msg"); + PyObject* bytes = PyUnicode_AsEncodedString(msg, "UTF-8", "strict"); + retv = sass_make_error(PySass_Bytes_AS_STRING(bytes)); + Py_DECREF(msg); + Py_DECREF(bytes); + return retv; +} + +static union Sass_Value* _unknown_type_to_sass_error(PyObject* value) { + union Sass_Value* retv = NULL; + PyObject* type = PyObject_Type(value); + PyObject* type_name = PyObject_GetAttrString(type, "__name__"); + PyObject* fmt = PyUnicode_FromString( + "Unexpected type: `{0}`.\n" + "Expected one of:\n" + "- None\n" + "- bool\n" + "- str\n" + "- SassNumber\n" + "- SassColor\n" + "- SassList\n" + "- dict\n" + "- SassMap\n" + "- SassWarning\n" + "- SassError\n" + ); + PyObject* format_meth = PyObject_GetAttrString(fmt, "format"); + PyObject* result = PyObject_CallFunctionObjArgs( + format_meth, type_name, NULL + ); + PyObject* bytes = PyUnicode_AsEncodedString(result, "UTF-8", "strict"); + retv = sass_make_error(PySass_Bytes_AS_STRING(bytes)); + Py_DECREF(type); + Py_DECREF(type_name); + Py_DECREF(fmt); + Py_DECREF(format_meth); + Py_DECREF(result); + Py_DECREF(bytes); + return retv; +} + +static union Sass_Value* _exception_to_sass_error() { + union Sass_Value* retv = NULL; + PyObject* etype = NULL; + PyObject* evalue = NULL; + PyObject* etb = NULL; + PyErr_Fetch(&etype, &evalue, &etb); + { + PyObject* traceback_mod = PyImport_ImportModule("traceback"); + PyObject* traceback_parts = PyObject_CallMethod( + traceback_mod, "format_exception", "OOO", etype, evalue, etb + ); + PyList_Insert(traceback_parts, 0, PyUnicode_FromString("\n")); + PyObject* joinstr = PyUnicode_FromString(""); + PyObject* result = PyUnicode_Join(joinstr, traceback_parts); + PyObject* bytes = PyUnicode_AsEncodedString( + result, "UTF-8", "strict" + ); + retv = sass_make_error(PySass_Bytes_AS_STRING(bytes)); + Py_DECREF(traceback_mod); + Py_DECREF(traceback_parts); + Py_DECREF(joinstr); + Py_DECREF(result); + Py_DECREF(bytes); + } + return retv; +} + static union Sass_Value* _to_sass_value(PyObject* value) { union Sass_Value* retv = NULL; PyObject* types_mod = PyImport_ImportModule("sass"); @@ -137,8 +319,6 @@ static union Sass_Value* _to_sass_value(PyObject* value) { PyObject* sass_list_t = PyObject_GetAttrString(types_mod, "SassList"); PyObject* sass_warning_t = PyObject_GetAttrString(types_mod, "SassWarning"); PyObject* sass_error_t = PyObject_GetAttrString(types_mod, "SassError"); - PyObject* sass_comma = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_COMMA"); - PyObject* sass_space = PyObject_GetAttrString(types_mod, "SASS_SEPARATOR_SPACE"); PyObject* collections_mod = PyImport_ImportModule("collections"); PyObject* mapping_t = PyObject_GetAttrString(collections_mod, "Mapping"); @@ -147,114 +327,26 @@ static union Sass_Value* _to_sass_value(PyObject* value) { } else if (PyBool_Check(value)) { retv = sass_make_boolean(value == Py_True); } else if (PyUnicode_Check(value)) { - PyObject* bytes = PyUnicode_AsEncodedString(value, "UTF-8", "strict"); - retv = sass_make_string(PySass_Bytes_AS_STRING(bytes)); - Py_DECREF(bytes); + retv = _unicode_to_sass_value(value); } else if (PySass_Bytes_Check(value)) { retv = sass_make_string(PySass_Bytes_AS_STRING(value)); /* XXX: PyMapping_Check returns true for lists and tuples in python3 :( */ } else if (PyObject_IsInstance(value, mapping_t)) { - size_t i = 0; - Py_ssize_t pos = 0; - PyObject* d_key = NULL; - PyObject* d_value = NULL; - PyObject* dct = PyDict_New(); - PyDict_Update(dct, value); - retv = sass_make_map(PyDict_Size(dct)); - while (PyDict_Next(dct, &pos, &d_key, &d_value)) { - sass_map_set_key(retv, i, _to_sass_value(d_key)); - sass_map_set_value(retv, i, _to_sass_value(d_value)); - i += 1; - } - Py_DECREF(dct); + retv = _mapping_to_sass_value(value); } else if (PyObject_IsInstance(value, sass_number_t)) { - PyObject* d_value = PyObject_GetAttrString(value, "value"); - PyObject* unit = PyObject_GetAttrString(value, "unit"); - PyObject* bytes = PyUnicode_AsEncodedString(unit, "UTF-8", "strict"); - retv = sass_make_number( - PyFloat_AsDouble(d_value), PySass_Bytes_AS_STRING(bytes) - ); - Py_DECREF(d_value); - Py_DECREF(unit); - Py_DECREF(bytes); + retv = _number_to_sass_value(value); } else if (PyObject_IsInstance(value, sass_color_t)) { - PyObject* r_value = PyObject_GetAttrString(value, "r"); - PyObject* g_value = PyObject_GetAttrString(value, "g"); - PyObject* b_value = PyObject_GetAttrString(value, "b"); - PyObject* a_value = PyObject_GetAttrString(value, "a"); - retv = sass_make_color( - PyFloat_AsDouble(r_value), - PyFloat_AsDouble(g_value), - PyFloat_AsDouble(b_value), - PyFloat_AsDouble(a_value) - ); - Py_DECREF(r_value); - Py_DECREF(g_value); - Py_DECREF(b_value); - Py_DECREF(a_value); + retv = _color_to_sass_value(value); } else if (PyObject_IsInstance(value, sass_list_t)) { - Py_ssize_t i = 0; - PyObject* items = PyObject_GetAttrString(value, "items"); - PyObject* separator = PyObject_GetAttrString(value, "separator"); - Sass_Separator sep = SASS_COMMA; - if (separator == sass_comma) { - sep = SASS_COMMA; - } else if (separator == sass_space) { - sep = SASS_SPACE; - } else { - assert(0); - } - retv = sass_make_list(PyTuple_Size(items), sep); - for (i = 0; i < PyTuple_Size(items); i += 1) { - sass_list_set_value( - retv, i, _to_sass_value(PyTuple_GET_ITEM(items, i)) - ); - } - Py_DECREF(items); - Py_DECREF(separator); + retv = _list_to_sass_value(value); } else if (PyObject_IsInstance(value, sass_warning_t)) { - PyObject* msg = PyObject_GetAttrString(value, "msg"); - PyObject* bytes = PyUnicode_AsEncodedString(msg, "UTF-8", "strict"); - retv = sass_make_warning(PySass_Bytes_AS_STRING(bytes)); - Py_DECREF(msg); - Py_DECREF(bytes); + retv = _warning_to_sass_value(value); } else if (PyObject_IsInstance(value, sass_error_t)) { - PyObject* msg = PyObject_GetAttrString(value, "msg"); - PyObject* bytes = PyUnicode_AsEncodedString(msg, "UTF-8", "strict"); - retv = sass_make_error(PySass_Bytes_AS_STRING(bytes)); - Py_DECREF(msg); - Py_DECREF(bytes); + retv = _error_to_sass_value(value); } if (retv == NULL) { - PyObject* type = PyObject_Type(value); - PyObject* type_name = PyObject_GetAttrString(type, "__name__"); - PyObject* fmt = PyUnicode_FromString( - "Unexpected type: `{0}`.\n" - "Expected one of:\n" - "- None\n" - "- bool\n" - "- str\n" - "- SassNumber\n" - "- SassColor\n" - "- SassList\n" - "- dict\n" - "- SassMap\n" - "- SassWarning\n" - "- SassError\n" - ); - PyObject* format_meth = PyObject_GetAttrString(fmt, "format"); - PyObject* result = PyObject_CallFunctionObjArgs( - format_meth, type_name, NULL - ); - PyObject* bytes = PyUnicode_AsEncodedString(result, "UTF-8", "strict"); - retv = sass_make_error(PySass_Bytes_AS_STRING(bytes)); - Py_DECREF(type); - Py_DECREF(type_name); - Py_DECREF(fmt); - Py_DECREF(format_meth); - Py_DECREF(result); - Py_DECREF(bytes); + retv = _unknown_type_to_sass_error(value); } Py_DECREF(types_mod); @@ -263,8 +355,6 @@ static union Sass_Value* _to_sass_value(PyObject* value) { Py_DECREF(sass_list_t); Py_DECREF(sass_warning_t); Py_DECREF(sass_error_t); - Py_DECREF(sass_comma); - Py_DECREF(sass_space); Py_DECREF(collections_mod); Py_DECREF(mapping_t); return retv; @@ -280,7 +370,7 @@ static union Sass_Value* _call_py_f( union Sass_Value* sass_result = NULL; for (i = 0; i < sass_list_get_length(sass_args); i += 1) { - union Sass_Value* sass_arg = sass_list_get_value(sass_args, i); + const union Sass_Value* sass_arg = sass_list_get_value(sass_args, i); PyObject* py_arg = NULL; if (!(py_arg = _to_py_value(sass_arg))) goto done; PyTuple_SetItem(py_args, i, py_arg); @@ -291,28 +381,7 @@ static union Sass_Value* _call_py_f( done: if (sass_result == NULL) { - PyObject* etype = NULL; - PyObject* evalue = NULL; - PyObject* etb = NULL; - { - PyErr_Fetch(&etype, &evalue, &etb); - PyObject* traceback_mod = PyImport_ImportModule("traceback"); - PyObject* traceback_parts = PyObject_CallMethod( - traceback_mod, "format_exception", "OOO", etype, evalue, etb - ); - PyList_Insert(traceback_parts, 0, PyUnicode_FromString("\n")); - PyObject* joinstr = PyUnicode_FromString(""); - PyObject* result = PyUnicode_Join(joinstr, traceback_parts); - PyObject* bytes = PyUnicode_AsEncodedString( - result, "UTF-8", "strict" - ); - sass_result = sass_make_error(PySass_Bytes_AS_STRING(bytes)); - Py_DECREF(traceback_mod); - Py_DECREF(traceback_parts); - Py_DECREF(joinstr); - Py_DECREF(result); - Py_DECREF(bytes); - } + sass_result = _exception_to_sass_error(); } Py_XDECREF(py_args); Py_XDECREF(py_result); From 115c2351358b2d32ac70e672141aa94c5ae1bac0 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Feb 2015 12:57:40 -0800 Subject: [PATCH 5/6] pypy is unhappy with dicts --- pysass.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pysass.cpp b/pysass.cpp index 62180b5c..4e36736e 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -331,7 +331,8 @@ static union Sass_Value* _to_sass_value(PyObject* value) { } else if (PySass_Bytes_Check(value)) { retv = sass_make_string(PySass_Bytes_AS_STRING(value)); /* XXX: PyMapping_Check returns true for lists and tuples in python3 :( */ - } else if (PyObject_IsInstance(value, mapping_t)) { + /* XXX: pypy derps on dicts: https://bitbucket.org/pypy/pypy/issue/1970 */ + } else if (PyDict_Check(value) || PyObject_IsInstance(value, mapping_t)) { retv = _mapping_to_sass_value(value); } else if (PyObject_IsInstance(value, sass_number_t)) { retv = _number_to_sass_value(value); From b5faf604fef9138a66d8d43b192aa9bd50f0d090 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Tue, 3 Feb 2015 21:32:52 -0800 Subject: [PATCH 6/6] Maybe fix windows!!!!! --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7a310230..988c49cf 100644 --- a/setup.py +++ b/setup.py @@ -90,7 +90,7 @@ def spawn(self, cmd): spawn(cmd, dry_run=self.dry_run) from distutils.msvc9compiler import MSVCCompiler MSVCCompiler.spawn = spawn - flags = ['-I' + os.path.abspath('win32')] + flags = ['-I' + os.path.abspath('win32'), '/EHsc'] link_flags = [] else: flags = ['-fPIC', '-std=c++0x', '-Wall', '-Wno-parentheses'] pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy