Commits

jason kirtland  committed 1b54552 Merge

Merged with transifex.

  • Participants
  • Parent commits 77611c2, 8cc696e

Comments (0)

Files changed (9)

File flatland/schema/base.py

 import itertools
 import operator
 from flatland.schema.paths import pathexpr
+from flatland.schema.properties import Properties
 from flatland.signals import validator_validated
 from flatland.util import (
     Unspecified,
     restricted to validation routines.
     """
 
+    properties = Properties()
+    """A mapping of arbitrary data associated with the element."""
+
     flattenable = False
     children_flattenable = True
     validates_down = None
         cls.validators = mutable
         return cls
 
+    @class_cloner
+    def with_properties(cls, **properties):
+        """TODO: doc"""
+        cls.properties.update(properties)
+        return cls
+
     def validate_element(self, element, state, descending):
         """Assess the validity of an element.
 

File flatland/schema/paths.py

 
 TOP = symbol('TOP')
 UP = symbol('UP')
+HERE = symbol('HERE')
 SLICE = symbol('SLICE')
 NAME = symbol('NAME')
 
       (?<!\\)\[(-?\d*:?-?\d*\:?-?\d*)(?<!\\)\](?=$|/|\[)
     | # [bogus]
       (?<!\\)\[[^\]]*(?<!\\)\](?=$|/|\[)
-    | # .. at start
-      ^\.\.
-    | # .. in expression
-      (?<=[^\\]/)\.\.
+    | # . or .. at start
+      ^\.\.?
+    | # . or .. in expression
+      (?<=[^\\]/)\.\.?
     |
       \[
     )
                 elif op is UP:
                     if el.parent:
                         el = el.parent
+                elif op is HERE:
+                    pass
                 elif op is NAME:
                     try:
                         el = el._index(data)
     """Parse *path* and return a list of (OP, data) pairs."""
     tokens = []
     last, last_type = None, None
-    upped = False
+    canonical = True
 
     for token, slice_spec in _tokenize_re.findall(path):
         if token == '/':
             elif last == '/':
                 tokens.append((NAME, None))
 
+        # . -> here
+        elif token == '.':
+            canonical = False
+            tokens.append((HERE, None))
+
         # foo/../bar -> 'foo', up, 'bar'
         elif token == '..':
-            upped = True
+            canonical = False
             tokens.append((UP, None))
 
         # [:] or [123] or [1:] or [5] etc.
         last = token
         last_type = tokens[-1][0]
 
-    if not upped:
+    if canonical:
         return tokens
     # foo/../bar -> bar
     return _canonicalize(tokens)
 
 
 def _canonicalize(tokens):
-    """Collapse redundant steps from token lists containing UP ops."""
+    """Collapse redundant steps from token lists containing UP or HERE ops."""
     canonical = []
     for token in tokens:
+        if token[0] is HERE and len(tokens) > 1:
+            continue
         if token[0] is not UP or not canonical:
             canonical.append(token)
             continue

File flatland/schema/properties.py

+from flatland.util import symbol
+
+
+Deleted = symbol('deleted')
+
+
+class DictLike(object):
+
+    def iteritems(self):  # pragma: nocover
+        raise NotImplementedError
+
+    def items(self):
+        return list(self.iteritems())
+
+    def iterkeys(self):
+        return (item[0] for item in self.iteritems())
+
+    def keys(self):
+        return list(self.iterkeys())
+
+    def itervalues(self):
+        return (item[1] for item in self.iteritems())
+
+    def values(self):
+        return list(self.itervalues())
+
+    def get(self, key, default=None):
+        try:
+            return self[key]
+        except KeyError:
+            return default
+
+    def copy(self):
+        return dict(self.iteritems())
+
+    def popitem(self):
+        raise NotImplementedError
+
+    def __contains__(self, key):
+        return key in self.iterkeys()
+
+    def __nonzero__(self):
+        return bool(self.copy())
+
+    def __eq__(self, other):
+        return self.copy() == other
+
+    def __ne__(self, other):
+        return self.copy() != other
+
+    def __repr__(self):
+        return repr(self.copy())
+
+
+class _TypeLookup(DictLike):
+    __slots__ = 'base', 'map', 'descriptor_id'
+
+    def __init__(self, cls, descriptor):
+        self.base = cls
+        self.map = descriptor.map
+        self.descriptor_id = id(descriptor)
+
+    def __getitem__(self, key):
+        for frame in self._frames():
+            try:
+                value = frame[key]
+            except KeyError:
+                pass
+            else:
+                if value is Deleted:
+                    raise KeyError(key)
+                return value
+        raise KeyError(key)
+
+    def __setitem__(self, key, value):
+        self._base_frame[key] = value
+
+    def __delitem__(self, key):
+        self[key]  # must exist to delete
+        self._base_frame[key] = Deleted
+
+    def clear(self):
+        frame = self._base_frame
+        for key in self.iterkeys():
+            frame[key] = Deleted
+
+    def pop(self, key, *default):
+        try:
+            current = self[key]
+        except KeyError:
+            if not default:
+                raise KeyError(key)
+            return default[0]
+        self[key] = Deleted
+        return current
+
+    def setdefault(self, key, default):
+        return self._base_frame.setdefault(key, default)
+
+    def update(self, *iterable, **values):
+        simplified = dict(*iterable, **values)
+        self._base_frame.update(simplified)
+
+    def iteritems(self):
+        seen = set()
+        for frame in self._frames():
+            for key, value in frame.iteritems():
+                if key not in seen:
+                    seen.add(key)
+                    if value is not Deleted:
+                        yield (key, value)
+
+    def _frames(self):
+        for cls in self.base.__mro__:
+            member = cls.__dict__.get('properties')
+            if cls not in self.map:
+                if member is None or id(member) != self.descriptor_id:
+                    continue
+                self.map.setdefault(cls, member.initial_set)
+            yield self.map[cls]
+            if member is not None and id(member) == self.descriptor_id:
+                break
+
+    @property
+    def _base_frame(self):
+        try:
+            return self.map[self.base]
+        except KeyError:
+            pass
+        if 'properties' in self.base.__dict__:
+            member = self.base.__dict__['properties']
+            if id(member) == self.descriptor_id:
+                return self.map.setdefault(self.base, member.initial_set)
+        return self.map.setdefault(self.base, {})
+
+
+class local_storage(dict):
+    """A marker type for local storage overlays."""
+
+
+class _InstanceLookup(DictLike):
+    __slots__ = 'local', 'class_lookup'
+
+    def __init__(self, instance, class_lookup):
+        try:
+            local = instance.__dict__.setdefault('properties', local_storage())
+        except AttributeError:
+            # Descriptor not supported for slots types.
+            raise AttributeError(
+                "%s object has no attribute 'properties'" % (
+                    instance.__class__))
+        self.local = local
+        self.class_lookup = class_lookup
+
+    def __getitem__(self, key):
+        try:
+            value = self.local[key]
+        except KeyError:
+            pass
+        else:
+            if value is Deleted:
+                raise KeyError(key)
+            return value
+        return self.class_lookup[key]
+
+    def __setitem__(self, key, value):
+        self.local[key] = value
+
+    def __delitem__(self, key):
+        self[key]  # must exist to delete
+        self.local[key] = Deleted
+
+    def clear(self):
+        self.local.clear()
+        for key in self.class_lookup.keys():
+            self.local[key] = Deleted
+
+    def pop(self, key, *default):
+        try:
+            return self.local.pop(key)
+        except KeyError:
+            return self.class_lookup.pop(key, *default)
+
+    def setdefault(self, key, default):
+        try:
+            return self[key]
+        except KeyError:
+            return self.local.setdefault(key, default)
+
+    def update(self, *iterable, **values):
+        simplified = dict(*iterable, **values)
+        self.local.update(simplified)
+
+    def iteritems(self):
+        seen = set()
+        for key, value in self.local.iteritems():
+            seen.add(key)
+            if value is not Deleted:
+                yield key, value
+        for key, value in self.class_lookup.iteritems():
+            if key not in seen:
+                seen.add(key)
+                if value is not Deleted:  # pragma: nocover  (coverage bug)
+                    yield key, value
+
+
+class Properties(object):
+
+    def __init__(self, *iterable, **initial_set):
+        simplified = dict(*iterable, **initial_set)
+        self.initial_set = simplified
+        self.map = {}
+
+    def __get__(self, instance, cls):
+        class_lookup = _TypeLookup(cls, self)
+        if instance is None:
+            return class_lookup
+        try:
+            local = instance.__dict__['properties']
+        except (KeyError, AttributeError):
+            pass
+        else:
+            # wholesale assignments to instances replace the inheritance
+            # routine entirely
+            if type(local) is not local_storage:
+                return local
+        return _InstanceLookup(instance, class_lookup)
+
+    def __set__(self, instance, value):
+        instance.__dict__['properties'] = value

File flatland/validation/number.py

         self.method = method
 
     def validate(self, element, state):
-        npa = element.el(self.npa).value
-        nxx = element.el(self.nxx).value
+        npa = element.find(self.npa, single=True).value
+        nxx = element.find(self.nxx, single=True).value
 
         if self.errors_to:
-            err = element.el(self.errors_to)
+            err = element.find(self.errors_to)
         else:
             err = element
 

File flatland/validation/scalars.py

         Validator.__init__(self, **kw)
 
     def validate(self, element, state):
-        elements = [element.el(name) for name in self.field_paths]
+        elements = [element.find(name, single=True) for name in self.field_paths]
         fn = self.transform
         sample = fn(elements[0])
         if all(fn(el) == sample for el in elements[1:]):
       from flatland.validation import ValuesEqual
 
       class MyForm(flatland.Form):
-          schema = [ String('password'), String('password_again') ]
-          validators = ValuesEqual('password', 'password_again')
+          password = String
+          password_again = String
+          validators = [ValuesEqual('password', 'password_again')]
 
     .. attribute:: transform()
 

File tests/schema/test_paths.py

     SLICE,
     TOP,
     UP,
+    HERE,
     pathexpr,
     tokenize,
     )
 
 top = (TOP, None)
 up = (UP, None)
+here = (HERE, None)
 name = lambda x: (NAME, x)
 sl = lambda x: (SLICE, x)
 
 
 def test_tokenize():
     _tokencases = [
+        ('.', [here]),
         ('/', [top]),
         ('..', [up]),
         ('/..', [top]),
         ('foo/', [name('foo')]),
         ('foo/bar', [name('foo'), name('bar')]),
         ('foo/../bar', [name('bar')]),
+        ('foo/./bar', [name('foo'), name('bar')]),
+        ('foo/./bar/.', [name('foo'), name('bar')]),
         ('foo/bar[bogus]', [name('foo'), name('bar[bogus]')]),
         ('a[b][c:d][0]', [name('a[b][c:d]'), name('0')]),
+        ('.[1]', [name('1')]),
         ('foo[1]', [name('foo'), name('1')]),
         ('foo[1]/', [name('foo'), name('1')]),
+        ('./foo[1]/', [name('foo'), name('1')]),
         ('foo[1][2][3]', [name('foo'), name('1'), name('2'), name('3')]),
         ('[1][2][3]', [name('1'), name('2'), name('3')]),
         ('[1]/foo/[2]', [name('1'), name('foo'), name('2')]),
 
 def test_tokenize_escapes():
     _tokencases = [
+        (r'\.', [name('.')]),
         (r'\/', [name('/')]),
         (r'\.\.', [name('..')]),
         (r'/\.\.', [top, name('..')]),
     today = date.today()
 
     _finders = [
+        (el['i1'], '.', [0]),
         (el, 'i1', [0]),
         (el, '/i1', [0]),
         (el, '../i1', [0]),
+        (el, '../i1/.', [0]),
         (el['i1'], '../i1', [0]),
         (el, 'd1/d1i1', [1]),
         (el, '/d1/d1i2', [2]),
         (el, '/l1[:]', [3, 3]),
         (el, '/l1[2:]', []),
         (el, '/l1[0]', [3]),
+        (el, './l1[0]', [3]),
+        (el, 'l1/.[0]', [3]),
         (el, '/l2[:]/l2i1', [4, 4, 4]),
         (el, '/l3[:]', [[6, 6], [6, 6]]),
         (el, '/l3[:][:]', [6, 6, 6, 6]),
         (el, 'a1[::-2]', [15, 13, 11]),
         (el, 'dt1', [today]),
         (el, 'dt1/year', [today.year]),
+        (el, 'dt1/./year', [today.year]),
         ]
     for element, path, expected in _finders:
         yield _finds, element, path, expected

File tests/schema/test_properties.py

+from flatland.schema.properties import Properties
+from nose.tools import assert_raises
+
+
+def test_empty():
+    class Base(object):
+        properties = Properties()
+
+    assert not Base.properties
+    b = Base()
+    assert not b.properties
+    assert not Base.properties
+
+    assert Base.properties == {}
+    assert Base().properties == {}
+
+    class Sub(Base):
+        pass
+
+    assert Sub.properties == {}
+    assert Sub().properties == {}
+
+    Sub().properties['abc'] = 123
+
+    assert Sub.properties == {}
+    assert Sub().properties == {}
+    assert Base.properties == {}
+    assert Base().properties == {}
+
+    Sub.properties['def'] = 456
+    assert Base.properties == {}
+    assert Base().properties == {}
+
+
+def test_dictlike():
+    class Base(object):
+        properties = Properties({'def': 456}, abc=123)
+
+    props = Base.properties
+    assert sorted(props.items()) == [('abc', 123), ('def', 456)]
+
+    assert sorted(props.keys()) == ['abc', 'def']
+    assert sorted(props.iterkeys()) == ['abc', 'def']
+
+    assert sorted(props.values()) == [123, 456]
+    assert sorted(props.itervalues()) == [123, 456]
+
+    assert props.get('abc') == 123
+    assert props.get('abc', 'blah') == 123
+    assert props.get('blah', 'default') == 'default'
+    assert props.get('blah') is None
+
+    assert_raises(NotImplementedError, props.popitem)
+
+    assert 'abc' in props
+    assert 'ghi' not in props
+
+    assert props == {'abc': 123, 'def': 456}
+    assert props != {'ghi': 789}
+
+    assert props
+    props.clear()
+    assert not props
+
+    assert repr(props) == '{}'
+
+
+def test_instance_population():
+    class Base(object):
+        properties = Properties()
+
+    assert not Base.properties
+    b = Base()
+    b.properties.update(a=1, b=2, c=3)
+
+    assert b.properties == {'a': 1, 'b': 2, 'c': 3}
+    assert Base.properties == {}
+
+    class Sub(Base):
+        pass
+
+    assert Sub.properties == {}
+    s = Sub()
+    assert s.properties == {}
+
+    s.properties['d'] = 4
+    assert s.properties == {'d': 4}
+    assert Sub.properties == {}
+    assert Base.properties == {}
+    assert Sub().properties == {}
+
+
+def test_instance_overlay():
+    class Base(object):
+        properties = Properties()
+
+    Base.properties['a'] = 1
+    b = Base()
+    b.properties['b'] = 2
+
+    assert Base.properties == {'a': 1}
+    assert b.properties == {'a': 1, 'b': 2}
+    del b.properties['a']
+    assert b.properties == {'b': 2}
+    assert Base.properties == {'a': 1}
+
+    b.properties.update(b='x', c=3, d=4)
+    assert b.properties['b'] == 'x'
+    assert b.properties == {'b': 'x', 'c': 3, 'd': 4}
+
+    del b.properties['b']
+    assert b.properties == {'c': 3, 'd': 4}
+    assert_raises(KeyError, lambda: b.properties['b'])
+
+    assert b.properties.setdefault('e', 5) == 5
+    assert b.properties.setdefault('e', 'blah') == 5
+
+    assert b.properties == {'c': 3, 'd': 4, 'e': 5}
+
+    assert b.properties.pop('e', 'blah') == 5
+    assert b.properties.pop('e', 'blah') == 'blah'
+    assert_raises(KeyError, b.properties.pop, 'e')
+
+    b.properties.clear()
+    assert b.properties == {}
+    assert Base.properties == {'a': 1}
+
+    Base.properties['b'] = 2
+    assert b.properties == {'b': 2}
+    assert Base.properties == {'a': 1, 'b': 2}
+
+    Base.properties.update(c=3, d=4, e=5)
+    del Base.properties['e']
+    assert b.properties == {'b': 2, 'c': 3, 'd': 4}
+    assert Base.properties == {'a': 1, 'b': 2, 'c': 3, 'd': 4}
+
+
+def test_instance_member_assignment():
+
+    class Base(object):
+        properties = Properties(abc=123)
+
+    b = Base()
+    assert b.properties == {'abc': 123}
+    b.properties = {'abc': 'detached'}
+
+    assert b.properties == {'abc': 'detached'}
+
+    Base.properties['def'] = 456
+
+    assert b.properties == {'abc': 'detached'}
+
+
+def test_subclass_overlay():
+    class Base(object):
+        properties = Properties()
+
+    class Middle(Base):
+        pass
+
+    class Lowest(Middle):
+        pass
+
+    Lowest.properties['def'] = 456
+
+    assert Base.properties == {}
+    assert Middle.properties == {}
+    assert Lowest.properties == {'def': 456}
+
+    Base.properties['abc'] = 123
+
+    assert Base.properties == {'abc': 123}
+    assert Middle.properties == {'abc': 123}
+    assert Lowest.properties == {'abc': 123, 'def': 456}
+
+    del Middle.properties['abc']
+
+    assert Base.properties == {'abc': 123}
+    assert 'abc' in Base.properties
+
+    assert Middle.properties == {}
+    assert 'abc' not in Middle.properties
+    assert_raises(KeyError, lambda: Middle.properties['abc'])
+
+    assert Lowest.properties == {'def': 456}
+    assert 'abc' not in Lowest.properties
+    assert_raises(KeyError, lambda: Lowest.properties['abc'])
+
+    Middle.properties.setdefault('ghi', 789)
+    Middle.properties.setdefault('ghi', 'blah')
+
+    assert Base.properties == {'abc': 123}
+    assert Middle.properties == {'ghi': 789}
+    assert Lowest.properties == {'ghi': 789, 'def': 456}
+
+    assert Lowest.properties.pop('def', 'blah') == 456
+    assert Lowest.properties.pop('def', 'blah') == 'blah'
+    assert_raises(KeyError, Lowest.properties.pop, 'def')
+
+    assert Base.properties == {'abc': 123}
+    assert Middle.properties == {'ghi': 789}
+    assert Lowest.properties == {'ghi': 789}
+
+    Lowest.properties.clear()
+
+    assert Base.properties == {'abc': 123}
+    assert Middle.properties == {'ghi': 789}
+    assert Lowest.properties == {}
+
+
+def test_subclass_override():
+    class Base(object):
+        properties = Properties()
+
+    class Middle(Base):
+        pass
+
+    class Override(Middle):
+        properties = Properties({'def': 456})
+
+    assert Override.properties == {'def': 456}
+    assert Middle.properties == {}
+    assert Base.properties == {}
+
+    Base.properties['abc'] = 123
+
+    assert Base.properties == {'abc': 123}
+    assert Middle.properties == {'abc': 123}
+    assert Override.properties == {'def': 456}
+
+
+def test_initialization():
+    class Base(object):
+        properties = Properties(abc=123)
+
+    assert Base.properties == {'abc': 123}
+    Base.properties['def'] = 456
+
+    assert Base.properties == {'abc': 123, 'def': 456}
+    del Base.properties['abc']
+    assert Base.properties == {'def': 456}
+
+
+def test_perverse():
+    class Base(object):
+        properties = Properties()
+
+    descriptor = Base.__dict__['properties']
+    props = Base.properties
+    del Base.properties
+    assert list(props._frames()) == []
+
+    def unattached_properties():
+        class Unrelated(object):
+            pass
+
+        return descriptor.__get__(None, Unrelated)
+
+    lost = unattached_properties()
+    assert lost == {}
+
+    lost2 = unattached_properties()
+    assert_raises(KeyError, lambda: lost2['abc'])
+
+    class Broken(object):
+        properties = 'something else'
+
+    broken = descriptor.__get__(None, Broken)
+    broken.update(abc=123)
+    assert broken == {'abc': 123}
+    assert Broken.properties == 'something else'
+
+
+def test_perverse_slots():
+
+    class Base(object):
+        __slots__ = 'properties',
+        properties = Properties()
+
+    b = Base()
+    assert_raises(AttributeError, lambda: b.properties['abc'])

File tests/schema/test_scalars.py

                  (-123,      d('-123'),  u'-123.000000'),
                  (d(123),    d('123'),   u'123.000000'),
                  (d(-123),   d('-123'),  u'-123.000000'),
-                 (123.456,   None,       u'123.456'),
                  (u'+123',   d('123'),   u'123.000000', dict(signed=False)),
                  (u'-123',   None,       u'-123', dict(signed=False)),
                  (None,      None,       u'', {}, True)):

File tests/validation/test_scalars.py

 
 
 def test_values_equal_resolution():
-    v = ValuesEqual('x', '.sub.xx')
+    v = ValuesEqual('x', '/sub/xx')
     el = form(dict(x='a', sub=dict(xx='a')))
     assert v.validate(el, None)
 
-    v = ValuesEqual('.x', 'xx')
+    v = ValuesEqual('/x', 'xx')
     el = form(dict(x='a', sub=dict(xx='a')))
     assert v.validate(el['sub'], None)