diff options
author | Eevee (Alex Munroe) <eevee.git@veekun.com> | 2014-08-27 14:56:41 -0700 |
---|---|---|
committer | Eevee (Alex Munroe) <eevee.git@veekun.com> | 2014-08-27 14:56:41 -0700 |
commit | 11e2b44c0e7992db0e339f4dd53fe93a16710802 (patch) | |
tree | 26be7ac1067fae42bfa5eff3f37a08d267e47c71 | |
parent | 6a8efa934d9a91c0916ecbc6659f275e25858bc6 (diff) | |
download | pyscss-11e2b44c0e7992db0e339f4dd53fe93a16710802.tar.gz |
Introduce an Extension class, finally. Making a stab at #130.
All the core functions have been moved into a core Extension, and
Namespace has been beefed up a little bit so declaring functions is
easy.
-rw-r--r-- | scss/compiler.py | 75 | ||||
-rw-r--r-- | scss/core.py (renamed from scss/functions/core.py) | 257 | ||||
-rw-r--r-- | scss/extension.py | 40 | ||||
-rw-r--r-- | scss/functions/__init__.py | 14 | ||||
-rw-r--r-- | scss/legacy.py | 8 | ||||
-rw-r--r-- | scss/namespace.py | 37 | ||||
-rw-r--r-- | scss/tests/functions/test_core.py | 6 | ||||
-rw-r--r-- | scss/tests/test_expression.py | 7 |
8 files changed, 276 insertions, 168 deletions
diff --git a/scss/compiler.py b/scss/compiler.py index fa25e7e..d7f43c7 100644 --- a/scss/compiler.py +++ b/scss/compiler.py @@ -16,12 +16,15 @@ import warnings import six import scss.config as config +from scss.core import CoreExtension from scss.cssdefs import _spaces_re from scss.cssdefs import _escape_chars_re from scss.cssdefs import _prop_split_re from scss.errors import SassError from scss.expression import Calculator -from scss.functions import ALL_BUILTINS_LIBRARY +from scss.extension import Extension +from scss.extension import NamespaceAdapterExtension +from scss.functions import COMPASS_LIBRARY from scss.functions.compass.sprites import sprite_map from scss.rule import BlockAtRuleHeader from scss.rule import Namespace @@ -50,16 +53,10 @@ except ImportError: # TODO should mention logging for the programmatic interface in the # documentation +# TODO or have a little helper (or compiler setting) to turn it on log = logging.getLogger(__name__) -# TODO produce this -# TODO is this a Library (can't define values), a Namespace (lot of -# semi-private functionality, no support for magic imports), or an Extension -# (doesn't exist yet)? -DEFAULT_NAMESPACE = Namespace(functions=ALL_BUILTINS_LIBRARY) - - _default_rule_re = re.compile(r'(?i)\s+!default\Z') _xcss_extends_re = re.compile(r'\s+extends\s+') @@ -87,14 +84,17 @@ def warn_deprecated(rule, message): ) +# TODO it's probably still kind of weird to go Compiler().compile('a/b/c') and +# not have stuff in a/b/ importable. maybe need a top-level compile_sass() +# function that takes a single file? (compile_file? compile_string?) class Compiler(object): """A Sass compiler. Stores settings and knows how to fire off a compilation. Main entry point into compiling Sass. """ def __init__( self, root='', search_path=('',), - namespace=DEFAULT_NAMESPACE, output_style='nested', - generate_source_map=False, + namespace=None, extensions=(CoreExtension, Namespace(functions=COMPASS_LIBRARY)), + output_style='nested', generate_source_map=False, live_errors=False, warn_unused_imports=False, super_selector='', ): @@ -107,18 +107,36 @@ class Compiler(object): to ``root``. Absolute and parent paths are allowed here, but ``@import`` will refuse to load files that aren't in one of the directories here. Defaults to only the root. - :param namespace: Global namespace to inject into compiled Sass. See - the Namespace documentation for details. - :type namespace: :class:`Namespace` """ - # normpath() will (textually) eliminate any use of .. - self.root = os.path.normpath(os.path.abspath(root)) + if root is None: + self.root = None + else: + # normpath() will (textually) eliminate any use of .. + self.root = os.path.normpath(os.path.abspath(root)) + self.search_path = tuple( - os.path.normpath(os.path.join(self.root, path)) + self.normalize_path(path) for path in search_path ) - self.namespace = namespace + self.extensions = [] + if namespace is not None: + self.extensions.append(NamespaceAdapterExtension(namespace)) + for extension in extensions: + if isinstance(extension, Extension): + self.extensions.append(extension) + elif (isinstance(extension, type) and + issubclass(extension, Extension)): + self.extensions.append(extension()) + elif isinstance(extension, Namespace): + self.extensions.append( + NamespaceAdapterExtension(extension)) + else: + raise TypeError( + "Expected an Extension or Namespace, got: {0!r}" + .format(extension) + ) + self.output_style = output_style self.generate_source_map = generate_source_map self.live_errors = live_errors @@ -127,6 +145,15 @@ class Compiler(object): self.calculator = Calculator() + def normalize_path(self, path): + if self.root is None: + if not os.path.isabs(path): + raise IOError("Can't make absolute path when root is None") + else: + path = os.path.join(self.root, path) + + return os.path.normpath(path) + def make_compilation(self): return Compilation(self) @@ -145,8 +172,11 @@ class Compiler(object): else: raise - def compile(self): + def compile(self, *filenames): compilation = self.make_compilation() + for filename in filenames: + source = SourceFile.from_filename(self.normalize_path(filename)) + compilation.add_source(source) return self.call_and_catch_errors(compilation.run) @@ -156,8 +186,13 @@ class Compilation(object): self.compiler = compiler # TODO this needs a write barrier, so assignment can't overwrite what's - # in the original namespace - self.root_namespace = compiler.namespace + # in the original namespaces + # TODO or maybe the extensions themselves should take care of that, so + # it IS possible to overwrite from within sass, but only per-instance? + self.root_namespace = Namespace.derive_from(*( + ext.namespace for ext in compiler.extensions + if ext.namespace + )) self.sources = [] self.source_index = {} diff --git a/scss/functions/core.py b/scss/core.py index 1fc830b..1f33514 100644 --- a/scss/functions/core.py +++ b/scss/core.py @@ -1,22 +1,25 @@ -"""Functions from the Sass "standard library", i.e., built into the original -Ruby implementation. -""" +"""Extension for built-in Sass functionality.""" from __future__ import absolute_import from __future__ import division -from __future__ import unicode_literals +from __future__ import print_function -import logging import math from six.moves import xrange -from scss.functions.library import FunctionLibrary -from scss.types import Arglist, Boolean, Color, List, Null, Number, String, Map, expect_type +from scss.extension import Extension +from scss.namespace import Namespace +from scss.types import ( + Arglist, Boolean, Color, List, Null, Number, String, Map, expect_type) -log = logging.getLogger(__name__) -CORE_LIBRARY = FunctionLibrary() -register = CORE_LIBRARY.register +class CoreExtension(Extension): + name = 'core' + namespace = Namespace() + + +# Alias to make the below declarations less noisy +ns = CoreExtension.namespace # ------------------------------------------------------------------------------ @@ -39,7 +42,7 @@ def _interpret_percentage(n, relto=1., clamp=True): return ret -@register('rgba', 4) +@ns.declare def rgba(r, g, b, a): r = _interpret_percentage(r, relto=255) g = _interpret_percentage(g, relto=255) @@ -49,14 +52,13 @@ def rgba(r, g, b, a): return Color.from_rgb(r, g, b, a) -@register('rgb', 3) +@ns.declare def rgb(r, g, b, type='rgb'): return rgba(r, g, b, Number(1.0)) -@register('rgba', 1) -@register('rgba', 2) -def rgba2(color, a=None): +@ns.declare +def rgba_(color, a=None): if a is None: alpha = 1 else: @@ -65,12 +67,12 @@ def rgba2(color, a=None): return Color.from_rgb(*color.rgba[:3], alpha=alpha) -@register('rgb', 1) -def rgb1(color): +@ns.declare +def rgb_(color): return rgba2(color, a=Number(1)) -@register('hsla', 4) +@ns.declare def hsla(h, s, l, a): return Color.from_hsl( h.value / 360 % 1, @@ -82,24 +84,22 @@ def hsla(h, s, l, a): ) -@register('hsl', 3) +@ns.declare def hsl(h, s, l): return hsla(h, s, l, Number(1)) -@register('hsla', 1) -@register('hsla', 2) -def hsla2(color, a=None): +@ns.declare +def hsla_(color, a=None): return rgba2(color, a) -@register('hsl', 1) -def hsl1(color): +@ns.declare +def hsl_(color): return rgba2(color, a=Number(1)) -@register('mix', 2) -@register('mix', 3) +@ns.declare def mix(color1, color2, weight=Number(50, "%")): """ Mixes together two colors. Specifically, takes the average of each of the @@ -136,8 +136,9 @@ def mix(color1, color2, weight=Number(50, "%")): # to get the combined weight (in [-1, 1]) of color1. # This formula has two especially nice properties: # - # * When either w or a are -1 or 1, the combined weight is also that number - # (cases where w * a == -1 are undefined, and handled as a special case). + # * When either w or a are -1 or 1, the combined weight is also that + # number (cases where w * a == -1 are undefined, and handled as a + # special case). # # * When a is 0, the combined weight is w, and vice versa # @@ -176,60 +177,65 @@ def mix(color1, color2, weight=Number(50, "%")): # ------------------------------------------------------------------------------ # Color inspection -@register('red', 1) +@ns.declare def red(color): r, g, b, a = color.rgba return Number(r * 255) -@register('green', 1) +@ns.declare def green(color): r, g, b, a = color.rgba return Number(g * 255) -@register('blue', 1) +@ns.declare def blue(color): r, g, b, a = color.rgba return Number(b * 255) -@register('opacity', 1) -@register('alpha', 1) +@ns.declare_alias('opacity') +@ns.declare def alpha(color): return Number(color.alpha) -@register('hue', 1) +@ns.declare def hue(color): h, s, l = color.hsl return Number(h * 360, "deg") -@register('saturation', 1) +@ns.declare def saturation(color): h, s, l = color.hsl return Number(s * 100, "%") -@register('lightness', 1) +@ns.declare def lightness(color): h, s, l = color.hsl return Number(l * 100, "%") -@register('ie-hex-str', 1) +@ns.declare def ie_hex_str(color): c = Color(color).value - return String('#%02X%02X%02X%02X' % (round(c[3] * 255), round(c[0]), round(c[1]), round(c[2]))) + return String("#{3:02X}{0:02X}{1:02X}{2:02X}".format( + int(round(c[0])), + int(round(c[1])), + int(round(c[2])), + int(round(c[3] * 255)), + )) # ------------------------------------------------------------------------------ # Color modification -@register('fade-in', 2) -@register('fadein', 2) -@register('opacify', 2) +@ns.declare_alias('fade-in') +@ns.declare_alias('fadein') +@ns.declare def opacify(color, amount): r, g, b, a = color.rgba if amount.is_simple_unit('%'): @@ -241,9 +247,9 @@ def opacify(color, amount): alpha=a + amt) -@register('fade-out', 2) -@register('fadeout', 2) -@register('transparentize', 2) +@ns.declare_alias('fade-out') +@ns.declare_alias('fadeout') +@ns.declare def transparentize(color, amount): r, g, b, a = color.rgba if amount.is_simple_unit('%'): @@ -255,92 +261,94 @@ def transparentize(color, amount): alpha=a - amt) -@register('lighten', 2) +@ns.declare def lighten(color, amount): return adjust_color(color, lightness=amount) -@register('darken', 2) +@ns.declare def darken(color, amount): return adjust_color(color, lightness=-amount) -@register('saturate', 2) +@ns.declare def saturate(color, amount): return adjust_color(color, saturation=amount) -@register('desaturate', 2) +@ns.declare def desaturate(color, amount): return adjust_color(color, saturation=-amount) -@register('greyscale', 1) +@ns.declare def greyscale(color): h, s, l = color.hsl return Color.from_hsl(h, 0, l, alpha=color.alpha) -@register('grayscale', 1) +@ns.declare def grayscale(color): if isinstance(color, Number): - # grayscale(n) and grayscale(n%) are CSS3 filters and should be left intact, but only - # when using the "a" spelling + # grayscale(n) and grayscale(n%) are CSS3 filters and should be left + # intact, but only when using the "a" spelling return String.unquoted("grayscale(%s)" % (color.render(),)) else: return greyscale(color) -@register('spin', 2) -@register('adjust-hue', 2) +@ns.declare_alias('spin') +@ns.declare def adjust_hue(color, degrees): h, s, l = color.hsl delta = degrees.value / 360 return Color.from_hsl((h + delta) % 1, s, l, alpha=color.alpha) -@register('complement', 1) +@ns.declare def complement(color): h, s, l = color.hsl return Color.from_hsl((h + 0.5) % 1, s, l, alpha=color.alpha) -@register('invert', 1) +@ns.declare def invert(color): - """ - Returns the inverse (negative) of a color. - The red, green, and blue values are inverted, while the opacity is left alone. + """Returns the inverse (negative) of a color. The red, green, and blue + values are inverted, while the opacity is left alone. """ r, g, b, a = color.rgba return Color.from_rgb(1 - r, 1 - g, 1 - b, alpha=a) -@register('adjust-lightness', 2) +@ns.declare def adjust_lightness(color, amount): return adjust_color(color, lightness=amount) -@register('adjust-saturation', 2) +@ns.declare def adjust_saturation(color, amount): return adjust_color(color, saturation=amount) -@register('scale-lightness', 2) +@ns.declare def scale_lightness(color, amount): return scale_color(color, lightness=amount) -@register('scale-saturation', 2) +@ns.declare def scale_saturation(color, amount): return scale_color(color, saturation=amount) -@register('adjust-color') -def adjust_color(color, red=None, green=None, blue=None, hue=None, saturation=None, lightness=None, alpha=None): +@ns.declare +def adjust_color( + color, red=None, green=None, blue=None, + hue=None, saturation=None, lightness=None, alpha=None): do_rgb = red or green or blue do_hsl = hue or saturation or lightness if do_rgb and do_hsl: - raise ValueError("Can't adjust both RGB and HSL channels at the same time") + raise ValueError( + "Can't adjust both RGB and HSL channels at the same time") zero = Number(0) a = color.alpha + (alpha or zero).value @@ -378,12 +386,15 @@ def _scale_channel(channel, scaleby): return channel * (1 + factor) -@register('scale-color') -def scale_color(color, red=None, green=None, blue=None, saturation=None, lightness=None, alpha=None): +@ns.declare +def scale_color( + color, red=None, green=None, blue=None, + saturation=None, lightness=None, alpha=None): do_rgb = red or green or blue do_hsl = saturation or lightness if do_rgb and do_hsl: - raise ValueError("Can't scale both RGB and HSL channels at the same time") + raise ValueError( + "Can't scale both RGB and HSL channels at the same time") scaled_alpha = _scale_channel(color.alpha, alpha) @@ -396,16 +407,20 @@ def scale_color(color, red=None, green=None, blue=None, saturation=None, lightne else: channels = [ _scale_channel(channel, scaleby) - for channel, scaleby in zip(color.hsl, (None, saturation, lightness))] + for channel, scaleby + in zip(color.hsl, (None, saturation, lightness))] return Color.from_hsl(*channels, alpha=scaled_alpha) -@register('change-color') -def change_color(color, red=None, green=None, blue=None, hue=None, saturation=None, lightness=None, alpha=None): +@ns.declare +def change_color( + color, red=None, green=None, blue=None, + hue=None, saturation=None, lightness=None, alpha=None): do_rgb = red or green or blue do_hsl = hue or saturation or lightness if do_rgb and do_hsl: - raise ValueError("Can't change both RGB and HSL channels at the same time") + raise ValueError( + "Can't change both RGB and HSL channels at the same time") if alpha is None: alpha = color.alpha @@ -441,9 +456,9 @@ def change_color(color, red=None, green=None, blue=None, hue=None, saturation=No # ------------------------------------------------------------------------------ # String functions -@register('e', 1) -@register('escape', 1) -@register('unquote') +@ns.declare_alias('e') +@ns.declare_alias('escape') +@ns.declare def unquote(*args): arg = List.from_maybe_starargs(args).maybe() @@ -453,7 +468,7 @@ def unquote(*args): return String(arg.render(), quotes=None) -@register('quote') +@ns.declare def quote(*args): arg = List.from_maybe_starargs(args).maybe() @@ -463,7 +478,7 @@ def quote(*args): return String(arg.render(), quotes='"') -@register('str-length', 1) +@ns.declare def str_length(string): expect_type(string, String) @@ -474,7 +489,7 @@ def str_length(string): # TODO this and several others should probably also require integers # TODO and assert that the indexes are valid -@register('str-insert', 3) +@ns.declare def str_insert(string, insert, index): expect_type(string, String) expect_type(insert, String) @@ -482,13 +497,11 @@ def str_insert(string, insert, index): py_index = index.to_python_index(len(string.value), check_bounds=False) return String( - string.value[:py_index] + - insert.value + - string.value[py_index:], + string.value[:py_index] + insert.value + string.value[py_index:], quotes=string.quotes) -@register('str-index', 2) +@ns.declare def str_index(string, substring): expect_type(string, String) expect_type(substring, String) @@ -497,8 +510,7 @@ def str_index(string, substring): return Number(string.value.find(substring.value) + 1) -@register('str-slice', 2) -@register('str-slice', 3) +@ns.declare def str_slice(string, start_at, end_at=None): expect_type(string, String) expect_type(start_at, Number, unit=None) @@ -516,14 +528,14 @@ def str_slice(string, start_at, end_at=None): quotes=string.quotes) -@register('to-upper-case', 1) +@ns.declare def to_upper_case(string): expect_type(string, String) return String(string.value.upper(), quotes=string.quotes) -@register('to-lower-case', 1) +@ns.declare def to_lower_case(string): expect_type(string, String) @@ -533,15 +545,16 @@ def to_lower_case(string): # ------------------------------------------------------------------------------ # Number functions -@register('percentage', 1) +@ns.declare def percentage(value): expect_type(value, Number, unit=None) return value * Number(100, unit='%') -CORE_LIBRARY.add(Number.wrap_python_function(abs), 'abs', 1) -CORE_LIBRARY.add(Number.wrap_python_function(round), 'round', 1) -CORE_LIBRARY.add(Number.wrap_python_function(math.ceil), 'ceil', 1) -CORE_LIBRARY.add(Number.wrap_python_function(math.floor), 'floor', 1) + +ns.set_function('abs', 1, Number.wrap_python_function(abs)) +ns.set_function('round', 1, Number.wrap_python_function(round)) +ns.set_function('ceil', 1, Number.wrap_python_function(math.ceil)) +ns.set_function('floor', 1, Number.wrap_python_function(math.floor)) # ------------------------------------------------------------------------------ @@ -568,15 +581,15 @@ def __parse_separator(separator, default_from=None): # TODO get the compass bit outta here -@register('-compass-list-size') -@register('length') -def _length(*lst): +@ns.declare_alias('-compass-list-size') +@ns.declare +def length(*lst): if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)): lst = lst[0] return Number(len(lst)) -@register('set-nth', 3) +@ns.declare def set_nth(list, n, value): expect_type(n, Number, unit=None) @@ -587,8 +600,8 @@ def set_nth(list, n, value): # TODO get the compass bit outta here -@register('-compass-nth', 2) -@register('nth', 2) +@ns.declare_alias('-compass-nth') +@ns.declare def nth(lst, n): """Return the nth item in the list.""" expect_type(n, (String, Number), unit=None) @@ -607,8 +620,7 @@ def nth(lst, n): return lst[i] -@register('join', 2) -@register('join', 3) +@ns.declare def join(lst1, lst2, separator=None): ret = [] ret.extend(List.from_maybe(lst1)) @@ -618,22 +630,21 @@ def join(lst1, lst2, separator=None): return List(ret, use_comma=use_comma) -@register('min') +@ns.declare def min_(*lst): if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)): lst = lst[0] return min(lst) -@register('max') +@ns.declare def max_(*lst): if len(lst) == 1 and isinstance(lst[0], (list, tuple, List)): lst = lst[0] return max(lst) -@register('append', 2) -@register('append', 3) +@ns.declare def append(lst, val, separator=None): ret = [] ret.extend(List.from_maybe(lst)) @@ -643,7 +654,7 @@ def append(lst, val, separator=None): return List(ret, use_comma=use_comma) -@register('index', 2) +@ns.declare def index(lst, val): for i in xrange(len(lst)): if lst.value[i] == val: @@ -651,7 +662,7 @@ def index(lst, val): return Boolean(False) -@register('zip') +@ns.declare def zip_(*lists): return List( [List(zipped) for zipped in zip(*lists)], @@ -659,7 +670,7 @@ def zip_(*lists): # TODO need a way to use "list" as the arg name without shadowing the builtin -@register('list-separator', 1) +@ns.declare def list_separator(list): if list.use_comma: return String.unquoted('comma') @@ -670,12 +681,12 @@ def list_separator(list): # ------------------------------------------------------------------------------ # Map functions -@register('map-get', 2) +@ns.declare def map_get(map, key): return map.to_dict().get(key, Null()) -@register('map-merge', 2) +@ns.declare def map_merge(*maps): key_order = [] index = {} @@ -690,34 +701,33 @@ def map_merge(*maps): return Map(pairs, index=index) -@register('map-keys', 1) +@ns.declare def map_keys(map): return List( [k for (k, v) in map.to_pairs()], use_comma=True) -@register('map-values', 1) +@ns.declare def map_values(map): return List( [v for (k, v) in map.to_pairs()], use_comma=True) -@register('map-has-key', 2) +@ns.declare def map_has_key(map, key): return Boolean(key in map.to_dict()) # DEVIATIONS: these do not exist in ruby sass -@register('map-get', 3) +@ns.declare def map_get3(map, key, default): return map.to_dict().get(key, default) -@register('map-get-nested', 2) -@register('map-get-nested', 3) +@ns.declare def map_get_nested3(map, keys, default=Null()): for key in keys: map = map.to_dict().get(key, None) @@ -727,7 +737,7 @@ def map_get_nested3(map, keys, default=Null()): return map -@register('map-merge-deep', 2) +@ns.declare def map_merge_deep(*maps): pairs = [] keys = set() @@ -749,12 +759,12 @@ def map_merge_deep(*maps): # ------------------------------------------------------------------------------ # Meta functions -@register('type-of', 1) -def _type_of(obj): # -> bool, number, string, color, list +@ns.declare +def type_of(obj): # -> bool, number, string, color, list return String(obj.sass_type_name) -@register('unit', 1) +@ns.declare def unit(number): # -> px, em, cm, etc. numer = '*'.join(sorted(number.unit_numer)) denom = '*'.join(sorted(number.unit_denom)) @@ -766,7 +776,7 @@ def unit(number): # -> px, em, cm, etc. return String.unquoted(ret) -@register('unitless', 1) +@ns.declare def unitless(value): if not isinstance(value, Number): raise TypeError("Expected number, got %r" % (value,)) @@ -774,7 +784,7 @@ def unitless(value): return Boolean(value.is_unitless) -@register('comparable', 2) +@ns.declare def comparable(number1, number2): left = number1.to_base_units() right = number2.to_base_units() @@ -783,7 +793,7 @@ def comparable(number1, number2): and left.unit_denom == right.unit_denom) -@register('keywords', 1) +@ns.declare def keywords(value): """Extract named arguments, as a map, from an argument list.""" expect_type(value, Arglist) @@ -793,7 +803,6 @@ def keywords(value): # ------------------------------------------------------------------------------ # Miscellaneous -@register('if', 2) -@register('if', 3) +@ns.declare def if_(condition, if_true, if_false=Null()): return if_true if condition else if_false diff --git a/scss/extension.py b/scss/extension.py new file mode 100644 index 0000000..396b296 --- /dev/null +++ b/scss/extension.py @@ -0,0 +1,40 @@ +"""Support for extending the Sass compiler.""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + + +class Extension(object): + """An extension to the Sass compile process. Subclass to add your own + behavior. + + Methods are hooks, called by the compiler at certain points. Each + extension is considered in the order it's provided. + """ + + # TODO unsure how this could work given that we'd have to load modules for + # it to be available + name = None + """A unique name for this extension, which will allow it to be referenced + from the command line. + """ + + namespace = None + """An optional :class:`scss.namespace.Namespace` that will be injected into + the compiler. + """ + + def __init__(self): + pass + + def before_import(self): + pass + + +class NamespaceAdapterExtension(Extension): + """Trivial wrapper that adapts a bare :class:`scss.namespace.Namespace` + into a full extension. + """ + + def __init__(self, namespace): + self.namespace = namespace diff --git a/scss/functions/__init__.py b/scss/functions/__init__.py index 484b8bf..ab6fdd2 100644 --- a/scss/functions/__init__.py +++ b/scss/functions/__init__.py @@ -5,6 +5,7 @@ from scss.functions.library import FunctionLibrary from scss.functions.core import CORE_LIBRARY from scss.functions.extra import EXTRA_LIBRARY from scss.functions.fonts import FONTS_LIBRARY +from scss.functions.compass.configuration import ns from scss.functions.compass.sprites import COMPASS_SPRITES_LIBRARY from scss.functions.compass.gradients import COMPASS_GRADIENTS_LIBRARY from scss.functions.compass.helpers import COMPASS_HELPERS_LIBRARY @@ -12,18 +13,11 @@ from scss.functions.compass.images import COMPASS_IMAGES_LIBRARY from scss.functions.bootstrap import BOOTSTRAP_LIBRARY -ALL_BUILTINS_LIBRARY = FunctionLibrary() -ALL_BUILTINS_LIBRARY.inherit( - CORE_LIBRARY, - EXTRA_LIBRARY, - FONTS_LIBRARY, +COMPASS_LIBRARY = FunctionLibrary() +COMPASS_LIBRARY.inherit( COMPASS_GRADIENTS_LIBRARY, COMPASS_HELPERS_LIBRARY, COMPASS_IMAGES_LIBRARY, COMPASS_SPRITES_LIBRARY, - BOOTSTRAP_LIBRARY, + ns, ) - -# TODO back-compat for the only codebase still using the old name :) -FunctionRegistry = FunctionLibrary -scss_builtins = ALL_BUILTINS_LIBRARY diff --git a/scss/legacy.py b/scss/legacy.py index 095d2ce..12e225d 100644 --- a/scss/legacy.py +++ b/scss/legacy.py @@ -7,7 +7,6 @@ import six from scss.compiler import Compiler import scss.config as config -from scss.functions import ALL_BUILTINS_LIBRARY from scss.expression import Calculator from scss.namespace import Namespace from scss.scss_meta import ( @@ -49,7 +48,7 @@ class Scss(object): def __init__( self, scss_vars=None, scss_opts=None, scss_files=None, super_selector='', live_errors=False, - library=ALL_BUILTINS_LIBRARY, func_registry=None, + library=None, search_paths=None): self.super_selector = super_selector @@ -69,9 +68,7 @@ class Scss(object): self._scss_opts = scss_opts or {} self._scss_files = scss_files - # NOTE: func_registry is backwards-compatibility for only one user and - # has never existed in a real release - self._library = func_registry or library + self._library = library self._search_paths = search_paths # If true, swallow compile errors and embed them in the output instead @@ -92,7 +89,6 @@ class Scss(object): root_namespace = Namespace( variables=self.scss_vars, - functions=self._library, ) # Figure out search paths. Fall back from provided explicitly to diff --git a/scss/namespace.py b/scss/namespace.py index f469c46..6c0ae14 100644 --- a/scss/namespace.py +++ b/scss/namespace.py @@ -3,6 +3,7 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function +import inspect import logging import six @@ -145,6 +146,42 @@ class Namespace(object): """ return type(self).derive_from(self) + def declare(self, function): + """Insert a Python function into this Namespace, detecting its name and + argument count automatically. + """ + self._auto_register_function(function, function.__name__) + return function + + def declare_alias(self, name): + """Insert a Python function into this Namespace with an + explicitly-given name, but detect its argument count automatically. + """ + def decorator(f): + self._auto_register_function(f, name) + return f + + return decorator + + def _auto_register_function(self, function, name): + name = name.replace('_', '-').rstrip('-') + argspec = inspect.getargspec(function) + + if argspec.varargs or argspec.keywords: + # Accepts some arbitrary number of arguments + arities = [None] + else: + # Accepts a fixed range of arguments + if argspec.defaults: + num_optional = len(argspec.defaults) + else: + num_optional = 0 + num_args = len(argspec.args) + arities = range(num_args - num_optional, num_args + 1) + + for arity in arities: + self.set_function(name, arity, function) + @property def variables(self): return dict((k, self._variables[k]) for k in self._variables.keys()) diff --git a/scss/tests/functions/test_core.py b/scss/tests/functions/test_core.py index a16b447..1d7c64d 100644 --- a/scss/tests/functions/test_core.py +++ b/scss/tests/functions/test_core.py @@ -7,9 +7,8 @@ from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals +from scss.core import CoreExtension from scss.expression import Calculator -from scss.functions.core import CORE_LIBRARY -from scss.rule import Namespace from scss.types import Color, Number, String import pytest @@ -20,8 +19,7 @@ xfail = pytest.mark.xfail @pytest.fixture def calc(): - ns = Namespace(functions=CORE_LIBRARY) - return Calculator(ns).evaluate_expression + return Calculator(CoreExtension.namespace).evaluate_expression # ------------------------------------------------------------------------------ diff --git a/scss/tests/test_expression.py b/scss/tests/test_expression.py index 8c7a82a..0c607da 100644 --- a/scss/tests/test_expression.py +++ b/scss/tests/test_expression.py @@ -4,9 +4,9 @@ contains a variety of expression-related tests. from __future__ import absolute_import from __future__ import unicode_literals +from scss.core import CoreExtension from scss.errors import SassEvaluationError from scss.expression import Calculator -from scss.functions.core import CORE_LIBRARY from scss.rule import Namespace from scss.types import Color, List, Null, Number, String @@ -33,7 +33,7 @@ def test_reference_operations(): # Need to build the calculator manually to get at its namespace, and need # to use calculate() instead of evaluate_expression() so interpolation # works - ns = Namespace(functions=CORE_LIBRARY) + ns = CoreExtension.namespace.derive() calc = Calculator(ns).calculate # Simple example @@ -81,8 +81,7 @@ def test_parse(calc): def test_functions(calc): - ns = Namespace(functions=CORE_LIBRARY) - calc = Calculator(ns).calculate + calc = Calculator(CoreExtension.namespace).calculate assert calc('grayscale(red)') == Color.from_rgb(0.5, 0.5, 0.5) assert calc('grayscale(1)') == String('grayscale(1)', quotes=None) # Misusing css built-in functions (with scss counterpart) |