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

Commit 13eb3f8

Browse files
committed
Merge pull request #110 from asottile/importer_callbacks
Importer callbacks
2 parents 797a571 + f3acec5 commit 13eb3f8

File tree

4 files changed

+361
-20
lines changed

4 files changed

+361
-20
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ build2/libsass/cpp/%.o: libsass/src/%.cpp
2626

2727
build2/pysass.o: pysass.cpp
2828
@mkdir -p build2
29-
gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fPIC -I./libsass/include $(PY_HEADERS) -c $^ -o $@ -c -O2 -fPIC -std=c++0x -Wall -Wno-parentheses
29+
gcc -pthread -fno-strict-aliasing -Wno-write-strings -DNDEBUG -g -fwrapv -O2 -Wall -fPIC -I./libsass/include $(PY_HEADERS) -c $^ -o $@ -c -O2 -fPIC -std=c++0x -Wall -Wno-parentheses
3030

3131
_sass.so: $(C_OBJECTS) $(CPP_OBJECTS) build2/pysass.o
3232
g++ -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-Bsymbolic-functions -Wl,-z,relro $^ -L./libsass -o $@ -fPIC -lstdc++

pysass.cpp

Lines changed: 119 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,8 @@ static union Sass_Value* _unknown_type_to_sass_error(PyObject* value) {
272272
return retv;
273273
}
274274

