-
Notifications
You must be signed in to change notification settings - Fork 57
Importer callback support. #103
Changes from all commits
2dde3d5
7981dd5
2c1e233
bb75194
f65a640
0f733a9
d504dc2
164951b
c8f1fea
c032269
1d895a0
b780b23
78f526c
eaac1ef
66d5097
3852d21
5e5172e
92416a8
73a5a8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sweet, this'll use UTF-8 to decode the string: https://docs.python.org/2/c-api/unicode.html#c.PyUnicode_FromString (I had a concern this would use ascii like
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Indeed. That also goes for the format string I switched the |
||
| 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); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should test this case probably. The exception handling code was probably the hardest bit of the custom functions
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had unintentionally tested this by hand, but I'll add a proper case for it. |
||
|
|
||
| Py_XDECREF(exc); | ||
| Py_XDECREF(py_result); | ||
| return sass_imports; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. py_args leaks here |
||
| } | ||
|
|
||
| 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); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there's an additional error if the function returns nonsense here
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, that's something I couldn't easily find. What's the template for catching ParseTuple exceptions C-side?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Most (if not all) errors in the C-Api return |
||
|
|
||
| /* 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; | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. introduced some redspace here. I should really add http://pre-commit.com to this project at some point :)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Heh, we diverge on style guides in a few interesting ways, and yeah, "redspace" is one of them. I will, of course, ensure my changes conform to the local package style in the end. |
||
| 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); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| <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 | ||
| <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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's do some validation of this parameter here. We can also force them to be tuple of tuples and then the C code is safer (right now it'd crash if I pass in some other iterable type, let's test that too) |
||
|
|
||
| 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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, let's resolve this before shipping, should be pretty easy to combine with the above function