Skip to content
This repository was archived by the owner on Oct 24, 2025. It is now read-only.

Commit ea9a7c9

Browse files
amcgregorasottile
authored andcommitted
Importer callbacks (Previous commits)
1 parent 797a571 commit ea9a7c9

File tree

3 files changed

+227
-13
lines changed

3 files changed

+227
-13
lines changed

pysass.cpp

Lines changed: 146 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,37 @@ static union Sass_Value* _exception_to_sass_error() {
303303
return retv;
304304
}
305305

306+
static PyObject* _exception_to_bytes() {
307+
/* Grabs a Bytes instance for you to PySass_Bytes_AS_STRING.
308+
Remember to Py_DECREF the object later!
309+
TODO: This is a terrible violation of DRY, see above.
310+
*/
311+
PyObject* retv = NULL;
312+
PyObject* etype = NULL;
313+
PyObject* evalue = NULL;
314+
PyObject* etb = NULL;
315+
PyErr_Fetch(&etype, &evalue, &etb);
316+
PyErr_NormalizeException(&etype, &evalue, &etb);
317+
{
318+
PyObject* traceback_mod = PyImport_ImportModule("traceback");
319+
PyObject* traceback_parts = PyObject_CallMethod(
320+
traceback_mod, "format_exception", "OOO", etype, evalue, etb
321+
);
322+
PyList_Insert(traceback_parts, 0, PyUnicode_FromString("\n"));
323+
PyObject* joinstr = PyUnicode_FromString("");
324+
PyObject* result = PyUnicode_Join(joinstr, traceback_parts);
325+
retv = PyUnicode_AsEncodedString(result, "UTF-8", "strict");
326+
Py_DECREF(traceback_mod);
327+
Py_DECREF(traceback_parts);
328+
Py_DECREF(joinstr);
329+
Py_DECREF(result);
330+
}
331+
Py_DECREF(etype);
332+
Py_DECREF(evalue);
333+
Py_DECREF(etb);
334+
return retv;
335+
}
336+
306337
static union Sass_Value* _to_sass_value(PyObject* value) {
307338
union Sass_Value* retv = NULL;
308339
PyObject* types_mod = PyImport_ImportModule("sass");
@@ -404,6 +435,107 @@ static void _add_custom_functions(
404435
sass_option_set_c_functions(options, fn_list);
405436
}
406437

438+
Sass_Import_List _call_py_importer_f(
439+
const char* path,
440+
Sass_Importer_Entry cb,
441+
struct Sass_Compiler* comp
442+
) {
443+
PyObject* pyfunc = (PyObject*)sass_importer_get_cookie(cb);
444+
PyObject* py_path = PyUnicode_FromString(path);
445+
PyObject* py_result = NULL;
446+
PyObject *iterator;
447+
PyObject *import_item;
448+
Sass_Import_List sass_imports = NULL;
449+
Py_ssize_t i;
450+
451+
py_result = PyObject_CallObject(pyfunc, PySass_IF_PY3("y", "s"), py_path);
452+
453+
if (!py_result) {
454+
sass_imports = sass_make_import_list(1);
455+
sass_imports[0] = sass_make_import_entry(path, 0, 0);
456+
457+
PyObject* exc = _exception_to_bytes();
458+
char* err = PySass_Bytes_AS_STRING(exc);
459+
460+
sass_import_set_error(sass_imports[0],
461+
err,
462+
0, 0);
463+
464+
Py_XDECREF(exc);
465+
Py_XDECREF(py_result);
466+
return sass_imports;
467+
}
468+
469+
if (py_result == Py_None) {
470+
Py_XDECREF(py_result);
471+
return 0;
472+
}
473+
474+
sass_imports = sass_make_import_list(PyList_Size(py_result));
475+
476+
iterator = PyObject_GetIter(py_result);
477+
while (import_item = PyIter_Next(iterator)) {
478+
char* path_str = NULL; /* XXX: Memory leak? */
479+
char* source_str = NULL;
480+
char* sourcemap_str = NULL;
481+
482+
/* TODO: Switch statement and error handling for default case. Better way? */
483+
if ( PyTuple_GET_SIZE(import_item) == 1 ) {
484+
PyArg_ParseTuple(import_item, "es",
485+
0, &path_str);
486+
} else if ( PyTuple_GET_SIZE(import_item) == 2 ) {
487+
PyArg_ParseTuple(import_item, "eses",
488+
0, &path_str, 0, &source_str);
489+
} else if ( PyTuple_GET_SIZE(import_item) == 3 ) {
490+
PyArg_ParseTuple(import_item, "eseses",
491+
0, &path_str, 0, &source_str, 0, &sourcemap_str);
492+
}
493+
494+
/* We need to give copies of these arguments; libsass handles
495+
deallocation of them later, whereas path_str is left flapping
496+
in the breeze -- it's treated const, so that's okay. */
497+
if ( source_str ) source_str = strdup(source_str);
498+
if ( sourcemap_str ) sourcemap_str = strdup(sourcemap_str);
499+
500+
sass_imports[i] = sass_make_import_entry(path_str, source_str, sourcemap_str);
501+
502+
Py_XDECREF(import_item);
503+
}
504+
505+
Py_XDECREF(iterator);
506+
Py_XDECREF(py_result);
507+
508+
return sass_imports;
509+
}
510+
511+
static void _add_custom_importers(
512+
struct Sass_Options* options, PyObject* custom_importers
513+
) {
514+
Py_ssize_t i;
515+
Sass_Importer_List importer_list;
516+
517+
if ( custom_importers == Py_None ) {
518+
return;
519+
}
520+
521+
importer_list = sass_make_importer_list(PyList_Size(custom_importers));
522+
523+
for (i = 0; i < PyList_GET_SIZE(custom_importers); i += 1) {
524+
PyObject* item = PyList_GET_ITEM(custom_importers, i);
525+
int priority = 0;
526+
PyObject* import_function = NULL;
527+
528+
PyArg_ParseTuple(item, "iO",
529+
&priority, &import_function);
530+
531+
importer_list[i] = sass_make_importer(_call_py_importer_f,
532+
priority,
533+
import_function);
534+
}
535+
536+
sass_option_set_c_importers(options, importer_list);
537+
}
538+
407539
static PyObject *
408540
PySass_compile_string(PyObject *self, PyObject *args) {
409541
struct Sass_Context *ctx;
@@ -414,13 +546,14 @@ PySass_compile_string(PyObject *self, PyObject *args) {
414546
Sass_Output_Style output_style;
415547
int source_comments, error_status, precision, indented;
416548
PyObject *custom_functions;
549+
PyObject *custom_importers;
417550
PyObject *result;
418-
551+
419552
if (!PyArg_ParseTuple(args,
420-
PySass_IF_PY3("yiiyiOi", "siisiOi"),
553+
PySass_IF_PY3("yiiyiOiO", "siisiOiO"),
421554
&string, &output_style, &source_comments,
422555
&include_paths, &precision,
423-
&custom_functions, &indented)) {
556+
&custom_functions, &indented, &custom_importers)) {
424557
return NULL;
425558
}
426559

@@ -432,7 +565,8 @@ PySass_compile_string(PyObject *self, PyObject *args) {
432565
sass_option_set_precision(options, precision);
433566
sass_option_set_is_indented_syntax_src(options, indented);
434567
_add_custom_functions(options, custom_functions);
435-
568+
_add_custom_importers(options, custom_importers);
569+
436570
sass_compile_data_context(context);
437571

438572
ctx = sass_data_context_get_context(context);
@@ -444,6 +578,7 @@ PySass_compile_string(PyObject *self, PyObject *args) {
444578
(short int) !error_status,
445579
error_status ? error_message : output_string
446580
);
581+
447582
sass_delete_data_context(context);
448583
return result;
449584
}
@@ -457,13 +592,15 @@ PySass_compile_filename(PyObject *self, PyObject *args) {
457592
const char *error_message, *output_string, *source_map_string;
458593
Sass_Output_Style output_style;
459594
int source_comments, error_status, precision;
460-
PyObject *source_map_filename, *custom_functions, *result;
461-
595+
PyObject *source_map_filename, *custom_functions, *custom_importers,
596+
*result;
597+
462598
if (!PyArg_ParseTuple(args,
463-
PySass_IF_PY3("yiiyiOO", "siisiOO"),
599+
PySass_IF_PY3("yiiyiOOO", "siisiOOO"),
464600
&filename, &output_style, &source_comments,
465601
&include_paths, &precision,
466-
&source_map_filename, &custom_functions)) {
602+
&source_map_filename, &custom_functions,
603+
&custom_importers)) {
467604
return NULL;
468605
}
469606

@@ -487,6 +624,7 @@ PySass_compile_filename(PyObject *self, PyObject *args) {
487624
sass_option_set_include_path(options, include_paths);
488625
sass_option_set_precision(options, precision);
489626
_add_custom_functions(options, custom_functions);
627+
_add_custom_importers(options, custom_importers);
490628

491629
sass_compile_file_context(context);
492630

sass.py

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ def __str__(self):
146146

147147
def compile_dirname(
148148
search_path, output_path, output_style, source_comments, include_paths,
149-
precision, custom_functions,
149+
precision, custom_functions, importers
150150
):
151151
fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
152152
for dirpath, _, filenames in os.walk(search_path):
@@ -163,7 +163,7 @@ def compile_dirname(
163163
input_filename = input_filename.encode(fs_encoding)
164164
s, v, _ = compile_filename(
165165
input_filename, output_style, source_comments, include_paths,
166-
precision, None, custom_functions,
166+
precision, None, custom_functions, importers
167167
)
168168
if s:
169169
v = v.decode('UTF-8')
@@ -219,6 +219,10 @@ def compile(**kwargs):
219219
formatted. :const:`False` by default
220220
:type indented: :class:`bool`
221221
:returns: the compiled CSS string
222+
:param importers: optional callback functions.
223+
see also below `importer callbacks
224+
<importer-callbacks>`_ description
225+
:type importers: :class:`collections.Callable`
222226
:rtype: :class:`str`
223227
:raises sass.CompileError: when it fails for any reason
224228
(for example the given SASS has broken syntax)
@@ -253,6 +257,10 @@ def compile(**kwargs):
253257
:type custom_functions: :class:`collections.Set`,
254258
:class:`collections.Sequence`,
255259
:class:`collections.Mapping`
260+
:param importers: optional callback functions.
261+
see also below `importer callbacks
262+
<importer-callbacks>`_ description
263+
:type importers: :class:`collections.Callable`
256264
:returns: the compiled CSS string, or a pair of the compiled CSS string
257265
and the source map string if ``source_comments='map'``
258266
:rtype: :class:`str`, :class:`tuple`
@@ -347,6 +355,49 @@ def func_name(a, b):
347355
custom_functions={func_name}
348356
)
349357
358+
.. _importer-callbacks:
359+
360+
Newer versions of ``libsass`` allow developers to define callbacks to be
361+
called and given a chance to process ``@import`` directives. You can
362+
define yours by passing in a list of callables via the ``importers``
363+
parameter. The callables must be passed as 2-tuples in the form:
364+
365+
.. code-block:: python
366+
367+
(priority_int, callback_fn)
368+
369+
A priority of zero is acceptable; priority determines the order callbacks
370+
are attempted.
371+
372+
These callbacks must accept a single string argument representing the path
373+
passed to the ``@import`` directive, and either return ``None`` to
374+
indicate the path wasn't handled by that callback (to continue with others
375+
or fall back on internal ``libsass`` filesystem behaviour) or a list of
376+
one or more tuples, each in one of three forms:
377+
378+
* A 1-tuple representing an alternate path to handle internally; or,
379+
* A 2-tuple representing an alternate path and the content that path
380+
represents; or,
381+
* A 3-tuple representing the same as the 2-tuple with the addition of a
382+
"sourcemap".
383+
384+
All tuple return values must be strings. As a not overly realistic
385+
example:
386+
387+
.. code-block:: python
388+
389+
def my_importer(path):
390+
return [(path, '#' + path + ' { color: red; }')]
391+
392+
sass.compile(
393+
...,
394+
importers=[(0, my_importer)]
395+
)
396+
397+
Now, within the style source, attempting to ``@import 'button';`` will
398+
instead attach ``color: red`` as a property of an element with the
399+
imported name.
400+
350401
.. versionadded:: 0.4.0
351402
Added ``source_comments`` and ``source_map_filename`` parameters.
352403
@@ -458,6 +509,8 @@ def func_name(a, b):
458509
'not {1!r}'.format(SassFunction, custom_functions)
459510
)
460511

512+
importers = kwargs.pop('importers', None)
513+
461514
if 'string' in modes:
462515
string = kwargs.pop('string')
463516
if isinstance(string, text_type):
@@ -469,7 +522,7 @@ def func_name(a, b):
469522
_check_no_remaining_kwargs(compile, kwargs)
470523
s, v = compile_string(
471524
string, output_style, source_comments, include_paths, precision,
472-
custom_functions, indented,
525+
custom_functions, indented, importers,
473526
)
474527
if s:
475528
return v.decode('utf-8')
@@ -484,7 +537,7 @@ def func_name(a, b):
484537
_check_no_remaining_kwargs(compile, kwargs)
485538
s, v, source_map = compile_filename(
486539
filename, output_style, source_comments, include_paths, precision,
487-
source_map_filename, custom_functions,
540+
source_map_filename, custom_functions, importers
488541
)
489542
if s:
490543
v = v.decode('utf-8')
@@ -530,7 +583,7 @@ def func_name(a, b):
530583
_check_no_remaining_kwargs(compile, kwargs)
531584
s, v = compile_dirname(
532585
search_path, output_path, output_style, source_comments,
533-
include_paths, precision, custom_functions,
586+
include_paths, precision, custom_functions, importers
534587
)
535588
if s:
536589
return

sasstests.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,29 @@ def test_compile_string_sass_style(self):
274274
indented=True)
275275
assert actual == 'a b {\n color: blue; }\n'
276276

277+
# TODO: Test "nop" (return None) handling. Pseudo-code.
278+
def test_compile_string_with_importer_callback(self):
279+
def importer_callback(path):
280+
return [
281+
(path, '#' + path + ' { color: blue; }\n'),
282+
(path, '.' + path + ' { color: red; }\n')
283+
]
284+
285+
source = '''@import 'button';
286+
a { color: green; }'''
287+
288+
actual = sass.compile(string=source,
289+
importers=[(0, importer_callback)])
290+
assert actual == """#button {
291+
color: blue; }
292+
293+
#button {
294+
color: blue; }
295+
296+
a {
297+
color: green; }
298+
"""
299+
277300
def test_compile_string_deprecated_source_comments_line_numbers(self):
278301
source = '''a {
279302
b { color: blue; }

0 commit comments

Comments
 (0)