From 2dde3d5ed9bc434c557c3128319dcdcd65facc23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Wed, 25 Nov 2015 04:30:24 -0500 Subject: [PATCH 01/19] Initial function draft; note the comments! Getting late. --- pysass.cpp | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/pysass.cpp b/pysass.cpp index 89484b75..69697e1e 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -404,6 +404,73 @@ 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); + PyObject* py_args = PyTuple_New(1); + PyObject* py_result = NULL; + Sass_Import_List sass_imports = NULL; + + PyTuple_SetItem(py_args, 0, py_path); + + if (!(py_result = PyObject_CallObject(pyfunc, py_args))) { + sass_imports = sass_make_import_list(1); + sass_imports[0] = sass_make_import_entry(path, 0, 0); + + + sass_import_set_error(list[0], strdup(message), 0, 0); + + Py_XDECREF(py_result); + return sass_imports; + } + + Py_XDECREF(py_args); + + if ( py_result == Py_None ) { + Py_XDECREF(py_result); + return 0; + } + + sass_imports = sass_make_import_list(PyList_Size(py_result)); + + /* TODO: Iterator instead of literal list? Memory savings Python-side! */ + for (i = 0; i < PyList_GET_SIZE(py_result); i += 1) { + PyObject* import_item = PyList_GET_ITEM(py_result, i); + 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() == 1 ) { + PyArg_ParseTuple(import_item, + PySass_IF_PY3("y", "s"), + &path_str); + } else if ( PyTuple_GET_SIZE() == 2 ) { + PyArg_ParseTuple(import_item, + PySass_IF_PY3("yy", "ss"), + &path_str, &source_str); + } else if ( PyTuple_GET_SIZE() == 3 ) { + PyArg_ParseTuple(import_item, + PySass_IF_PY3("yyy", "sss"), + &path_str, &source_str, &sourcemap_str); + } + + /* POSSIBLE LEAK: owns arg source_str, sourcemap_str, but NOT path_str, ownership given @XXX?, + ref: https://github.com/sass/libsass/blob/master/docs/api-importer.md#return-imports */ + sass_imports[i] = sass_make_import_entry(path_str, source_str, sourcemap_str) + + Py_XDECREF(import_item); + } + + Py_XDECREF(py_result); + + return sass_imports; +} + static PyObject * PySass_compile_string(PyObject *self, PyObject *args) { struct Sass_Context *ctx; From 7981dd5943e35a4253f2dbedf5e524b0c0612666 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Wed, 25 Nov 2015 04:30:45 -0500 Subject: [PATCH 02/19] Documentation. --- sass.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/sass.py b/sass.py index bf2498fd..1c6db3b4 100644 --- a/sass.py +++ b/sass.py @@ -209,6 +209,10 @@ def compile(**kwargs): formatted. :const:`False` by default :type indented: :class:`bool` :returns: the compiled CSS string + :param importer: optional callback function. + see also below `importer callbacks + `_ description + :type importer: :class:`collections.Callable` :rtype: :class:`str` :raises sass.CompileError: when it fails for any reason (for example the given SASS has broken syntax) @@ -336,6 +340,25 @@ def func_name(a, b): ..., custom_functions={func_name} ) + + .. _importer-callbacks: + + Newer versions of ``libsass`` allow developers to define a callback to be + called to be given a chance to process ``@import`` directives. You can + define yours by passing in a callable via the ``importer`` parameter. + + This callback must accept a single string argument representing the path + passed to the ``@import`` directive, and either return ``None`` to + indicate the path should be handled internally by ``libsass``, 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 values must be strings. .. versionadded:: 0.4.0 Added ``source_comments`` and ``source_map_filename`` parameters. From 2c1e233c9342cdddb884526a2c17d68a42b2500e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Wed, 25 Nov 2015 04:30:57 -0500 Subject: [PATCH 03/19] Initial test draft. --- sasstests.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/sasstests.py b/sasstests.py index 3829e618..c1fd25de 100644 --- a/sasstests.py +++ b/sasstests.py @@ -260,7 +260,23 @@ def test_compile_string_sass_style(self): actual = sass.compile(string='a\n\tb\n\t\tcolor: blue;', 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; }') + (path, '.' + path + ' { color: red; }') + ] + + source = '''@import('button') + a { color: green; }''' + + actual = sass.compile(string=source, importer=importer_callback) + assert actual == "#button { color: blue; }\n" + ".button { color: red; }\n" + "a { color: green; }" + def test_compile_string_deprecated_source_comments_line_numbers(self): source = '''a { b { color: blue; } From bb75194d091097b59a2b0be649d49d8ca9a33e82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Thu, 26 Nov 2015 11:04:31 -0500 Subject: [PATCH 04/19] Memory management tweaks. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now increments the reference count for the current list item, as it isn’t auto-retained by `PyList_GET_ITEM`. Additionally, corrected missing DECREF to prevent leak on early exit due to error. --- pysass.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pysass.cpp b/pysass.cpp index 69697e1e..19df1e0c 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -424,6 +424,7 @@ Sass_Import_List _call_py_importer_f( sass_import_set_error(list[0], strdup(message), 0, 0); + Py_XDECREF(py_args); Py_XDECREF(py_result); return sass_imports; } @@ -444,6 +445,8 @@ Sass_Import_List _call_py_importer_f( char* source_str = NULL; char* sourcemap_str = NULL; + Py_INCREF(import_item); /* GET_ITEM doesn't auto-retain. */ + /* TODO: Switch statement and error handling for default case. Better way? */ if ( PyTuple_GET_SIZE() == 1 ) { PyArg_ParseTuple(import_item, From f65a640f5ccafbad121e96aadf56f1170b7ff2e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Thu, 26 Nov 2015 11:04:50 -0500 Subject: [PATCH 05/19] Verified argument ownership, added missing strdup. --- pysass.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pysass.cpp b/pysass.cpp index 19df1e0c..ce613eba 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -462,8 +462,9 @@ Sass_Import_List _call_py_importer_f( &path_str, &source_str, &sourcemap_str); } - /* POSSIBLE LEAK: owns arg source_str, sourcemap_str, but NOT path_str, ownership given @XXX?, - ref: https://github.com/sass/libsass/blob/master/docs/api-importer.md#return-imports */ + 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); From 0f733a934b80ca96f1377f9024c1ed6f4de7d76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Thu, 26 Nov 2015 14:41:27 -0500 Subject: [PATCH 06/19] Updated C extension with wired-in importer callbacks. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remarkably, this actually compiles. I’m almost guaranteed missing post-compile cleanup involving releasing the function reference “cookies”. --- pysass.cpp | 67 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/pysass.cpp b/pysass.cpp index ce613eba..ef556413 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -414,6 +414,7 @@ Sass_Import_List _call_py_importer_f( PyObject* py_args = PyTuple_New(1); PyObject* py_result = NULL; Sass_Import_List sass_imports = NULL; + Py_ssize_t i; PyTuple_SetItem(py_args, 0, py_path); @@ -421,8 +422,9 @@ Sass_Import_List _call_py_importer_f( sass_imports = sass_make_import_list(1); sass_imports[0] = sass_make_import_entry(path, 0, 0); - - sass_import_set_error(list[0], strdup(message), 0, 0); + sass_import_set_error(sass_imports[0], + strdup("Error calling importer callback."), + 0, 0); Py_XDECREF(py_args); Py_XDECREF(py_result); @@ -445,27 +447,32 @@ Sass_Import_List _call_py_importer_f( char* source_str = NULL; char* sourcemap_str = NULL; - Py_INCREF(import_item); /* GET_ITEM doesn't auto-retain. */ + /* GET_ITEM doesn't auto-retain. Future armoring relating to above + TODO, where iteration may return to the Python code context. */ + Py_INCREF(import_item); /* TODO: Switch statement and error handling for default case. Better way? */ - if ( PyTuple_GET_SIZE() == 1 ) { + if ( PyTuple_GET_SIZE(import_item) == 1 ) { PyArg_ParseTuple(import_item, PySass_IF_PY3("y", "s"), &path_str); - } else if ( PyTuple_GET_SIZE() == 2 ) { + } else if ( PyTuple_GET_SIZE(import_item) == 2 ) { PyArg_ParseTuple(import_item, PySass_IF_PY3("yy", "ss"), &path_str, &source_str); - } else if ( PyTuple_GET_SIZE() == 3 ) { + } else if ( PyTuple_GET_SIZE(import_item) == 3 ) { PyArg_ParseTuple(import_item, 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) + sass_imports[i] = sass_make_import_entry(path_str, source_str, sourcemap_str); Py_XDECREF(import_item); } @@ -475,6 +482,31 @@ Sass_Import_List _call_py_importer_f( return sass_imports; } +static void _add_custom_importers( + struct Sass_Options* options, PyObject* custom_importers +) { + Py_ssize_t i; + Sass_Importer_List importer_list; + + 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, + PySass_IF_PY3("iO", "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; @@ -485,13 +517,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; } @@ -503,7 +536,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); @@ -515,6 +549,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; } @@ -528,13 +563,16 @@ 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("yiiyiOOiO", "siisiOOiO"), &filename, &output_style, &source_comments, &include_paths, &precision, - &source_map_filename, &custom_functions)) { + &source_map_filename, &custom_functions, + &custom_importers)) { return NULL; } @@ -558,6 +596,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); From d504dc261f51efe9202baa32b5cde9125ef1f92e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Thu, 26 Nov 2015 15:04:43 -0500 Subject: [PATCH 07/19] Updated documentation. --- sass.py | 46 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/sass.py b/sass.py index 1c6db3b4..2bb64ad7 100644 --- a/sass.py +++ b/sass.py @@ -209,10 +209,10 @@ def compile(**kwargs): formatted. :const:`False` by default :type indented: :class:`bool` :returns: the compiled CSS string - :param importer: optional callback function. + :param importers: optional callback functions. see also below `importer callbacks `_ description - :type importer: :class:`collections.Callable` + :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) @@ -247,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 + `_ 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` @@ -343,14 +347,23 @@ def func_name(a, b): .. _importer-callbacks: - Newer versions of ``libsass`` allow developers to define a callback to be - called to be given a chance to process ``@import`` directives. You can - define yours by passing in a callable via the ``importer`` parameter. + 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. - This callback must accept a single string argument representing the path + These callbacks must accept a single string argument representing the path passed to the ``@import`` directive, and either return ``None`` to - indicate the path should be handled internally by ``libsass``, or a list - of one or more tuples, each in one of three forms: + 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 @@ -358,7 +371,22 @@ def func_name(a, b): * A 3-tuple representing the same as the 2-tuple with the addition of a "sourcemap". - All tuple values must be strings. + 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. From 164951ba6833b11cb7e2417a965f26f85b422a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Thu, 26 Nov 2015 15:07:30 -0500 Subject: [PATCH 08/19] Handle the case of no passed importers. --- pysass.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pysass.cpp b/pysass.cpp index ef556413..f2f8ab14 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -487,6 +487,10 @@ static void _add_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)); From c8f1fea187cde629a226f8717a44b4e77bf21d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Thu, 26 Nov 2015 17:39:20 -0500 Subject: [PATCH 09/19] Wire in the importer callback option. --- pysass.cpp | 3 +-- sass.py | 12 +++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pysass.cpp b/pysass.cpp index f2f8ab14..2a5839a0 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -570,9 +570,8 @@ PySass_compile_filename(PyObject *self, PyObject *args) { PyObject *source_map_filename, *custom_functions, *custom_importers, *result; - if (!PyArg_ParseTuple(args, - PySass_IF_PY3("yiiyiOOiO", "siisiOOiO"), + PySass_IF_PY3("yiiyiOOO", "siisiOOO"), &filename, &output_style, &source_comments, &include_paths, &precision, &source_map_filename, &custom_functions, diff --git a/sass.py b/sass.py index 2bb64ad7..9890ba00 100644 --- a/sass.py +++ b/sass.py @@ -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): @@ -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') @@ -498,6 +498,8 @@ def my_importer(path): '- a set/sequence of named functions,\n' 'not {1!r}'.format(SassFunction, custom_functions) ) + + importers = kwargs.pop('importers', None) if 'string' in modes: string = kwargs.pop('string') @@ -509,7 +511,7 @@ def my_importer(path): 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') @@ -523,7 +525,7 @@ def my_importer(path): 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') @@ -568,7 +570,7 @@ def my_importer(path): '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 From c032269c4885ad596fa5027724956a8ddea589d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Thu, 26 Nov 2015 17:40:07 -0500 Subject: [PATCH 10/19] Capture errors during callback execution. --- pysass.cpp | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pysass.cpp b/pysass.cpp index 2a5839a0..c71e345e 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -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. + */ + 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"); @@ -422,10 +453,14 @@ Sass_Import_List _call_py_importer_f( 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], - strdup("Error calling importer callback."), + err, 0, 0); + Py_XDECREF(exc); Py_XDECREF(py_args); Py_XDECREF(py_result); return sass_imports; From 1d895a0f4e6a4bc6a6fef0a69380439e80060b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Thu, 26 Nov 2015 17:40:42 -0500 Subject: [PATCH 11/19] Test updates for real syntax. Also shows what was emitted in the event of a failure. --- sasstests.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/sasstests.py b/sasstests.py index c1fd25de..67e93ad4 100644 --- a/sasstests.py +++ b/sasstests.py @@ -264,18 +264,20 @@ def test_compile_string_sass_style(self): # TODO: Test "nop" (return None) handling. Pseudo-code. def test_compile_string_with_importer_callback(self): def importer_callback(path): + print("HERE") return [ - (path, '#' + path + ' { color: blue; }') - (path, '.' + path + ' { color: red; }') + (path, '#' + path + ' { color: blue; }\n'), + (path, '.' + path + ' { color: red; }\n') ] - source = '''@import('button') + source = '''@import 'button'; a { color: green; }''' - actual = sass.compile(string=source, importer=importer_callback) - assert actual == "#button { color: blue; }\n" - ".button { color: red; }\n" - "a { color: green; }" + actual = sass.compile(string=source, + importers=[(0, importer_callback)]) + assert actual == "#button { color: blue; }\n" \ + ".button { color: red; }\n" \ + "a { color: green; }", "got: " + actual def test_compile_string_deprecated_source_comments_line_numbers(self): source = '''a { From b780b23e3e6185dbb96044cd076cdf26d11dc3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Fri, 27 Nov 2015 01:40:58 -0500 Subject: [PATCH 12/19] Corrected test. --- sasstests.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/sasstests.py b/sasstests.py index 67e93ad4..da260ca4 100644 --- a/sasstests.py +++ b/sasstests.py @@ -264,7 +264,6 @@ def test_compile_string_sass_style(self): # TODO: Test "nop" (return None) handling. Pseudo-code. def test_compile_string_with_importer_callback(self): def importer_callback(path): - print("HERE") return [ (path, '#' + path + ' { color: blue; }\n'), (path, '.' + path + ' { color: red; }\n') @@ -275,10 +274,16 @@ def importer_callback(path): actual = sass.compile(string=source, importers=[(0, importer_callback)]) - assert actual == "#button { color: blue; }\n" \ - ".button { color: red; }\n" \ - "a { color: green; }", "got: " + actual - + 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; } From 78f526c657ad27d000e9b748188cf9fa807fdf98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Fri, 27 Nov 2015 02:27:39 -0500 Subject: [PATCH 13/19] Cross-compatible `PyArg_ParseTuple` usage. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Turns out, it’s also more flexible this way. --- pysass.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/pysass.cpp b/pysass.cpp index c71e345e..b5de12f4 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -488,17 +488,17 @@ Sass_Import_List _call_py_importer_f( /* TODO: Switch statement and error handling for default case. Better way? */ if ( PyTuple_GET_SIZE(import_item) == 1 ) { - PyArg_ParseTuple(import_item, - PySass_IF_PY3("y", "s"), - &path_str); + fprintf(stderr, "il cb item 1-tup\n"); + PyArg_ParseTuple(import_item, "es", + 0, &path_str); } else if ( PyTuple_GET_SIZE(import_item) == 2 ) { - PyArg_ParseTuple(import_item, - PySass_IF_PY3("yy", "ss"), - &path_str, &source_str); + fprintf(stderr, "il cb item 2-tup\n"); + PyArg_ParseTuple(import_item, "eses", + 0, &path_str, 0, &source_str); } else if ( PyTuple_GET_SIZE(import_item) == 3 ) { - PyArg_ParseTuple(import_item, - PySass_IF_PY3("yyy", "sss"), - &path_str, &source_str, &sourcemap_str); + fprintf(stderr, "il cb item 3-tup\n"); + PyArg_ParseTuple(import_item, "eseses", + 0, &path_str, 0, &source_str, 0, &sourcemap_str); } /* We need to give copies of these arguments; libsass handles @@ -534,8 +534,7 @@ static void _add_custom_importers( int priority = 0; PyObject* import_function = NULL; - PyArg_ParseTuple(item, - PySass_IF_PY3("iO", "iO"), + PyArg_ParseTuple(item, "iO", &priority, &import_function); importer_list[i] = sass_make_importer(_call_py_importer_f, From eaac1ef57f3996677a87f72c8d22184a7668f6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Fri, 27 Nov 2015 02:28:05 -0500 Subject: [PATCH 14/19] PEP-8 conformance. --- sass.py | 24 ++++++++++++------------ sasstests.py | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/sass.py b/sass.py index 9890ba00..b8c80425 100644 --- a/sass.py +++ b/sass.py @@ -344,46 +344,46 @@ 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. @@ -498,7 +498,7 @@ def my_importer(path): '- a set/sequence of named functions,\n' 'not {1!r}'.format(SassFunction, custom_functions) ) - + importers = kwargs.pop('importers', None) if 'string' in modes: diff --git a/sasstests.py b/sasstests.py index da260ca4..c7b06ee5 100644 --- a/sasstests.py +++ b/sasstests.py @@ -260,7 +260,7 @@ def test_compile_string_sass_style(self): actual = sass.compile(string='a\n\tb\n\t\tcolor: blue;', 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): @@ -268,10 +268,10 @@ def importer_callback(path): (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 { From 66d5097c5755498d5e33f4752ae83085d60cfcb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Wed, 2 Dec 2015 02:26:17 -0500 Subject: [PATCH 15/19] Switch to PyObject_CallObject. --- pysass.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pysass.cpp b/pysass.cpp index b5de12f4..85d63ade 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -442,14 +442,15 @@ Sass_Import_List _call_py_importer_f( ) { PyObject* pyfunc = (PyObject*)sass_importer_get_cookie(cb); PyObject* py_path = PyUnicode_FromString(path); - PyObject* py_args = PyTuple_New(1); PyObject* py_result = NULL; + PyObject *iterator; + PyObject *import_item; Sass_Import_List sass_imports = NULL; Py_ssize_t i; - - PyTuple_SetItem(py_args, 0, py_path); - - if (!(py_result = PyObject_CallObject(pyfunc, py_args))) { + + 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); From 3852d217b91cc05cd27d4c31a7919ce7864c12c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Wed, 2 Dec 2015 02:26:43 -0500 Subject: [PATCH 16/19] "Trailing whitespace" cleanup. --- pysass.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pysass.cpp b/pysass.cpp index 85d63ade..dcb23aac 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -453,14 +453,14 @@ Sass_Import_List _call_py_importer_f( 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); - + Py_XDECREF(exc); Py_XDECREF(py_args); Py_XDECREF(py_result); @@ -473,7 +473,7 @@ Sass_Import_List _call_py_importer_f( Py_XDECREF(py_result); return 0; } - + sass_imports = sass_make_import_list(PyList_Size(py_result)); /* TODO: Iterator instead of literal list? Memory savings Python-side! */ @@ -501,20 +501,20 @@ Sass_Import_List _call_py_importer_f( PyArg_ParseTuple(import_item, "eseses", 0, &path_str, 0, &source_str, 0, &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); - + Py_XDECREF(import_item); } - + Py_XDECREF(py_result); - + return sass_imports; } From 5e5172e43c4adbc7bca729d70875e8684d3f2b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Wed, 2 Dec 2015 02:27:15 -0500 Subject: [PATCH 17/19] Iterator use. --- pysass.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pysass.cpp b/pysass.cpp index dcb23aac..f91d4dad 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -475,10 +475,9 @@ Sass_Import_List _call_py_importer_f( } sass_imports = sass_make_import_list(PyList_Size(py_result)); - - /* TODO: Iterator instead of literal list? Memory savings Python-side! */ - for (i = 0; i < PyList_GET_SIZE(py_result); i += 1) { - PyObject* import_item = PyList_GET_ITEM(py_result, i); + + iterator = PyObject_GetIter(obj); + while (import_item = PyIter_Next(iterator)) { char* path_str = NULL; /* XXX: Memory leak? */ char* source_str = NULL; char* sourcemap_str = NULL; @@ -513,6 +512,7 @@ Sass_Import_List _call_py_importer_f( Py_XDECREF(import_item); } + Py_XDECREF(iterator); Py_XDECREF(py_result); return sass_imports; From 92416a8b61b77f5fd1bd1d20310be02e8b9df15d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Wed, 2 Dec 2015 02:27:35 -0500 Subject: [PATCH 18/19] Clean-up. --- pysass.cpp | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/pysass.cpp b/pysass.cpp index f91d4dad..5707c9a5 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -462,14 +462,11 @@ Sass_Import_List _call_py_importer_f( 0, 0); Py_XDECREF(exc); - Py_XDECREF(py_args); Py_XDECREF(py_result); return sass_imports; } - - Py_XDECREF(py_args); - - if ( py_result == Py_None ) { + + if (py_result == Py_None) { Py_XDECREF(py_result); return 0; } @@ -481,22 +478,15 @@ Sass_Import_List _call_py_importer_f( char* path_str = NULL; /* XXX: Memory leak? */ char* source_str = NULL; char* sourcemap_str = NULL; - - /* GET_ITEM doesn't auto-retain. Future armoring relating to above - TODO, where iteration may return to the Python code context. */ - Py_INCREF(import_item); - + /* TODO: Switch statement and error handling for default case. Better way? */ if ( PyTuple_GET_SIZE(import_item) == 1 ) { - fprintf(stderr, "il cb item 1-tup\n"); PyArg_ParseTuple(import_item, "es", 0, &path_str); } else if ( PyTuple_GET_SIZE(import_item) == 2 ) { - fprintf(stderr, "il cb item 2-tup\n"); PyArg_ParseTuple(import_item, "eses", 0, &path_str, 0, &source_str); } else if ( PyTuple_GET_SIZE(import_item) == 3 ) { - fprintf(stderr, "il cb item 3-tup\n"); PyArg_ParseTuple(import_item, "eseses", 0, &path_str, 0, &source_str, 0, &sourcemap_str); } From 73a5a8cf56e0f1ecf2c841d9ffe233307871f783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alice=20Bevan=E2=80=93McGregor?= Date: Mon, 7 Dec 2015 16:00:28 -0500 Subject: [PATCH 19/19] Reference fix. --- pysass.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pysass.cpp b/pysass.cpp index 5707c9a5..01701182 100644 --- a/pysass.cpp +++ b/pysass.cpp @@ -473,7 +473,7 @@ Sass_Import_List _call_py_importer_f( sass_imports = sass_make_import_list(PyList_Size(py_result)); - iterator = PyObject_GetIter(obj); + iterator = PyObject_GetIter(py_result); while (import_item = PyIter_Next(iterator)) { char* path_str = NULL; /* XXX: Memory leak? */ char* source_str = NULL;