def using(cls, **overrides): """Return a class with attributes set from *\*\*overrides*. :param \*\*overrides: new values for any attributes already present on the class. A ``TypeError`` is raised for unknown attributes. :returns: a new class """ # TODO: See TODO in __init__ if 'validators' in overrides: overrides['validators'] = list(overrides['validators']) if 'properties' in overrides: if not isinstance(overrides['properties'], Properties): overrides['properties'] = Properties(overrides['properties']) for attribute, value in overrides.iteritems(): # TODO: must make better if callable(value): value = staticmethod(value) if hasattr(cls, attribute): setattr(cls, attribute, value) continue raise TypeError("%r is an invalid keyword argument: not a known " "argument or an overridable class property of %s" % (attribute, cls.__name__)) return cls
def test_dsl(): Sub = String.with_properties(abc=123) assert 'abc' not in String.properties assert Sub.properties['abc'] == 123 Disconnected = Sub.using(properties={'def': 456}) assert Disconnected.properties['def'] == 456 assert 'abc' not in Disconnected.properties assert 'def' not in Sub.properties assert 'def' not in String.properties Sub.properties['ghi'] = 789 assert Disconnected.properties == {'def': 456} Disconnected2 = Sub.using(properties=Properties(jkl=123)) assert Disconnected2.properties == {'jkl': 123}
class Element(_BaseElement): """Base class for form fields. A data node that stores a Python and a text value plus added state. """ name = None """The Unicode name of the element.""" optional = False """If True, :meth:`validate` with return True if no value has been set. :attr:`validators` are not called for optional, empty elements. """ validators = () """A sequence of validators, invoked by :meth:`validate`. See `Validation`_ """ default = None """The default value of this element.""" default_factory = None """A callable to generate default element values. Passed an element. *default_factory* will be used preferentially over :attr:`default`. """ ugettext = None """If set, provides translation support to validation messages. See `Message Internationalization`_. """ ungettext = None """If set, provides translation support to validation messages. See `Message Internationalization`_. """ value = None """The element's native Python value. Only validation routines should write this attribute directly: use :meth:`set` to update the element's value. """ raw = Unset """The element's raw, unadapted value from input.""" u = u'' """A Unicode representation of the element's value. As in :attr:`value`, writing directly to this attribute should be restricted to validation routines. """ properties = Properties() """A mapping of arbitrary data associated with the element.""" flattenable = False children_flattenable = True validates_down = None validates_up = None def __init__(self, value=Unspecified, **kw): self.parent = kw.pop('parent', None) self.valid = Unevaluated self.errors = [] self.warnings = [] # FIXME This (and 'using') should also do descent_validators # via lookup - or don't copy at all if 'validators' in kw: kw['validators'] = list(kw['validators']) for attribute, override in kw.items(): if hasattr(self, attribute): setattr(self, attribute, override) else: raise TypeError( "%r is an invalid keyword argument: not a known " "argument or an overridable class property of %s" % (attribute, type(self).__name__)) if value is not Unspecified: self.set(value) @class_cloner def named(cls, name): """Return a class with ``name`` = *name* :param name: a string or None. ``str`` will be converted to ``unicode``. :returns: a new class """ if not isinstance(name, (unicode, NoneType)): name = unicode(name) cls.name = name return cls @class_cloner def using(cls, **overrides): """Return a class with attributes set from *\*\*overrides*. :param \*\*overrides: new values for any attributes already present on the class. A ``TypeError`` is raised for unknown attributes. :returns: a new class """ # TODO: See TODO in __init__ if 'validators' in overrides: overrides['validators'] = list(overrides['validators']) if 'properties' in overrides: if not isinstance(overrides['properties'], Properties): overrides['properties'] = Properties(overrides['properties']) for attribute, value in overrides.iteritems(): # TODO: must make better if callable(value): value = staticmethod(value) if hasattr(cls, attribute): setattr(cls, attribute, value) continue raise TypeError("%r is an invalid keyword argument: not a known " "argument or an overridable class property of %s" % (attribute, cls.__name__)) return cls @class_cloner def validated_by(cls, *validators): """Return a class with validators set to *\*validators*. :param \*validators: one or more validator functions, replacing any validators present on the class. :returns: a new class """ # TODO: See TODO in __init__ for validator in validators: # metaclass gymnastics can fool this assertion. don't do that. if isinstance(validator, type): raise TypeError( "Validator %r is a type, not a callable or instance of a" "validator class. Did you mean %r()?" % (validator, validator)) cls.validators = list(validators) return cls @class_cloner def including_validators(cls, *validators, **kw): """Return a class with additional *\*validators*. :param \*validators: one or more validator functions :param position: defaults to -1. By default, additional validators are placed after existing validators. Use 0 for before, or any other list index to splice in *validators* at that point. :returns: a new class """ position = kw.pop('position', -1) if kw: raise TypeError('including_validators() got an ' 'unexpected keyword argument %r' % (kw.popitem()[0])) mutable = list(cls.validators) if position < 0: position = len(mutable) + 1 + position mutable[position:position] = list(validators) cls.validators = mutable return cls @class_cloner def with_properties(cls, *iterable, **properties): """TODO: doc""" simplified = dict(*iterable, **properties) cls.properties.update(simplified) return cls def validate_element(self, element, state, descending): """Assess the validity of an element. TODO: this method is dead. Evaluate docstring for good bits that should be elsewhere. :param element: an :class:`Element` :param state: may be None, an optional value of supplied to ``element.validate`` :param descending: a boolean, True the first time the element has been seen in this run, False the next :returns: boolean; a truth value or None The :meth:`Element.validate` process visits each element in the tree twice: once heading down the tree, breadth-first, and again heading back up in the reverse direction. Scalar fields will typically validate on the first pass, and containers on the second. Return no value or None to ``pass``, accepting the element as presumptively valid. Exceptions raised by :meth:`validate_element` will not be caught by :meth:`Element.validate`. Directly modifying and normalizing :attr:`Element.value` and :attr:`Element.u` within a validation routine is acceptable. The standard implementation of validate_element is: - If :attr:`element.is_empty` and :attr:`self.optional`, return True. - If :attr:`self.validators` is empty and :attr:`element.is_empty`, return False. - If :attr:`self.validators` is empty and not :attr:`element.is_empty`, return True. - Iterate through :attr:`self.validators`, calling each member with (*element*, *state*). If one returns a false value, stop iterating and return False immediately. - Otherwise return True. """ return validate_element(element, state, self.validators) @classmethod def from_flat(cls, pairs, **kw): """Return a new element with its value initialized from *pairs*. :param \*\*kw: passed through to the :attr:`element_type`. .. testsetup:: import flatland cls = flatland.String pairs = kw = {} This is a convenience constructor for: .. testcode:: element = cls(**kw) element.set_flat(pairs) """ element = cls(**kw) element.set_flat(pairs) return element @classmethod def from_defaults(cls, **kw): """Return a new element with its value initialized from field defaults. :param \*\*kw: passed through to the :attr:`element_type`. .. testsetup:: import flatland cls = flatland.String kw = {} This is a convenience constructor for: .. testcode:: element = cls(**kw) element.set_default() """ element = cls(**kw) element.set_default() return element def __eq__(self, other): try: return self.value == other.value and self.u == other.u except AttributeError: return False def __ne__(self, other): return not self.__eq__(other) @assignable_class_property def label(self, cls): """The label of this element. If unassigned, the *label* will evaluate to the :attr:`name`. """ return cls.name if self is None else self.name def _get_all_valid(self): """True if this element and all children are valid.""" if not self.valid: return False for element in self.all_children: if not element.valid: return False return True def _set_all_valid(self, value): self.valid = value for element in self.all_children: element.valid = value all_valid = property(_get_all_valid, _set_all_valid) del _get_all_valid, _set_all_valid @property def root(self): """The top-most parent of the element.""" try: return list(self.parents)[-1] except IndexError: return self @property def parents(self): """An iterator of all parent elements.""" element = self.parent while element is not None: yield element element = element.parent raise StopIteration() @property def path(self): """An iterator of all elements from root to the Element, inclusive.""" return itertools.chain(reversed(list(self.parents)), (self, )) @property def children(self): """An iterator of immediate child elements.""" return iter(()) @property def all_children(self): """An iterator of all child elements, breadth-first.""" seen, queue = set((id(self), )), collections.deque(self.children) while queue: element = queue.popleft() if id(element) in seen: continue seen.add(id(element)) yield element queue.extend(element.children) def fq_name(self, sep=u'.'): """Return the fully qualified path name of the element. Returns a *sep*-separated string of :meth:`.el` compatible element indexes starting from the :attr:`Element.root` (``.``) down to the element. >>> from flatland import Dict, Integer >>> Point = Dict.named(u'point').of(Integer.named(u'x'), ... Integer.named(u'y')) >>> p = Point(dict(x=10, y=20)) >>> p.name u'point' >>> p.fq_name() u'.' >>> p['x'].name u'x' >>> p['x'].fq_name() u'.x' The index used in a path may not be the :attr:`.name` of the element. For example, sequence members are referenced by their numeric index. >>> from flatland import List, String >>> Addresses = List.named('addresses').of(String.named('address')) >>> form = Addresses([u'uptown', u'downtown']) >>> form.name u'addresses' >>> form.fq_name() u'.' >>> form[0].name u'address' >>> form[0].fq_name() u'.0' """ if self.parent is None: return sep children_of_root = reversed(list(self.parents)[:-1]) parts, mask = [], None for element in list(children_of_root) + [self]: # allow Slot elements to mask the names of their child # e.g. # <List name='l'> <Slot name='0'> <String name='s'> # has an .el()/Python path of just # l.0 # not # l.0.s if isinstance(element, Slot): mask = element.name continue elif mask: parts.append(mask) mask = None continue parts.append(element.name) return sep + sep.join(parts) def find(self, path, single=False, strict=True): """Find child elements by string path. :param path: a /-separated string specifying elements to select, such as 'child/grandchild/greatgrandchild'. Relative & absolute paths are supported, as well as container expansion. See :ref:`path_lookups`. :param single: if true, return a scalar result rather than a list of elements. If no elements match *path*, ``None`` is returned. If multiple elements match, a :exc:`LookupError` is raised. If multiple elements are found and *strict* is false, an unspecified element from the result set is returned. :param strict: defaults to True. If *path* specifies children or sequence indexes that do not exist, a `:ref:`LookupError` is raised. :returns: a list of :class:`Element` instances, an :class:`Element` if *single* is true, or raises :exc:`LookupError`. .. testsetup:: find from flatland import Form, Dict, List, String class Profile(Form): contact = Dict.of(String.named('name'), List.named('addresses'). of(Dict.of(String.named('street1'), String.named('city'))). using(default=1)) form = Profile( {'contact': {'name': 'Obed Marsh', 'addresses': [{'street1': 'Main', 'city': 'Kingsport'}, {'street1': 'Broadway', 'city': 'Dunwich'}]}}) .. doctest:: find >>> cities = form.find('/contact/addresses[:]/city') >>> [el.value for el in cities] [u'Kingsport', u'Dunwich'] >>> form.find('/contact/name', single=True) <String u'name'; value=u'Obed Marsh'> """ expr = pathexpr(path) results = expr(self, strict) if not single: return results elif not results: return None elif len(results) > 1 and strict: raise LookupError("Path %r matched multiple elements; single " "result expected." % (path, )) else: return results[0] def el(self, path, sep=u'.'): """Find a child element by string path. :param path: a *sep*-separated string of element names, or an iterable of names :param sep: optional, a string separator used to parse *path* :returns: an :class:`Element` or raises :exc:`KeyError`. .. testsetup:: el from flatland import Form, Dict, List, String class Profile(Form): contact = Dict.of(List.named('addresses'). of(Dict.of(String.named('street1'), String.named('city'))). using(default=1)) form = Profile.from_defaults() .. doctest:: el >>> first_address = form.el('contact.addresses.0') >>> first_address.el('street1') <String u'street1'; value=None> Given a relative path as above, :meth:`el` searches for a matching path among the element's children. If *path* begins with *sep*, the path is considered fully qualified and the search is resolved from the :attr:`Element.root`. The leading *sep* will always match the root node, regardless of its :attr:`.name`. .. doctest:: el >>> form.el('.contact.addresses.0.city') <String u'city'; value=None> >>> first_address.el('.contact.addresses.0.city') <String u'city'; value=None> """ try: names = list(self._parse_element_path(path, sep)) or () if names[0] is Root: element = self.root names.pop(0) else: element = self while names: element = element._index(names.pop(0)) return element except LookupError: raise KeyError('No element at %r' % (path, )) def _index(self, name): """Return a named child or raise LookupError.""" raise NotImplementedError() @classmethod def _parse_element_path(self, path, sep): if isinstance(path, basestring): if path == sep: return [Root] elif path.startswith(sep): path = path[len(sep):] parts = [Root] else: parts = [] parts.extend(path.split(sep)) return iter(parts) else: return iter(path) # fixme: nuke? if isinstance(path, (list, tuple)) or hasattr(path, 'next'): return path else: assert False return None def add_error(self, message): "Register an error message on this element, ignoring duplicates." if message not in self.errors: self.errors.append(message) def add_warning(self, message): "Register a warning message on this element, ignoring duplicates." if message not in self.warnings: self.warnings.append(message) def flattened_name(self, sep=u'_'): """Return the element's complete flattened name as a string. Joins this element's :attr:`path` with *sep* and returns the fully qualified, flattened name. Encodes all :class:`Container` and other structures into a single string. Example:: >>> import flatland >>> form = flatland.List('addresses', ... flatland.String('address')) >>> element = form() >>> element.set([u'uptown', u'downtown']) >>> element.el('0').value u'uptown' >>> element.el('0').flattened_name() u'addresses_0_address' """ return sep.join(parent.name for parent in self.path if parent.name is not None) def flatten(self, sep=u'_', value=operator.attrgetter('u')): """Export an element hierarchy as a flat sequence of key, value pairs. :arg sep: a string, will join together element names. :arg value: a 1-arg callable called once for each element. Defaults to a callable that returns the :attr:`.u` of each element. Encodes the element hierarchy in a *sep*-separated name string, paired with any representation of the element you like. The default is the Unicode value of the element, and the output of the default :meth:`flatten` can be round-tripped with :meth:`set_flat`. Given a simple form with a string field and a nested dictionary:: >>> from flatland import Dict, String >>> class Nested(Form): ... contact = Dict.of(String.named(u'name'), ... Dict.named(u'address').\ ... of(String.named(u'email'))) ... >>> element = Nested() >>> element.flatten() [(u'contact_name', u''), (u'contact_address_email', u'')] The value of each pair can be customized with the *value* callable:: >>> element.flatten(value=operator.attrgetter('u')) [(u'contact_name', u''), (u'contact_address_email', u'')] >>> element.flatten(value=lambda el: el.value) [(u'contact_name', None), (u'contact_address_email', None)] Solo elements will return a sequence containing a single pair:: >>> element['name'].flatten() [(u'contact_name', u'')] """ if self.flattenable: pairs = [(self.flattened_name(sep), value(self))] else: pairs = [] if self.children_flattenable: pairs.extend((e.flattened_name(sep), value(e)) for e in self.all_children if e.flattenable) return pairs def set(self, value): """Assign the native and Unicode value. Attempts to adapt the given *value* and assigns this element's :attr:`value` and :attr:`u` attributes in tandem. Returns True if the adaptation was successful. If adaptation succeeds, :attr:`value` will contain the adapted native value and :attr:`u` will contain a Unicode serialized version of it. A native value of None will be represented as u'' in :attr:`u`. If adaptation fails, :attr:`value` will be ``None`` and :attr:`u` will contain ``unicode(value)`` or ``u''`` for None. >>> from flatland import Integer >>> el = Integer() >>> el.u, el.value (u'', None) >>> el.set('123') True >>> el.u, el.value (u'123', 123) >>> el.set(456) True >>> el.u, el.value (u'456', 456) >>> el.set('abc') False >>> el.u, el.value (u'abc', None) >>> el.set(None) True >>> el.u, el.value (u'', None) """ raise NotImplementedError() def set_flat(self, pairs, sep=u'_'): """Set element values from pairs, expanding the element tree as needed. Given a sequence of name/value tuples or a dict, build out a structured tree of value elements. """ self.raw = Unset if hasattr(pairs, 'items'): pairs = pairs.items() return self._set_flat(pairs, sep) def _set_flat(self, pairs, sep): raise NotImplementedError() def set_default(self): """set() the element to the schema default.""" raise NotImplementedError() @property def is_empty(self): """True if the element has no value.""" return True if (self.value is None and self.u == u'') else False def validate(self, state=None, recurse=True): """Assess the validity of this element and its children. :param state: optional, will be passed unchanged to all validator callables. :param recurse: if False, do not validate children. :returns: True or False Iterates through this element and all of its children, invoking each element's :meth:`schema.validate_element`. Each element will be visited twice: once heading down the tree, breadth-first, and again heading back up in reverse order. Returns True if all validations pass, False if one or more fail. """ if not recurse: down = self._validate(state, True) if down is Unevaluated: self.valid = down else: self.valid = bool(down) up = self._validate(state, False) # an Unevaluated ascent validator does not override the results # of descent validation if up is not Unevaluated: self.valid = bool(up) return self.valid valid = True elements, seen, queue = [], set(), collections.deque([self]) # descend breadth first, skipping any branches that return All* while queue: element = queue.popleft() if id(element) in seen: continue seen.add(id(element)) elements.append(element) validated = element._validate(state, True) if validated is Unevaluated: element.valid = validated else: element.valid = bool(validated) if valid: valid &= validated if validated is SkipAll or validated is SkipAllFalse: continue queue.extend(element.children) # back up, visiting only the elements that weren't skipped above for element in reversed(elements): validated = element._validate(state, False) # an Unevaluated ascent validator does not override the results # of descent validation if validated is Unevaluated: pass elif element.valid: element.valid = bool(validated) if valid: valid &= validated return bool(valid) def _validate(self, state, descending): """Run validation, transforming None into success. Internal.""" if descending: if self.validates_down: validators = getattr(self, self.validates_down, None) return validate_element(self, state, validators) else: if self.validates_up: validators = getattr(self, self.validates_up, None) return validate_element(self, state, validators) return Unevaluated @property def default_value(self): """A calculated "default" value. If :attr:`default_factory` is present, it will be called with the element as a single positional argument. The result of the call will be returned. Otherwise, returns :attr:`default`. When comparing an element's :attr:`value` to its default value, use this property in the comparison. """ if self.default_factory is not None: return self.default_factory(self) else: return self.default @property def x(self): """Sugar, the xml-escaped value of :attr:`.u`.""" global xml if xml is None: import xml.sax.saxutils return xml.sax.saxutils.escape(self.u) @property def xa(self): """Sugar, the xml-attribute-escaped value of :attr:`.u`.""" global xml if xml is None: import xml.sax.saxutils return xml.sax.saxutils.quoteattr(self.u)[1:-1] def __hash__(self): raise TypeError('%s object is unhashable', self.__class__.__name__)
class Base(object): properties = Properties()
class Base(object): properties = Properties({'def': 456}, abc=123)
class Base(object): __slots__ = 'properties', properties = Properties()
class Base(object): properties = Properties(abc=123)
class Override(Middle): properties = Properties({'def': 456})