summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEevee (Alex Munroe) <eevee.git@veekun.com>2014-08-27 14:56:41 -0700
committerEevee (Alex Munroe) <eevee.git@veekun.com>2014-08-27 14:56:41 -0700
commit11e2b44c0e7992db0e339f4dd53fe93a16710802 (patch)
tree26be7ac1067fae42bfa5eff3f37a08d267e47c71
parent6a8efa934d9a91c0916ecbc6659f275e25858bc6 (diff)
downloadpyscss-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.py75
-rw-r--r--scss/core.py (renamed from scss/functions/core.py)257
-rw-r--r--scss/extension.py40
-rw-r--r--scss/functions/__init__.py14
-rw-r--r--scss/legacy.py8
-rw-r--r--scss/namespace.py37
-rw-r--r--scss/tests/functions/test_core.py6
-rw-r--r--scss/tests/test_expression.py7
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)