275-
static union Sass_Value* _exception_to_sass_error() {
276-
union Sass_Value* retv = NULL;
275+
static PyObject* _exception_to_bytes() {
276+
PyObject* retv = NULL;
277277
PyObject* etype = NULL;
278278
PyObject* evalue = NULL;
279279
PyObject* etb = NULL;
@@ -287,22 +287,34 @@ static union Sass_Value* _exception_to_sass_error() {
287287
PyList_Insert(traceback_parts, 0, PyUnicode_FromString("\n"));
288288
PyObject* joinstr = PyUnicode_FromString("");
289289
PyObject* result = PyUnicode_Join(joinstr, traceback_parts);
290-
PyObject* bytes = PyUnicode_AsEncodedString(
291-
result, "UTF-8", "strict"
292-
);
293-
retv = sass_make_error(PySass_Bytes_AS_STRING(bytes));
290+
retv = PyUnicode_AsEncodedString(result, "UTF-8", "strict");
294291
Py_DECREF(traceback_mod);
295292
Py_DECREF(traceback_parts);
296293
Py_DECREF(joinstr);
297294
Py_DECREF(result);
298-
Py_DECREF(bytes);
299295
}
300296
Py_DECREF(etype);
301297
Py_DECREF(evalue);
302298
Py_DECREF(etb);
303299
return retv;
304300
}
305301

302+
static union Sass_Value* _exception_to_sass_error() {
303+
PyObject* bytes = _exception_to_bytes();
304+
union Sass_Value* retv = sass_make_error(PySass_Bytes_AS_STRING(bytes));
305+
Py_DECREF(bytes);
306+
return retv;
307+
}
308+
309+
static Sass_Import_List _exception_to_sass_import_error(const char* path) {
310+
PyObject* bytes = _exception_to_bytes();
311+
Sass_Import_List import_list = sass_make_import_list(1);
312+
import_list[0] = sass_make_import_entry(path, 0, 0);
313+
sass_import_set_error(import_list[0], PySass_Bytes_AS_STRING(bytes), 0, 0);
314+
Py_DECREF(bytes);
315+
return import_list;
316+
}
317+
306318
static union Sass_Value* _to_sass_value(PyObject* value) {
307319
union Sass_Value* retv = NULL;
308320
PyObject* types_mod = PyImport_ImportModule("sass");
@@ -404,6 +416,96 @@ static void _add_custom_functions(
404416
sass_option_set_c_functions(options, fn_list);
405417
}
406418

419+
static Sass_Import_List _call_py_importer_f(
420+
const char* path, Sass_Importer_Entry cb, struct Sass_Compiler* comp
421+
) {
422+
PyObject* pyfunc = (PyObject*)sass_importer_get_cookie(cb);
423+
PyObject* py_result = NULL;
424+
Sass_Import_List sass_imports = NULL;
425+
Py_ssize_t i;
426+
427+
py_result = PyObject_CallFunction(pyfunc, PySass_IF_PY3("y", "s"), path);
428+
429+
/* Handle importer throwing an exception */
430+
if (!py_result) goto done;
431+
432+
/* Could return None indicating it could not handle the import */
433+
if (py_result == Py_None) {
434+
Py_XDECREF(py_result);
435+
return NULL;
436+
}
437+
438+
/* Otherwise, we know our importer is well formed (because we wrap it)
439+
* The return value will be a tuple of 1, 2, or 3 tuples */
440+
sass_imports = sass_make_import_list(PyTuple_GET_SIZE(py_result));
441+
for (i = 0; i < PyTuple_GET_SIZE(py_result); i += 1) {
442+
char* path_str = NULL; /* XXX: Memory leak? */
443+
char* source_str = NULL;
444+
char* sourcemap_str = NULL;
445+
PyObject* tup = PyTuple_GET_ITEM(py_result, i);
446+
Py_ssize_t size = PyTuple_GET_SIZE(tup);
447+
448+
if (size == 1) {
449+
PyArg_ParseTuple(tup, PySass_IF_PY3("y", "s"), &path_str);
450+
} else if (size == 2) {
451+
PyArg_ParseTuple(
452+
tup, PySass_IF_PY3("yy", "ss"), &path_str, &source_str
453+
);
454+
} else if (size == 3) {
455+
PyArg_ParseTuple(
456+
tup, PySass_IF_PY3("yyy", "sss"),
457+
&path_str, &source_str, &sourcemap_str
458+
);
459+
}
460+
461+
/* We need to give copies of these arguments; libsass handles
462+
* deallocation of them later, whereas path_str is left flapping
463+
* in the breeze -- it's treated const, so that's okay. */
464+
if (source_str) source_str = strdup(source_str);
465+
if (sourcemap_str) sourcemap_str = strdup(sourcemap_str);
466+
467+
sass_imports[i] = sass_make_import_entry(
468+
path_str, source_str, sourcemap_str
469+
);
470+
}
471+
472+
done:
473+
if (sass_imports == NULL) {
474+
sass_imports = _exception_to_sass_import_error(path);
475+
}
476+
477+
Py_XDECREF(py_result);
478+
479+
return sass_imports;
480+
}
481+
482+
static void _add_custom_importers(
483+
struct Sass_Options* options, PyObject* custom_importers
484+
) {
485+
Py_ssize_t i;
486+
Sass_Importer_List importer_list;
487+
488+
if (custom_importers == Py_None) {
489+
return;
490+
}
491+
492+
importer_list = sass_make_importer_list(PyTuple_GET_SIZE(custom_importers));
493+
494+
for (i = 0; i < PyTuple_GET_SIZE(custom_importers); i += 1) {
495+
PyObject* item = PyTuple_GET_ITEM(custom_importers, i);
496+
int priority = 0;
497+
PyObject* import_function = NULL;
498+
499+
PyArg_ParseTuple(item, "iO", &priority, &import_function);
500+
501+
importer_list[i] = sass_make_importer(
502+
_call_py_importer_f, priority, import_function
503+
);
504+
}
505+
506+
sass_option_set_c_importers(options, importer_list);
507+
}
508+
407509
static PyObject *
408510
PySass_compile_string(PyObject *self, PyObject *args) {
409511
struct Sass_Context *ctx;
@@ -414,13 +516,14 @@ PySass_compile_string(PyObject *self, PyObject *args) {
414516
Sass_Output_Style output_style;
415517
int source_comments, error_status, precision, indented;
416518
PyObject *custom_functions;
519+
PyObject *custom_importers;
417520
PyObject *result;
418521

419522
if (!PyArg_ParseTuple(args,
420-
PySass_IF_PY3("yiiyiOi", "siisiOi"),
523+
PySass_IF_PY3("yiiyiOiO", "siisiOiO"),
421524
&string, &output_style, &source_comments,
422525
&include_paths, &precision,
423-
&custom_functions, &indented)) {
526+
&custom_functions, &indented, &custom_importers)) {
424527
return NULL;
425528
}
426529

@@ -432,7 +535,7 @@ PySass_compile_string(PyObject *self, PyObject *args) {
432535
sass_option_set_precision(options, precision);
433536
sass_option_set_is_indented_syntax_src(options, indented);
434537
_add_custom_functions(options, custom_functions);
435-
538+
_add_custom_importers(options, custom_importers);
436539
sass_compile_data_context(context);
437540

438541
ctx = sass_data_context_get_context(context);
@@ -457,13 +560,15 @@ PySass_compile_filename(PyObject *self, PyObject *args) {
457560
const char *error_message, *output_string, *source_map_string;
458561
Sass_Output_Style output_style;
459562
int source_comments, error_status, precision;
460-
PyObject *source_map_filename, *custom_functions, *result;
563+
PyObject *source_map_filename, *custom_functions, *custom_importers,
564+
*result;
461565

462566
if (!PyArg_ParseTuple(args,
463-
PySass_IF_PY3("yiiyiOO", "siisiOO"),
567+
PySass_IF_PY3("yiiyiOOO", "siisiOOO"),
464568
&filename, &output_style, &source_comments,
465569
&include_paths, &precision,
466-
&source_map_filename, &custom_functions)) {
570+
&source_map_filename, &custom_functions,
571+
&custom_importers)) {
467572
return NULL;
468573
}
469574

@@ -487,7 +592,7 @@ PySass_compile_filename(PyObject *self, PyObject *args) {
487592
sass_option_set_include_path(options, include_paths);
488593
sass_option_set_precision(options, precision);
489594
_add_custom_functions(options, custom_functions);
490-
595+
_add_custom_importers(options, custom_importers);
491596
sass_compile_file_context(context);
492597

493598
ctx = sass_file_context_get_context(context);

sass.py

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from __future__ import absolute_import
1414

1515
import collections
16+
import functools
1617
import inspect
1718
from io import open
1819
import os
@@ -144,9 +145,59 @@ def __str__(self):
144145
return self.signature
145146

146147

148+
def _normalize_importer_return_value(result):
149+
# An importer must return an iterable of iterables of 1-3 stringlike
150+
# objects
151+
if result is None:
152+
return result
153+
154+
def _to_importer_result(single_result):
155+
single_result = tuple(single_result)
156+
if len(single_result) not in (1, 2, 3):
157+
raise ValueError(
158+
'Expected importer result to be a tuple of length (1, 2, 3) '
159+
'but got {0}: {1!r}'.format(len(single_result), single_result)
160+
)
161+
162+
def _to_bytes(obj):
163+
if not isinstance(obj, bytes):
164+
return obj.encode('UTF-8')
165+
else:
166+
return obj
167+
168+
return tuple(_to_bytes(s) for s in single_result)
169+
170+
return tuple(_to_importer_result(x) for x in result)
171+
172+
173+
def _importer_callback_wrapper(func):
174+
@functools.wraps(func)
175+
def inner(path):
176+
ret = func(path.decode('UTF-8'))
177+
return _normalize_importer_return_value(ret)
178+
return inner
179+
180+
181+
def _validate_importers(importers):
182+
"""Validates the importers and decorates the callables with our output
183+
formatter.
184+
"""
185+
# They could have no importers, that's chill
186+
if importers is None:
187+
return None
188+
189+
def _to_importer(priority, func):
190+
assert isinstance(priority, int), priority
191+
assert callable(func), func
192+
return (priority, _importer_callback_wrapper(func))
193+
194+
# Our code assumes tuple of tuples
195+
return tuple(_to_importer(priority, func) for priority, func in importers)
196+
197+
147198
def compile_dirname(
148199
search_path, output_path, output_style, source_comments, include_paths,
149-
precision, custom_functions,
200+
precision, custom_functions, importers
150201
):
151202
fs_encoding = sys.getfilesystemencoding() or sys.getdefaultencoding()
152203
for dirpath, _, filenames in os.walk(search_path):
@@ -163,7 +214,7 @@ def compile_dirname(
163214
input_filename = input_filename.encode(fs_encoding)
164215
s, v, _ = compile_filename(
165216
input_filename, output_style, source_comments, include_paths,
166-
precision, None, custom_functions,
217+
precision, None, custom_functions, importers
167218
)
168219
if s:
169220
v = v.decode('UTF-8')
@@ -219,6 +270,10 @@ def compile(**kwargs):
219270
formatted. :const:`False` by default
220271
:type indented: :class:`bool`
221272
:returns: the compiled CSS string
273+
:param importers: optional callback functions.
274+
see also below `importer callbacks
275+
<importer-callbacks>`_ description
276+
:type importers: :class:`collections.Callable`
222277
:rtype: :class:`str`
223278
:raises sass.CompileError: when it fails for any reason
224279
(for example the given SASS has broken syntax)
@@ -253,6 +308,10 @@ def compile(**kwargs):
253308
:type custom_functions: :class:`collections.Set`,
254309
:class:`collections.Sequence`,
255310
:class:`collections.Mapping`
311+
:param importers: optional callback functions.
312+
see also below `importer callbacks
313+
<importer-callbacks>`_ description
314+
:type importers: :class:`collections.Callable`
256315
:returns: the compiled CSS string, or a pair of the compiled CSS string
257316
and the source map string if ``source_comments='map'``
258317
:rtype: :class:`str`, :class:`tuple`
@@ -347,6 +406,49 @@ def func_name(a, b):
347406
custom_functions={func_name}
348407
)
349408
409+
.. _importer-callbacks:
410+
411+
Newer versions of ``libsass`` allow developers to define callbacks to be
412+
called and given a chance to process ``@import`` directives. You can
413+
define yours by passing in a list of callables via the ``importers``
414+
parameter. The callables must be passed as 2-tuples in the form:
415+
416+
.. code-block:: python
417+
418+
(priority_int, callback_fn)
419+
420+
A priority of zero is acceptable; priority determines the order callbacks
421+
are attempted.
422+
423+
These callbacks must accept a single string argument representing the path
424+
passed to the ``@import`` directive, and either return ``None`` to
425+
indicate the path wasn't handled by that callback (to continue with others
426+
or fall back on internal ``libsass`` filesystem behaviour) or a list of
427+
one or more tuples, each in one of three forms:
428+
429+
* A 1-tuple representing an alternate path to handle internally; or,
430+
* A 2-tuple representing an alternate path and the content that path
431+
represents; or,
432+
* A 3-tuple representing the same as the 2-tuple with the addition of a
433+
"sourcemap".
434+
435+
All tuple return values must be strings. As a not overly realistic
436+
example:
437+
438+
.. code-block:: python
439+
440+
def my_importer(path):
441+
return [(path, '#' + path + ' { color: red; }')]
442+
443+
sass.compile(
444+
...,
445+
importers=[(0, my_importer)]
446+
)
447+
448+
Now, within the style source, attempting to ``@import 'button';`` will
449+
instead attach ``color: red`` as a property of an element with the
450+
imported name.
451+
350452
.. versionadded:: 0.4.0
351453
Added ``source_comments`` and ``source_map_filename`` parameters.
352454
@@ -458,6 +560,8 @@ def func_name(a, b):
458560
'not {1!r}'.format(SassFunction, custom_functions)
459561
)
460562

563+
importers = _validate_importers(kwargs.pop('importers', None))
564+
461565
if 'string' in modes:
462566
string = kwargs.pop('string')
463567
if isinstance(string, text_type):
@@ -469,7 +573,7 @@ def func_name(a, b):
469573
_check_no_remaining_kwargs(compile, kwargs)
470574
s, v = compile_string(
471575
string, output_style, source_comments, include_paths, precision,
472-
custom_functions, indented,
576+
custom_functions, indented, importers,
473577
)
474578
if s:
475579
return v.decode('utf-8')
@@ -484,7 +588,7 @@ def func_name(a, b):
484588
_check_no_remaining_kwargs(compile, kwargs)
485589
s, v, source_map = compile_filename(
486590
filename, output_style, source_comments, include_paths, precision,
487-
source_map_filename, custom_functions,
591+
source_map_filename, custom_functions, importers,
488592
)
489593
if s:
490594
v = v.decode('utf-8')
@@ -530,7 +634,7 @@ def func_name(a, b):
530634
_check_no_remaining_kwargs(compile, kwargs)
531635
s, v = compile_dirname(
532636
search_path, output_path, output_style, source_comments,
533-
include_paths, precision, custom_functions,
637+
include_paths, precision, custom_functions, importers,
534638
)
535639
if s:
536640
return

0 commit comments

Comments
 (0)