Skip to content

Commit add89fc

Browse files
committed
Multiple forms of custom_functions
#44 (comment)
1 parent 4550fc2 commit add89fc

File tree

4 files changed

+335
-84
lines changed

4 files changed

+335
-84
lines changed

docs/changes.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ To be released.
2121

2222
- Added custom functions support. [:issue:`13`, :issue:`44` by Anthony Sottile]
2323

24+
- Added :class:`sass.SassFunction` class.
2425
- Added ``custom_functions`` parameter to :func:`sass.compile()` function.
2526
- Added data types for custom functions:
2627

pysass.cpp

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
#define PySass_Bytes_Check(o) PyBytes_Check(o)
1111
#define PySass_Bytes_GET_SIZE(o) PyBytes_GET_SIZE(o)
1212
#define PySass_Bytes_AS_STRING(o) PyBytes_AS_STRING(o)
13+
#define PySass_Object_Bytes(o) PyUnicode_AsUTF8String(PyObject_Str(o))
1314
#else
1415
#define PySass_IF_PY3(three, two) (two)
1516
#define PySass_Int_FromLong(v) PyInt_FromLong(v)
1617
#define PySass_Bytes_Check(o) PyString_Check(o)
1718
#define PySass_Bytes_GET_SIZE(o) PyString_GET_SIZE(o)
1819
#define PySass_Bytes_AS_STRING(o) PyString_AS_STRING(o)
20+
#define PySass_Object_Bytes(o) PyObject_Str(o)
1921
#endif
2022

2123
#ifdef __cplusplus
@@ -398,13 +400,12 @@ static void _add_custom_functions(
398400
PyList_Size(custom_functions)
399401
);
400402
for (i = 0; i < PyList_GET_SIZE(custom_functions); i += 1) {
401-
PyObject* signature_and_func = PyList_GET_ITEM(custom_functions, i);
402-
PyObject* signature = PyTuple_GET_ITEM(signature_and_func, 0);
403-
PyObject* func = PyTuple_GET_ITEM(signature_and_func, 1);
403+
PyObject* sass_function = PyList_GET_ITEM(custom_functions, i);
404+
PyObject* signature = PySass_Object_Bytes(sass_function);
404405
Sass_C_Function_Callback fn = sass_make_function(
405406
PySass_Bytes_AS_STRING(signature),
406407
_call_py_f,
407-
func
408+
sass_function
408409
);
409410
sass_function_set_list_entry(fn_list, i, fn);
410411
}

sass.py

Lines changed: 169 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
from _sass import OUTPUT_STYLES, compile_filename, compile_string
2525

2626
__all__ = ('MODES', 'OUTPUT_STYLES', 'SOURCE_COMMENTS', 'CompileError',
27-
'and_join', 'compile')
27+
'SassColor', 'SassError', 'SassFunction', 'SassList', 'SassMap',
28+
'SassNumber', 'SassWarning', 'and_join', 'compile')
2829
__version__ = '0.7.0'
2930

3031

@@ -59,21 +60,86 @@ def mkdirp(path):
5960
raise
6061

6162

