Skip to content

Commit

Permalink
Merge pull request #110 from asottile/importer_callbacks
Browse files Browse the repository at this point in the history
Importer callbacks
  • Loading branch information
asottile committed Dec 13, 2015
2 parents 797a571 + f3acec5 commit 13eb3f8
Show file tree
Hide file tree
Showing 4 changed files with 361 additions and 20 deletions.
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

0 comments on commit 13eb3f8

Please sign in to comment.