Source code for jsl.roles

# coding: utf-8
import collections

from ._compat import OrderedDict, iteritems, string_types


__all__ = ['all_', 'not_', 'Var', 'Scope', 'DEFAULT_ROLE']

DEFAULT_ROLE = 'default'
"""A default role."""


[docs]def all_(role): """ A matcher that always returns ``True``. :rtype: bool """ return True
[docs]def not_(*roles): """ Returns a matcher that returns ``True`` for all roles except those are listed as arguments. :rtype: callable """ return lambda role: role not in roles
def construct_matcher(matcher): if callable(matcher): return matcher elif isinstance(matcher, string_types): return lambda r: r == matcher elif isinstance(matcher, collections.Iterable): choices = frozenset(matcher) return lambda r: r in choices else: raise ValueError( 'Unknown matcher type {} ({!r}). Only callables, ' 'strings and iterables are supported.'.format(type(matcher), matcher) ) Resolution = collections.namedtuple('Resolution', ['value', 'role']) """ A resolution result, a :class:`~collections.namedtuple`. .. attribute:: value A resolved value (the first element). .. attribute:: role A role to be used for visiting nested objects (the second element). """
[docs]class Resolvable(object): """An interface that represents an object which value varies depending on a role. """
[docs] def resolve(self, role): # pragma: no cover """ Returns a value for a given ``role``. :param str role: A role. :returns: A :class:`resolution <.Resolution>`. """ raise NotImplementedError()
[docs] def iter_possible_values(self): # pragma: no cover """Iterates over all possible values except ``None`` ones.""" raise NotImplementedError()
[docs]class Var(Resolvable): """ A :class:`.Resolvable` implementation. :param values: A dictionary or a list of key-value pairs, where keys are matchers and values are corresponding values. Matchers are callables returning boolean values. Strings and iterables are also accepted and processed as follows: * A string ``s`` will be replaced with a lambda ``lambda r: r == s``; * An iterable ``i`` will be replaced with a lambda ``lambda r: r in i``. :type values: dict or list of pairs :param default: A value to return if all matchers returned ``False``. :param propagate: A matcher that determines which roles are to be propagated down to the nested objects. Default is :data:`all_` that matches all roles. :type propagate: callable, string or iterable """ def __init__(self, values=None, default=None, propagate=all_): self._values = [] if values is not None: values = iteritems(values) if isinstance(values, dict) else values for matcher, value in values: matcher = construct_matcher(matcher) self._values.append((matcher, value)) self.default = default self._propagate = construct_matcher(propagate) @property def values(self): """A list of pairs (matcher, value).""" return self._values @property def propagate(self): """A matcher that determines which roles are to be propagated down to the nested objects. """ return self._propagate
[docs] def iter_possible_values(self): """ Implements the :class:`.Resolvable` interface. Yields non-``None`` values from :attr:`values`. """ return (v for _, v in self._values if v is not None)
[docs] def resolve(self, role): """ Implements the :class:`.Resolvable` interface. :param str role: A role. :returns: A :class:`resolution <.Resolution>`, which value is the first value which matcher returns ``True`` and the role is either a given ``role`` (if :attr:`propagate`` matcher returns ``True``) or :data:`.DEFAULT_ROLE` (otherwise). """ for matcher, matcher_value in self._values: if matcher(role): value = matcher_value break else: value = self.default new_role = role if self._propagate(role) else DEFAULT_ROLE return Resolution(value, new_role)
[docs]class Scope(object): """ A scope consists of a set of fields and a matcher. Fields can be added to a scope as attributes:: scope = Scope('response') scope.name = StringField() scope.age = IntField() A scope can then be added to a :class:`~.Document`. During a document class construction process, fields of each of its scopes are added to the resulting class as :class:`variables <.Var>` which only resolve to fields when the matcher of the scope returns ``True``. If two fields with the same name are assigned to different document scopes, the matchers of the corresponding :class:`~.Var` will be the matchers of the scopes in order they were added to the class. :class:`.Scope` can also be used as a context manager. At the moment it does not do anything and only useful as a syntactic sugar -- to introduce an extra indentation level for the fields defined within the same scope. For example:: class User(Document): with Scope('db_role') as db: db._id = StringField(required=True) db.version = StringField(required=True) with Scope('response_role') as db: db.version = IntField(required=True) Is an equivalent of:: class User(Document): db._id = Var([ ('db_role', StringField(required=True)) ]) db.version = Var([ ('db_role', StringField(required=True)) ('response_role', IntField(required=True)) ]) :param matcher: A matcher. :type matcher: callable, string or iterable .. attribute:: __field__ An ordered dictionary of :class:`fields <.BaseField>`. .. attribute:: __matcher__ A matcher. """ def __init__(self, matcher): # names are chosen to avoid clashing with user field names super(Scope, self).__setattr__('__fields__', OrderedDict()) super(Scope, self).__setattr__('__matcher__', matcher) def __getattr__(self, key): odict = super(Scope, self).__getattribute__('__fields__') if key in odict: return odict[key] return super(Scope, self).__getattribute__(key) def __setattr__(self, key, val): self.__fields__[key] = val def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): pass