summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEevee (Alex Munroe) <eevee.git@veekun.com>2013-08-01 17:18:04 -0700
committerEevee (Alex Munroe) <eevee.git@veekun.com>2013-08-01 17:18:04 -0700
commit403451650f875d58ce6d3231808cfeac1db5bda3 (patch)
treed45913d1ce6f440d911a2d7bf2bc9ef0b5414474
parent689178d8350dbc69c47770a4668acd9d2c59f79d (diff)
downloadpyscss-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.py111
-rw-r--r--scss/rule.py50
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):