Skip to content
This repository was archived by the owner on Oct 24, 2025. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 146 additions & 8 deletions pysass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

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

*/
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");
Expand Down Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The 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 unicode(bytes_object) would, but it seems the c-api is not always consistent with the python behaviour!)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. That also goes for the format string I switched the ParseTuple statements to that is cross-verson consistent. It also does UTF-8 conversion, which, I feel, is a much more sane default. ;)

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);
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
Copy link
Member

Choose a reason for hiding this comment

The 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);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's an additional error if the function returns nonsense here

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most (if not all) errors in the C-Api return NULL (0) and set things you can look at with PyErr_...


/* 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;
Expand All @@ -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;

Copy link
Member

Choose a reason for hiding this comment

The 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 :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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;
}

Expand All @@ -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);
Expand All @@ -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;
}
Expand All @@ -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;
}

Expand All @@ -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);

Expand Down
63 changes: 58 additions & 5 deletions sass.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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')
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -448,6 +499,8 @@ def func_name(a, b):
'not {1!r}'.format(SassFunction, custom_functions)
)

importers = kwargs.pop('importers', None)
Copy link
Member

Choose a reason for hiding this comment

The 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):
Expand All @@ -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')
Expand All @@ -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')
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions sasstests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down