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..4e36736e 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; @@ -35,6 +48,369 @@ 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("sass"); + 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* _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"); + 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* collections_mod = PyImport_ImportModule("collections"); + PyObject* mapping_t = PyObject_GetAttrString(collections_mod, "Mapping"); + + 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)) { + 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 :( */ + /* 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); + } else if (PyObject_IsInstance(value, sass_color_t)) { + retv = _color_to_sass_value(value); + } else if (PyObject_IsInstance(value, sass_list_t)) { + retv = _list_to_sass_value(value); + } else if (PyObject_IsInstance(value, sass_warning_t)) { + retv = _warning_to_sass_value(value); + } else if (PyObject_IsInstance(value, sass_error_t)) { + retv = _error_to_sass_value(value); + } + + if (retv == NULL) { + retv = _unknown_type_to_sass_error(value); + } + + 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(collections_mod); + Py_DECREF(mapping_t); + 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) { + 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); + } + + if (!(py_result = PyObject_CallObject(pyfunc, py_args))) goto done; + sass_result = _to_sass_value(py_result); + +done: + if (sass_result == NULL) { + sass_result = _exception_to_sass_error(); + } + 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 +420,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 +438,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 +464,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 +494,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..07753a17 100644 --- a/sass.py +++ b/sass.py @@ -10,7 +10,9 @@ 'a b {\n color: blue; }\n' """ +from __future__ import absolute_import import collections +import inspect import os import os.path import re @@ -57,9 +59,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 +93,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') @@ -85,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``. @@ -274,13 +294,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 +319,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 +363,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: @@ -367,3 +396,98 @@ 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(collections.Mapping): + """Because sass maps can have mapping types as keys, we need an immutable + hashable mapping type. + """ + __slots__ = ('_dict', '_hash',) + + def __init__(self, *args, **kwargs): + self._dict = dict(*args, **kwargs) + # An assertion that all things are hashable + 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())) + + def __hash__(self): + return self._hash + + def _immutable(self, *_): + raise TypeError('SassMaps are immutable.') + + __setitem__ = __delitem__ = _immutable diff --git a/sasstests.py b/sasstests.py index e78432d3..42e48d99 100644 --- a/sasstests.py +++ b/sasstests.py @@ -735,6 +735,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.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.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.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.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.SassList( + ('foo', 'bar'), sass.SASS_SEPARATOR_COMMA, + ) + assert type(lst.items) is tuple, type(lst.items) + assert lst.separator is sass.SASS_SEPARATOR_COMMA, lst.separator + + def test_sass_list_conversion(self): + lst = sass.SassList( + ['foo', 'bar'], sass.SASS_SEPARATOR_SPACE, + ) + assert type(lst.items) is tuple, type(lst.items) + assert lst.separator is sass.SASS_SEPARATOR_SPACE, lst.separator + + def test_sass_warning_no_conversion(self): + 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.SassWarning(b'error msg') + assert type(warn.msg) is text_type, type(warn.msg) + + def test_sass_error_no_conversion(self): + err = sass.SassError(u'error msg') + assert type(err.msg) is text_type, type(err.msg) + + def test_sass_error_conversion(self): + err = sass.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.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, + 'returns_false': lambda: False, + 'returns_none': lambda: None, + 'returns_unicode': lambda: u'☃', + 'returns_bytes': lambda: u'☃'.encode('UTF-8'), + '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.SassList( + ('medium', 'none'), sass.SASS_SEPARATOR_SPACE, + ), + 'returns_py_dict': lambda: {'foo': 'bar'}, + 'returns_map': lambda: sass.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 +1129,9 @@ def test_error(self): DistutilsTestCase, SasscTestCase, CompileDirectoriesTest, + PrepareCustomFunctionListTest, + SassTypesTest, + CustomFunctionsTest, ] loader = unittest.defaultTestLoader suite = unittest.TestSuite() 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']