Skip to content
This repository was archived by the owner on Oct 24, 2025. It is now read-only.
Merged
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ build2/libsass/cpp/%.o: libsass/src/%.cpp

build2/pysass.o: pysass.cpp
@mkdir -p build2
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
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

_sass.so: $(C_OBJECTS) $(CPP_OBJECTS) build2/pysass.o
g++ -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-Bsymbolic-functions -Wl,-z,relro $^ -L./libsass -o $@ -fPIC -lstdc++
Expand Down
133 changes: 119 additions & 14 deletions pysass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -272,8 +272,8 @@ static union Sass_Value* _unknown_type_to_sass_error(PyObject* value) {
return retv;
}

static union Sass_Value* _exception_to_sass_error() {
union Sass_Value* retv = NULL;
static PyObject* _exception_to_bytes() {
PyObject* retv = NULL;
PyObject* etype = NULL;
PyObject* evalue = NULL;
PyObject* etb = NULL;
Expand All @@ -287,22 +287,34 @@ static union Sass_Value* _exception_to_sass_error() {
PyList_Insert(traceback_parts, 0, PyUnicode_FromString("\n"));
PyObject* joinstr = PyUnicode_FromString("");
PyObject* result = PyUnicode_Join(joinstr, traceback_parts);
PyObject* bytes = PyUnicode_AsEncodedString(
result, "UTF-8", "strict"
);
retv = sass_make_error(PySass_Bytes_AS_STRING(bytes));
retv = PyUnicode_AsEncodedString(result, "UTF-8", "strict");
Py_DECREF(traceback_mod);
Py_DECREF(traceback_parts);
Py_DECREF(joinstr);
Py_DECREF(result);
Py_DECREF(bytes);
}
Py_DECREF(etype);
Py_DECREF(evalue);
Py_DECREF(etb);
return retv;
}

static union Sass_Value* _exception_to_sass_error() {
PyObject* bytes = _exception_to_bytes();
union Sass_Value* retv = sass_make_error(PySass_Bytes_AS_STRING(bytes));
Py_DECREF(bytes);
return retv;
}

static Sass_Import_List _exception_to_sass_import_error(const char* path) {
PyObject* bytes = _exception_to_bytes();
Sass_Import_List import_list = sass_make_import_list(1);
import_list[0] = sass_make_import_entry(path, 0, 0);
sass_import_set_error(import_list[0], PySass_Bytes_AS_STRING(bytes), 0, 0);
Py_DECREF(bytes);
return import_list;
}

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 +416,96 @@ static void _add_custom_functions(
sass_option_set_c_functions(options, fn_list);
}

static 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_result = NULL;
Sass_Import_List sass_imports = NULL;
Py_ssize_t i;

py_result = PyObject_CallFunction(pyfunc, PySass_IF_PY3("y", "s"), path);

/* Handle importer throwing an exception */
if (!py_result) goto done;

/* Could return None indicating it could not handle the import */
if (py_result == Py_None) {
Py_XDECREF(py_result);
return NULL;
}

/* Otherwise, we know our importer is well formed (because we wrap it)
* The return value will be a tuple of 1, 2, or 3 tuples */
sass_imports = sass_make_import_list(PyTuple_GET_SIZE(py_result));
for (i = 0; i < PyTuple_GET_SIZE(py_result); i += 1) {
char* path_str = NULL; /* XXX: Memory leak? */
char* source_str = NULL;
char* sourcemap_str = NULL;
PyObject* tup = PyTuple_GET_ITEM(py_result, i);
Py_ssize_t size = PyTuple_GET_SIZE(tup);

if (size == 1) {
PyArg_ParseTuple(tup, PySass_IF_PY3("y", "s"), &path_str);
} else if (size == 2) {
PyArg_ParseTuple(
tup, PySass_IF_PY3("yy", "ss"), &path_str, &source_str
);
} else if (size == 3) {
PyArg_ParseTuple(
tup, PySass_IF_PY3("yyy", "sss"),
&path_str, &source_str, &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
);
}

done:
if (sass_imports == NULL) {
sass_imports = _exception_to_sass_import_error(path);
}

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(PyTuple_GET_SIZE(custom_importers));

for (i = 0; i < PyTuple_GET_SIZE(custom_importers); i += 1) {
PyObject* item = PyTuple_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 +516,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;
}

Expand All @@ -432,7 +535,7 @@ 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 @@ -457,13 +560,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,7 +592,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);

ctx = sass_file_context_get_context(context);
Expand Down
114 changes: 109 additions & 5 deletions sass.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from __future__ import absolute_import

import collections
import functools
import inspect
from io import open
import os
Expand Down Expand Up @@ -144,9 +145,59 @@ def __str__(self):
return self.signature


def _normalize_importer_return_value(result):
# An importer must return an iterable of iterables of 1-3 stringlike
# objects
if result is None:
return result

def _to_importer_result(single_result):
single_result = tuple(single_result)
if len(single_result) not in (1, 2, 3):
raise ValueError(
'Expected importer result to be a tuple of length (1, 2, 3) '
'but got {0}: {1!r}'.format(len(single_result), single_result)
)

def _to_bytes(obj):
if not isinstance(obj, bytes):
return obj.encode('UTF-8')
else:
return obj

return tuple(_to_bytes(s) for s in single_result)

return tuple(_to_importer_result(x) for x in result)


def _importer_callback_wrapper(func):
@functools.wraps(func)
def inner(path):
ret = func(path.decode('UTF-8'))
return _normalize_importer_return_value(ret)
return inner


def _validate_importers(importers):
"""Validates the importers and decorates the callables with our output
formatter.
"""
# They could have no importers, that's chill
if importers is None:
return None

def _to_importer(priority, func):
assert isinstance(priority, int), priority
assert callable(func), func
return (priority, _importer_callback_wrapper(func))

# Our code assumes tuple of tuples
return tuple(_to_importer(priority, func) for priority, func in importers)


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 +214,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 @@ -219,6 +270,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 @@ -253,6 +308,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 @@ -347,6 +406,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 @@ -458,6 +560,8 @@ def func_name(a, b):
'not {1!r}'.format(SassFunction, custom_functions)
)

importers = _validate_importers(kwargs.pop('importers', None))

if 'string' in modes:
string = kwargs.pop('string')
if isinstance(string, text_type):
Expand All @@ -469,7 +573,7 @@ def func_name(a, b):
_check_no_remaining_kwargs(compile, kwargs)
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 @@ -484,7 +588,7 @@ def func_name(a, b):
_check_no_remaining_kwargs(compile, kwargs)
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 @@ -530,7 +634,7 @@ def func_name(a, b):
_check_no_remaining_kwargs(compile, kwargs)
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
Loading