62-
def _prepare_custom_function_list(custom_functions):
63-
# (signature, function_reference)
64-
custom_function_list = []
65-
for func_name, func in sorted(custom_functions.items()):
66-
argspec = inspect.getargspec(func)
63+
class SassFunction(object):
64+
"""Custom function for Sass. It can be instantiated using
65+
:meth:`from_lambda()` and :meth:`from_named_function()` as well.
66+
67+
:param name: the function name
68+
:type name: :class:`str`
69+
:param arguments: the argument names
70+
:type arguments: :class:`collections.Sequence`
71+
:param callable_: the actual function to be called
72+
:type callable_: :class:`collections.Callable`
73+
74+
.. versionadded:: 0.7.0
75+
76+
"""
77+
78+
__slots__ = 'name', 'arguments', 'callable_'
79+
80+
@classmethod
81+
def from_lambda(cls, name, lambda_):
82+
"""Make a :class:`SassFunction` object from the given ``lambda_``
83+
function. Since lambda functions don't have their name, it need
84+
its ``name`` as well. Arguments are automatically inspected.
85+
86+
:param name: the function name
87+
:type name: :class:`str`
88+
:param lambda_: the actual lambda function to be called
89+
:type lambda_: :class:`types.LambdaType`
90+
:returns: a custom function wrapper of the ``lambda_`` function
91+
:rtype: :class:`SassFunction`
92+
93+
"""
94+
argspec = inspect.getargspec(lambda_)
6795
if argspec.varargs or argspec.keywords or argspec.defaults:
6896
raise TypeError(
69-
'Functions cannot have starargs or defaults: {0} {1}'.format(
70-
func_name, func,
97+
'functions cannot have starargs or defaults: {0} {1}'.format(
98+
name, lambda_
7199
)
72100
)
73-
blinged_args = ['$' + arg for arg in argspec.args]
74-
signature = '{0}({1})'.format(func_name, ', '.join(blinged_args))
75-
custom_function_list.append((signature.encode('UTF-8'), func))
76-
return custom_function_list
101+
return cls(name, argspec.args, lambda_)
102+
103+
@classmethod
104+
def from_named_function(cls, function):
105+
"""Make a :class:`SassFunction` object from the named ``function``.
106+
Function name and arguments are automatically inspected.
107+
108+
:param function: the named function to be called
109+
:type function: :class:`types.FunctionType`
110+
:returns: a custom function wrapper of the ``function``
111+
:rtype: :class:`SassFunction`
112+
113+
"""
114+
if not getattr(function, '__name__', ''):
115+
raise TypeError('function must be named')
116+
return cls.from_lambda(function.__name__, function)
117+
118+
def __init__(self, name, arguments, callable_):
119+
if not isinstance(name, string_types):
120+
raise TypeError('name must be a string, not ' + repr(name))
121+
elif not isinstance(arguments, collections.Sequence):
122+
raise TypeError('arguments must be a sequence, not ' +
123+
repr(arguments))
124+
elif not callable(callable_):
125+
raise TypeError(repr(callable_) + ' is not callable')
126+
self.name = name
127+
self.arguments = tuple(
128+
arg if arg.startswith('$') else '$' + arg
129+
for arg in arguments
130+
)
131+
self.callable_ = callable_
132+
133+
@property
134+
def signature(self):
135+
"""Signature string of the function."""
136+
return '{0}({1})'.format(self.name, ', '.join(self.arguments))
137+
138+
def __call__(self, *args, **kwargs):
139+
return self.callable_(*args, **kwargs)
140+
141+
def __str__(self):
142+
return self.signature
77143

78144

79145
def compile_dirname(
@@ -130,8 +196,12 @@ def compile(**kwargs):
130196
:type image_path: :class:`str`
131197
:param precision: optional precision for numbers. :const:`5` by default.
132198
:type precision: :class:`int`
133-
:param custom_functions: optional mapping of custom functions
134-
:type custom_functions: :class:`collections.Mapping`
199+
:param custom_functions: optional mapping of custom functions.
200+
see also below `custom functions
201+
<custom-functions>`_ description
202+
:type custom_functions: :class:`collections.Set`,
203+
:class:`collections.Sequence`,
204+
:class:`collections.Mapping`
135205
:returns: the compiled CSS string
136206
:rtype: :class:`str`
137207
:raises sass.CompileError: when it fails for any reason
@@ -163,8 +233,12 @@ def compile(**kwargs):
163233
:type image_path: :class:`str`
164234
:param precision: optional precision for numbers. :const:`5` by default.
165235
:type precision: :class:`int`
166-
:param custom_functions: optional mapping of custom functions
167-
:type custom_functions: :class:`collections.Mapping`
236+
:param custom_functions: optional mapping of custom functions.
237+
see also below `custom functions
238+
<custom-functions>`_ description
239+
:type custom_functions: :class:`collections.Set`,
240+
:class:`collections.Sequence`,
241+
:class:`collections.Mapping`
168242
:returns: the compiled CSS string, or a pair of the compiled CSS string
169243
and the source map string if ``source_comments='map'``
170244
:rtype: :class:`str`, :class:`tuple`
@@ -199,11 +273,68 @@ def compile(**kwargs):
199273
:type image_path: :class:`str`
200274
:param precision: optional precision for numbers. :const:`5` by default.
201275
:type precision: :class:`int`
202-
:param custom_functions: optional mapping of custom functions
203-
:type custom_functions: :class:`collections.Mapping`
276+
:param custom_functions: optional mapping of custom functions.
277+
see also below `custom functions
278+
<custom-functions>`_ description
279+
:type custom_functions: :class:`collections.Set`,
280+
:class:`collections.Sequence`,
281+
:class:`collections.Mapping`
204282
:raises sass.CompileError: when it fails for any reason
205283
(for example the given SASS has broken syntax)
206284
285+
.. _custom-functions:
286+
287+
The ``custom_functions`` parameter can take three types of forms:
288+
289+
:class:`~collections.Set`/:class:`~collections.Sequence` of \
290+
:class:`SassFunction`\ s
291+
It is the most general form. Although pretty verbose, it can take
292+
any kind of callables like type objects, unnamed functions,
293+
and user-defined callables.
294+
295+
.. code-block:: python
296+
297+
sass.compile(
298+
...,
299+
custom_functions={
300+
sass.SassFunction('func-name', ('$a', '$b'), some_callable),
301+
...
302+
}
303+
)
304+
305+
:class:`~collections.Mapping` of names to functions
306+
Less general, but easier-to-use form. Although it's not it can take
307+
any kind of callables, it can take any kind of *functions* defined
308+
using :keyword:`def`/:keyword:`lambda` syntax.
309+
It cannot take callables other than them since inspecting arguments
310+
is not always available for every kind of callables.
311+
312+
.. code-block:: python
313+
314+
sass.compile(
315+
...,
316+
custom_functions={
317+
'func-name': lambda a, b: ...,
318+
...
319+
}
320+
)
321+
322+
:class:`~collections.Set`/:class:`~collections.Sequence` of \
323+
named functions
324+
Not general, but the easiest-to-use form for *named* functions.
325+
It can take only named functions, defined using :keyword:`def`.
326+
It cannot take lambdas sinc names are unavailable for them.
327+
328+
.. code-block:: python
329+
330+
def func_name(a, b):
331+
return ...
332+
333+
sass.compile(
334+
...,
335+
custom_functions={func_name}
336+
)
337+
207338
.. versionadded:: 0.4.0
208339
Added ``source_comments`` and ``source_map_filename`` parameters.
209340
@@ -304,8 +435,26 @@ def compile(**kwargs):
304435
elif isinstance(image_path, text_type):
305436
image_path = image_path.encode(fs_encoding)
306437

307-
custom_functions = dict(kwargs.pop('custom_functions', {}))
308-
custom_functions = _prepare_custom_function_list(custom_functions)
438+
custom_functions = kwargs.pop('custom_functions', ())
439+
if isinstance(custom_functions, collections.Mapping):
440+
custom_functions = [
441+
SassFunction.from_lambda(name, lambda_)
442+
for name, lambda_ in custom_functions.items()
443+
]
444+
elif isinstance(custom_functions, (collections.Set, collections.Sequence)):
445+
custom_functions = [
446+
func if isinstance(func, SassFunction)
447+
else SassFunction.from_named_function(func)
448+
for func in custom_functions
449+
]
450+
else:
451+
raise TypeError(
452+
'custom_functions must be one of:\n'
453+
'- a set/sequence of {0.__module__}.{0.__name__} objects,\n'
454+
'- a mapping of function name strings to lambda functions,\n'
455+
'- a set/sequence of named functions,\n'
456+
'not {1!r}'.format(SassFunction, custom_functions)
457+
)
309458

310459
if 'string' in modes:
311460
string = kwargs.pop('string')

0 commit comments

Comments
 (0)