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'] 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