diff options
author | Eevee (Alex Munroe) <eevee.git@veekun.com> | 2013-08-01 17:18:04 -0700 |
---|---|---|
committer | Eevee (Alex Munroe) <eevee.git@veekun.com> | 2013-08-01 17:18:04 -0700 |
commit | 403451650f875d58ce6d3231808cfeac1db5bda3 (patch) | |
tree | d45913d1ce6f440d911a2d7bf2bc9ef0b5414474 | |
parent | 689178d8350dbc69c47770a4668acd9d2c59f79d (diff) | |
download | pyscss-403451650f875d58ce6d3231808cfeac1db5bda3.tar.gz |
Fix variable scoping to match Sass.
Turns out ChainMap wasn't quite the right thing. Sass defines a
variable the first time it appears, and updates the same variable **in
its original scope** on subsequent assignments.
-rw-r--r-- | scss/compat.py | 111 | ||||
-rw-r--r-- | scss/rule.py | 50 |
2 files changed, 41 insertions, 120 deletions
diff --git a/scss/compat.py b/scss/compat.py deleted file mode 100644 index 007c656..0000000 --- a/scss/compat.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Cross-version Python compatibility. Includes backports of useful stdlib -functionality and minor 2-vs-3 shims. - -Stdlib backports these fall back to the stdlib when possible, though in most -cases they're copied directly from the stdlib anyway. -""" - -### Python 3.3+ only -try: - from collections import ChainMap -except ImportError: - from collections import MutableMapping - - class ChainMap(MutableMapping): - ''' A ChainMap groups multiple dicts (or other mappings) together - to create a single, updateable view. - - The underlying mappings are stored in a list. That list is public and can - accessed or updated using the *maps* attribute. There is no other state. - - Lookups search the underlying mappings successively until a key is found. - In contrast, writes, updates, and deletions only operate on the first - mapping. - - ''' - - def __init__(self, *maps): - '''Initialize a ChainMap by setting *maps* to the given mappings. - If no mappings are provided, a single empty dictionary is used. - - ''' - self.maps = list(maps) or [{}] # always at least one map - - def __missing__(self, key): - raise KeyError(key) - - def __getitem__(self, key): - for mapping in self.maps: - try: - return mapping[key] # can't use 'key in mapping' with defaultdict - except KeyError: - pass - return self.__missing__(key) # support subclasses that define __missing__ - - def get(self, key, default=None): - return self[key] if key in self else default - - def __len__(self): - return len(set().union(*self.maps)) # reuses stored hash values if possible - - def __iter__(self): - return iter(set().union(*self.maps)) - - def __contains__(self, key): - return any(key in m for m in self.maps) - - def __bool__(self): - return any(self.maps) - - ### this is from reprlib, which doesn't exist in 2 - ###@_recursive_repr() - def __repr__(self): - return '{0.__class__.__name__}({1})'.format( - self, ', '.join(map(repr, self.maps))) - - @classmethod - def fromkeys(cls, iterable, *args): - 'Create a ChainMap with a single dict created from the iterable.' - return cls(dict.fromkeys(iterable, *args)) - - def copy(self): - 'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]' - return self.__class__(self.maps[0].copy(), *self.maps[1:]) - - __copy__ = copy - - def new_child(self): # like Django's Context.push() - 'New ChainMap with a new dict followed by all previous maps.' - return self.__class__({}, *self.maps) - - @property - def parents(self): # like Django's Context.pop() - 'New ChainMap from maps[1:].' - return self.__class__(*self.maps[1:]) - - def __setitem__(self, key, value): - self.maps[0][key] = value - - def __delitem__(self, key): - try: - del self.maps[0][key] - except KeyError: - raise KeyError('Key not found in the first mapping: {!r}'.format(key)) - - def popitem(self): - 'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.' - try: - return self.maps[0].popitem() - except KeyError: - raise KeyError('No keys found in the first mapping.') - - def pop(self, key, *args): - 'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].' - try: - return self.maps[0].pop(key, *args) - except KeyError: - raise KeyError('Key not found in the first mapping: {!r}'.format(key)) - - def clear(self): - 'Clear maps[0], leaving maps[1:] intact.' - self.maps[0].clear() diff --git a/scss/rule.py b/scss/rule.py index ffd3b83..a650c4f 100644 --- a/scss/rule.py +++ b/scss/rule.py @@ -1,4 +1,3 @@ -from scss.compat import ChainMap from scss.cssdefs import _has_placeholder_re @@ -8,21 +7,54 @@ def normalize_var(name): else: return name +class VariableScope(object): + """Implements Sass variable scoping. + + Similar to `ChainMap`, except that assigning a new value will replace an + existing value, not mask it. + """ + def __init__(self, *maps): + self.maps = [dict()] + list(maps) + + def __repr__(self): + return "<VariableScope(%s)>" % (', '.join(repr(map) for map in self.maps),) + + def __getitem__(self, key): + for map in self.maps: + if key in map: + return map[key] + + raise KeyError(key) + + def __setitem__(self, key, value): + for map in self.maps: + if key in map: + map[key] = value + return + + self.maps[0][key] = value + + def new_child(self): + return VariableScope(*self.maps) + + class Namespace(object): """...""" def __init__(self, variables=None, functions=None, mixins=None): if variables is None: - self._variables = ChainMap() + self._variables = VariableScope() else: - self._variables = ChainMap(variables) + # TODO parse into sass values once that's a thing, or require them + # all to be + self._variables = VariableScope(variables) if functions is None: - self._functions = ChainMap() + self._functions = VariableScope() else: - self._functions = ChainMap(functions._functions) + self._functions = VariableScope(functions._functions) - self._mixins = ChainMap() + self._mixins = VariableScope() @classmethod def derive_from(cls, *others): @@ -32,9 +64,9 @@ class Namespace(object): self._functions = others[0]._functions.new_child() self._mixins = others[0]._mixins.new_child() else: - self._variables = ChainMap(other._variables for other in others) - self._functions = ChainMap(other._functions for other in others) - self._mixins = ChainMap(other._mixins for other in others) + self._variables = VariableScope(other._variables for other in others) + self._functions = VariableScope(other._functions for other in others) + self._mixins = VariableScope(other._mixins for other in others) return self def derive(self): |