# 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