diff --git a/pysass.cpp b/pysass.cpp index 89484b75..01701182 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -303,6 +303,37 @@ static union Sass_Value* _exception_to_sass_error() { return retv; } +static PyObject* _exception_to_bytes() { + /* Grabs a Bytes instance for you to PySass_Bytes_AS_STRING. + Remember to Py_DECREF the object later! + TODO: This is a terrible violation of DRY, see above. + */ + PyObject* retv = NULL; + PyObject* etype = NULL; + PyObject* evalue = NULL; + PyObject* etb = NULL; + PyErr_Fetch(&etype, &evalue, &etb); + PyErr_NormalizeException(&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); + retv = PyUnicode_AsEncodedString(result, "UTF-8", "strict"); + Py_DECREF(traceback_mod); + Py_DECREF(traceback_parts); + Py_DECREF(joinstr); + Py_DECREF(result); + } + Py_DECREF(etype); + Py_DECREF(evalue); + Py_DECREF(etb); + return retv; +} + static union Sass_Value* _to_sass_value(PyObject* value) { union Sass_Value* retv = NULL; PyObject* types_mod = PyImport_ImportModule("sass"); @@ -404,6 +435,107 @@ static void _add_custom_functions( sass_option_set_c_functions(options, fn_list); } +Sass_Import_List _call_py_importer_f( + const char* path, + Sass_Importer_Entry cb, + struct Sass_Compiler* comp +) { + PyObject* pyfunc = (PyObject*)sass_importer_get_cookie(cb); + PyObject* py_path = PyUnicode_FromString(path); + PyObject* py_result = NULL; + PyObject *iterator; + PyObject *import_item; + Sass_Import_List sass_imports = NULL; + Py_ssize_t i; + + py_result = PyObject_CallObject(pyfunc, PySass_IF_PY3("y", "s"), py_path); + + if (!py_result) { + sass_imports = sass_make_import_list(1); + sass_imports[0] = sass_make_import_entry(path, 0, 0); + + PyObject* exc = _exception_to_bytes(); + char* err = PySass_Bytes_AS_STRING(exc); + + sass_import_set_error(sass_imports[0], + err, + 0, 0); + + Py_XDECREF(exc); + Py_XDECREF(py_result); + return sass_imports; + } + + if (py_result == Py_None) { + Py_XDECREF(py_result); + return 0; + } + + sass_imports = sass_make_import_list(PyList_Size(py_result)); + + iterator = PyObject_GetIter(py_result); + while (import_item = PyIter_Next(iterator)) { + char* path_str = NULL; /* XXX: Memory leak? */ + char* source_str = NULL; + char* sourcemap_str = NULL; + + /* TODO: Switch statement and error handling for default case. Better way? */ + if ( PyTuple_GET_SIZE(import_item) == 1 ) { + PyArg_ParseTuple(import_item, "es", + 0, &path_str); + } else if ( PyTuple_GET_SIZE(import_item) == 2 ) { + PyArg_ParseTuple(import_item, "eses", + 0, &path_str, 0, &source_str); + } else if ( PyTuple_GET_SIZE(import_item) == 3 ) { + PyArg_ParseTuple(import_item, "eseses", + 0, &path_str, 0, &source_str, 0, &sourcemap_str); + } + + /* We need to give copies of these arguments; libsass handles + deallocation of them later, whereas path_str is left flapping + in the breeze -- it's treated const, so that's okay. */ + if ( source_str ) source_str = strdup(source_str); + if ( sourcemap_str ) sourcemap_str = strdup(sourcemap_str); + + sass_imports[i] = sass_make_import_entry(path_str, source_str, sourcemap_str); + + Py_XDECREF(import_item); + } + + Py_XDECREF(iterator); + Py_XDECREF(py_result); + + return sass_imports; +} + +static void _add_custom_importers( + struct Sass_Options* options, PyObject* custom_importers +) { + Py_ssize_t i; + Sass_Importer_List importer_list; + + if ( custom_importers == Py_None ) { + return; + } + + importer_list = sass_make_importer_list(PyList_Size(custom_importers)); + + for (i = 0; i < PyList_GET_SIZE(custom_importers); i += 1) { + PyObject* item = PyList_GET_ITEM(custom_importers, i); + int priority = 0; + PyObject* import_function = NULL; + + PyArg_ParseTuple(item, "iO", + &priority, &import_function); + + importer_list[i] = sass_make_importer(_call_py_importer_f, + priority, + import_function); + } + + sass_option_set_c_importers(options, importer_list); +} + static PyObject * PySass_compile_string(PyObject *self, PyObject *args) { struct Sass_Context *ctx; @@ -414,13 +546,14 @@ PySass_compile_string(PyObject *self, PyObject *args) { Sass_Output_Style output_style; int source_comments, error_status, precision, indented; PyObject *custom_functions; + PyObject *custom_importers; PyObject *result; - + if (!PyArg_ParseTuple(args, - PySass_IF_PY3("yiiyiOi", "siisiOi"), + PySass_IF_PY3("yiiyiOiO", "siisiOiO"), &string, &output_style, &source_comments, &include_paths, &precision, - &custom_functions, &indented)) { + &custom_functions, &indented, &custom_importers)) { return NULL; } @@ -432,7 +565,8 @@ PySass_compile_string(PyObject *self, PyObject *args) { sass_option_set_precision(options, precision); sass_option_set_is_indented_syntax_src(options, indented); _add_custom_functions(options, custom_functions); - + _add_custom_importers(options, custom_importers); + sass_compile_data_context(context); ctx = sass_data_context_get_context(context); @@ -444,6 +578,7 @@ PySass_compile_string(PyObject *self, PyObject *args) { (short int) !error_status, error_status ? error_message : output_string ); + sass_delete_data_context(context); return result; } @@ -457,13 +592,15 @@ 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, *custom_functions, *result; - + PyObject *source_map_filename, *custom_functions, *custom_importers, + *result; + if (!PyArg_ParseTuple(args, - PySass_IF_PY3("yiiyiOO", "siisiOO"), + PySass_IF_PY3("yiiyiOOO", "siisiOOO"), &filename, &output_style, &source_comments, &include_paths, &precision, - &source_map_filename, &custom_functions)) { + &source_map_filename, &custom_functions, + &custom_importers)) { return NULL; } @@ -487,6 +624,7 @@ PySass_compile_filename(PyObject *self, PyObject *args) { sass_option_set_include_path(options, include_paths); sass_option_set_precision(options, precision); _add_custom_functions(options, custom_functions); + _add_custom_importers(options, custom_importers); sass_compile_file_context(context); diff --git a/sass.py b/sass.py index bf2498fd..b8c80425 100644 --- a/sass.py +++ b/sass.py @@ -146,7 +146,7 @@ def __str__(self): def compile_dirname( search_path, output_path, output_style, source_comments, include_paths, - precision, custom_functions, + precision, custom_functions, importers ): fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding() for dirpath, _, filenames in os.walk(search_path): @@ -163,7 +163,7 @@ def compile_dirname( input_filename = input_filename.encode(fs_encoding) s, v, _ = compile_filename( input_filename, output_style, source_comments, include_paths, - precision, None, custom_functions, + precision, None, custom_functions, importers ) if s: v = v.decode('UTF-8') @@ -209,6 +209,10 @@ def compile(**kwargs): formatted. :const:`False` by default :type indented: :class:`bool` :returns: the compiled CSS string + :param importers: optional callback functions. + see also below `importer callbacks + `_ description + :type importers: :class:`collections.Callable` :rtype: :class:`str` :raises sass.CompileError: when it fails for any reason (for example the given SASS has broken syntax) @@ -243,6 +247,10 @@ def compile(**kwargs): :type custom_functions: :class:`collections.Set`, :class:`collections.Sequence`, :class:`collections.Mapping` + :param importers: optional callback functions. + see also below `importer callbacks + `_ description + :type importers: :class:`collections.Callable` :returns: the compiled CSS string, or a pair of the compiled CSS string and the source map string if ``source_comments='map'`` :rtype: :class:`str`, :class:`tuple` @@ -337,6 +345,49 @@ def func_name(a, b): custom_functions={func_name} ) + .. _importer-callbacks: + + Newer versions of ``libsass`` allow developers to define callbacks to be + called and given a chance to process ``@import`` directives. You can + define yours by passing in a list of callables via the ``importers`` + parameter. The callables must be passed as 2-tuples in the form: + + .. code-block:: python + + (priority_int, callback_fn) + + A priority of zero is acceptable; priority determines the order callbacks + are attempted. + + These callbacks must accept a single string argument representing the path + passed to the ``@import`` directive, and either return ``None`` to + indicate the path wasn't handled by that callback (to continue with others + or fall back on internal ``libsass`` filesystem behaviour) or a list of + one or more tuples, each in one of three forms: + + * A 1-tuple representing an alternate path to handle internally; or, + * A 2-tuple representing an alternate path and the content that path + represents; or, + * A 3-tuple representing the same as the 2-tuple with the addition of a + "sourcemap". + + All tuple return values must be strings. As a not overly realistic + example: + + .. code-block:: python + + def my_importer(path): + return [(path, '#' + path + ' { color: red; }')] + + sass.compile( + ..., + importers=[(0, my_importer)] + ) + + Now, within the style source, attempting to ``@import 'button';`` will + instead attach ``color: red`` as a property of an element with the + imported name. + .. versionadded:: 0.4.0 Added ``source_comments`` and ``source_map_filename`` parameters. @@ -448,6 +499,8 @@ def func_name(a, b): 'not {1!r}'.format(SassFunction, custom_functions) ) + importers = kwargs.pop('importers', None) + if 'string' in modes: string = kwargs.pop('string') if isinstance(string, text_type): @@ -458,7 +511,7 @@ def func_name(a, b): repr(source_comments)) s, v = compile_string( string, output_style, source_comments, include_paths, precision, - custom_functions, indented + custom_functions, indented, importers ) if s: return v.decode('utf-8') @@ -472,7 +525,7 @@ def func_name(a, b): filename = filename.encode(fs_encoding) s, v, source_map = compile_filename( filename, output_style, source_comments, include_paths, precision, - source_map_filename, custom_functions, + source_map_filename, custom_functions, importers ) if s: v = v.decode('utf-8') @@ -517,7 +570,7 @@ def func_name(a, b): 'output_dir)') s, v = compile_dirname( search_path, output_path, output_style, source_comments, - include_paths, precision, custom_functions, + include_paths, precision, custom_functions, importers ) if s: return diff --git a/sasstests.py b/sasstests.py index 3829e618..c7b06ee5 100644 --- a/sasstests.py +++ b/sasstests.py @@ -261,6 +261,29 @@ def test_compile_string_sass_style(self): indented=True) assert actual == 'a b {\n color: blue; }\n' + # TODO: Test "nop" (return None) handling. Pseudo-code. + def test_compile_string_with_importer_callback(self): + def importer_callback(path): + return [ + (path, '#' + path + ' { color: blue; }\n'), + (path, '.' + path + ' { color: red; }\n') + ] + + source = '''@import 'button'; + a { color: green; }''' + + actual = sass.compile(string=source, + importers=[(0, importer_callback)]) + assert actual == """#button { + color: blue; } + +#button { + color: blue; } + +a { + color: green; } +""" + def test_compile_string_deprecated_source_comments_line_numbers(self): source = '''a { b { color: blue; }