Source code for django_crucrudile.entities.store

"""The entity store class provides functions to register entity
instances in an entity store.

It also allow entity classes to be registered at class-level in a
"base store". These entity classes will be instantiated when the
entity store gets instantiated, and the resulting instances will be
registered.

It also allows "register mappings" to be defined, they allow objects
passed to register functions to be transformed based on a
mapping. When looking for a mapping, register functions will compare
their argument to the mapping key (using ``issubclass``, or
``isinstance``), and use the corresponding mapping value to get the
object that will be registered.

Additionally, register mapping keys can be iterables (list or tuples),
in which case the register functions will compare their argument to
any of the elements in the mapping key, and match even if only a
single item in the mapping key matches the argument.

The base store is copied on each class definition, using a metaclass,
so that using register functions that class-level won't alter the base
store of other class definitions.

This module also contains a :func:`provides` decorator, that
decorates a entity store class, adding an object to its base store.

Doctests that use functionality in :class:`EntityStore` can be seen in
other classes (in particular
:class:`django_crucrudile.routers.Router`). They may help to get a
good idea of what the entity, entity store and entity graph concepts
mean.

"""
from collections import OrderedDict
from abc import ABCMeta

__all__ = ['provides', 'EntityStoreMetaclass', 'EntityStore']


[docs]def provides(provided, **kwargs): """Return a decorator that uses :func:`EntityStore.register_class` to register the given object in the base store. :argument provided: Class (or object) to register in the base store. This can be an object since it may be transformed by :func:`EntityStore.register_apply_map` :type provided: object """ def register_obj_in_store(router): """Register the provided class to :argument:`store` :argument route: Router to register provided object to :type router: :class:`EntityStore`""" router.register_class(provided, **kwargs) return router return register_obj_in_store
[docs]class BaseStoreMetaclassMixin(type): """Allows :class:`EntityStore` to use different :attr:`_base_register_map` and :attr:`_base_register_map` class-level mappings (list instance) for each class definitions. (``cls`` instantiation) .. inheritance-diagram:: BaseStoreMetaclassMixin >>> class Store(metaclass=BaseStoreMetaclassMixin): ... pass >>> >>> class FailStore: ... _fail_store = [] >>> >>> class NewStore(Store): ... pass >>> >>> class FailNewStore(FailStore): ... pass >>> (NewStore._base_register_map is ... Store._base_register_map) False >>> (NewStore._base_register_class_map is ... Store._base_register_class_map) False >>> (FailNewStore._fail_store is ... FailStore._fail_store) True """ _base_register_map = OrderedDict() _base_register_class_map = OrderedDict() """ :attribute _base_register_map: Base register map (see :func:`EntityStore.get_register_map` and :func:`EntityStore.register`). Use OrderedDict because mappings should be used in the order they are added (as multiple mappings may match). :type _base_store: dict :attribute _base_register_class_map: Base register class map (see :func:`EntityStore.get_register_class_map` and :func:`EntityStore.register_class`). Use OrderedDict because mappings should be used in the order they are added (as multiple mappings may match). :type _base_register_class_map: dict """ def __init__(cls, name, bases, attrs): """Replace :attr:`_base_store`, :attr:`_base_register_map` and :attr:`_base_register_class_map` by copies of themselves :argument name: New class name :type name: str :argument bases: New class bases :type bases: tuple :argument attrs: New class attributes :type attrs: dict .. seealso:: For doctests that use this member, see :class:`django_crucrudile.entities.store.BaseStoreMetaclassMixin` """ super().__init__(name, bases, attrs) cls._base_register_map = cls._base_register_map.copy() cls._base_register_class_map = cls._base_register_class_map.copy()
[docs]class EntityStoreMetaclassMixin(type): """Allows :class:`EntityStore` to use a different :attr:`_base_store` store (list instance) for each class definitions (``cls`` instantiation) .. inheritance-diagram:: EntityStoreMetaclassMixin >>> class Store(metaclass=EntityStoreMetaclassMixin): ... pass >>> >>> class FailStore: ... _fail_store = [] >>> >>> class NewStore(Store): ... pass >>> >>> class FailNewStore(FailStore): ... pass >>> (NewStore._base_store is ... Store._base_store) False >>> (FailNewStore._fail_store is ... FailStore._fail_store) True """ _base_store = [] """ :attribute _base_store: Routed entity class store, instantiated upon Router instantiation. :type _base_store: list """ def __init__(cls, name, bases, attrs): """Replace :attr:`_base_store`, :attr:`_base_register_map` and :attr:`_base_register_class_map` by copies of themselves :argument name: New class name :type name: str :argument bases: New class bases :type bases: tuple :argument attrs: New class attributes :type attrs: dict .. seealso:: For doctests that use this member, see :class:`django_crucrudile.entities.store.EntityStoreMetaclassMixin` """ super().__init__(name, bases, attrs) cls._base_store = cls._base_store.copy()
[docs]class EntityStoreMetaclass(EntityStoreMetaclassMixin, BaseStoreMetaclassMixin, ABCMeta): """Use the entity store and base store metaclass mixins, that handle creating a new instance of the stores for each class definition. See :class:`EntityStoreMetaclassMixin` and :class:`BaseStoreMetaclassMixin` for more information. .. note:: Also subclasses :class:`abc.ABCMeta` because it will be used as the metaclass for an entity, and entity are abstract classes, which needs the :class:`âbc.ABCMeta` base class. .. inheritance-diagram:: EntityStoreMetaclass """
[docs]class EntityStore(metaclass=EntityStoreMetaclass): """Provides an entity store, and a :func:`register` method that registers entities in the entity store. The subclass implementation of :func:`patterns` should iterate over the entity store. .. inheritance-diagram:: EntityStore """ def __init__(self): """Initialize router (create empty store and register base store) """ super().__init__() self._store = [] self.register_base_store() @staticmethod
[docs] def register_apply_map(entity, mapping, transform_kwargs=None, silent=True): """Apply mapping of value in ``mapping`` if ``entity`` is subclass (:func:`issubclass`) or instance (:func:`isinstance`) of key :argument entity: Object to pass to found mappings :type entity: object or class :argument mapping: Register mapping, used to get callable to pass ``entity`` to :type mapping: dict :argument transform_kwargs: Extra keyword arguments to pass to the found transform functions (mapping keys) :type transform_kwargs: dict :argument silent: If set to ``False``, will fail if not matching mapping was found. :raises LookupError: If ``silent`` is ``False``, and no matching mapping was found >>> from mock import Mock >>> >>> class Class: ... pass >>> class SubClass(Class): ... pass >>> class OtherClass: ... pass >>> >>> instance = SubClass() With instance : >>> class_mock = Mock() >>> applied = EntityStore.register_apply_map( ... instance, ... {Class: class_mock} ... ) >>> class_mock.assert_called_once_with(instance) With instance, and default mapping : >>> class_mock = Mock() >>> applied = EntityStore.register_apply_map( ... instance, ... {None: class_mock} ... ) >>> class_mock.assert_called_once_with(instance) With instance and iterable bases : >>> class_mock = Mock() >>> applied = EntityStore.register_apply_map( ... instance, ... {(OtherClass, Class): class_mock} ... ) >>> class_mock.assert_called_once_with(instance) With instance and iterable bases (no matching base) : >>> class_mock = Mock() >>> applied = EntityStore.register_apply_map( ... instance, ... {(OtherClass, ): class_mock} ... ) >>> applied is instance True >>> class_mock.called False With instance and iterable bases (no matching base, not silent) : >>> class_mock = Mock() >>> applied = EntityStore.register_apply_map( ... instance, ... {(OtherClass, ): class_mock}, ... silent=False ... ) ... # doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): ... LookupError: Could not find matching key in register mapping. Used test 'isinstance', register mapping bases are 'OtherClass', tested against 'SubClass' With subclass : >>> class_mock = Mock() >>> applied = EntityStore.register_apply_map( ... SubClass, ... {Class: class_mock} ... ) >>> class_mock.assert_called_once_with(SubClass) With subclass and iterable bases (no matching base) : >>> class_mock = Mock() >>> applied = EntityStore.register_apply_map( ... SubClass, ... {(OtherClass, ): class_mock} ... ) >>> applied is SubClass True >>> class_mock.called False With subclass and single bases (no matching base, not silent) : >>> class_mock = Mock() >>> applied = EntityStore.register_apply_map( ... SubClass, ... {OtherClass: class_mock}, ... silent=False ... ) ... # doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): ... LookupError: Could not find matching key in register mapping. Used test 'issubclass', register mapping bases are 'OtherClass', tested against 'SubClass' With subclass and no mappings (not silent) : >>> class_mock = Mock() >>> applied = EntityStore.register_apply_map( ... SubClass, ... {}, ... silent=False ... ) ... # doctest: +NORMALIZE_WHITESPACE Traceback (most recent call last): ... LookupError: Could not find matching key in register mapping. Used test 'issubclass', register mapping bases are '', tested against 'SubClass' """ if transform_kwargs is None: transform_kwargs = {} def _match_entity(base, test): """Match the current entity against a given base, with a given test """ if base is None: return True elif isinstance(base, (list, tuple)): return any( test(entity, _base) for _base in base ) else: return test(entity, base) def _make_entity(func): """Make the new entity using the given function""" return func( entity, **transform_kwargs ) def _find_entity(test): """Find an entity matching the given test in register_map keys, then, with the matching value, return :func:`_make_entity(value)`. If no key matches, return original entity or fail with a ``LookupError`` if silent is False. """ for base, func in mapping.items(): if _match_entity(base, test): # matching key, use value to make entity return _make_entity(func) else: # not returned, so no match found if silent: return entity else: def _get_base_names(): for base, key in mapping.items(): if isinstance(base, tuple): yield ', '.join(b.__name__ for b in base) else: yield base.__name__ raise LookupError( "Could not find matching key in register mapping. " "Used test '{}', register mapping bases are '{}', " "tested against '{}'".format( test.__name__, ', '.join(_get_base_names()), type(entity).__name__ if not isinstance(entity, type) else entity.__name__ ) ) if isinstance(entity, type): # entity is a class, test with issubclass return _find_entity(issubclass) else: # entity is not a class, test with isinstance return _find_entity(isinstance)
@classmethod
[docs] def get_register_class_map(self): """Mapping of type to function that will be evaluated (with entity) when calling register. See :func:`register_class` and :func:`register_apply_map`. Overriding implementations must call the base implementation (using super(), usually), so that the base mappings set by :func:`set_register_class_mapping` can be returned. The base implementation returns a copy of the stored mapping, so overriding implementations may append to the return value. .. warning:: The matching mapping will be used. This is why this method must return an :class:`collections.OrderedDict`, so that the adding order is used. .. seealso:: For doctests that use this member, see :func:`django_crucrudile.entities.store.EntityStore.register_class` """ return OrderedDict(self._base_register_class_map)
@classmethod
[docs] def get_register_class_map_kwargs(cls): """Arguments passed when applying register map, in :func:`register_class` .. seealso:: For doctests that use this member, see :func:`django_crucrudile.entities.store.EntityStore.register_class` """ return {}
[docs] def get_register_map(self): """Mapping of type to function that will be evaluated (with entity) when calling register. See :func:`register` and :func:`register_apply_map` Overriding implementations *MUST* call the base implementation (using super(), usually), so that the base mappings set by :func:`set_register_mapping` can be returned. The base implementation returns a copy of the stored mapping, so overriding implementations may append to the return value. .. warning:: The matching mapping will be used. This is why this method must return an :class:`collections.OrderedDict`, so that the adding order is used. .. seealso:: For doctests that use this member, see :func:`django_crucrudile.entities.store.EntityStore.register` """ return OrderedDict(self._base_register_map)
[docs] def get_register_map_kwargs(self): """Arguments passed when applying register map, in :func:`register` .. seealso:: For doctests that use this member, see :func:`django_crucrudile.entities.store.EntityStore.register` """ return self.get_register_class_map_kwargs()
@classmethod
[docs] def set_register_class_mapping(self, key, value): """Set a base register class mapping, that will be returned (possibly with other mappings) by :func:`get_register_class_map`. :argument key: Register class mapping bases :type key: class or tuple of classes :argument value: Register class mapping value :type value: callable >>> from mock import Mock >>> mock_mapping_func = Mock() >>> >>> class Class: ... pass >>> class Store(EntityStore): ... pass >>> >>> >>> Store.set_register_class_mapping( ... Class, mock_mapping_func ... ) >>> Store.get_register_class_map() == ( ... {Class: mock_mapping_func} ... ) True """ self._base_register_class_map[key] = value
@classmethod
[docs] def set_register_mapping(self, key, value): """Set a base register mapping, that will be returned (possibly with other mappings) by :func:`get_register_map`. :argument key: Register mapping bases :type key: class or tuple of classes :argument value: Register mapping value :type value: callable >>> from mock import Mock >>> mock_mapping_func = Mock() >>> >>> class Class: ... pass >>> >>> class Store(EntityStore): ... pass >>> >>> Store.set_register_mapping( ... Class, mock_mapping_func ... ) >>> Store().get_register_map() == ( ... {Class: mock_mapping_func} ... ) True """ self._base_register_map[key] = value
@classmethod
[docs] def register_class(cls, register_cls, map_kwargs=None): """Add a route class to :attr:`_base_store`, appling mapping from :func:`get_register_class_map` where required. This route class will be instantiated (with kwargs from :func:`get_base_store_kwargs`) when the Router is itself instiated, using :func:`register_base_store`. :argument register_cls: Object to register (usually Route or Router classes, but could be anything because of possible mapping in :func:`get_register_class_map_kwargs`) :argument map_kwargs: Argument to pass to mapping value if entity gets transformed. :type map_kwargs: dict :returns: The registered class, transformed by class register mappings if there was a matching mapping :rtype: class >>> from mock import Mock >>> mock_entity_instance = Mock() >>> mock_entity = Mock() >>> mock_entity.side_effect = [mock_entity_instance] >>> mock_mapping_func = Mock() >>> mock_mapping_func.side_effect = [mock_entity] >>> >>> class Class: ... pass >>> >>> class Store(EntityStore): ... @classmethod ... def get_register_class_map(self): ... return {Class: mock_mapping_func} >>> >>> Store.register_class(Class) is mock_entity True >>> >>> Store._base_store == [mock_entity] True >>> >>> store = Store() >>> store._store == [mock_entity_instance] True """ register_class_map = cls.get_register_class_map() if register_class_map: if map_kwargs is None: map_kwargs = {} map_kwargs = dict( cls.get_register_class_map_kwargs(), **map_kwargs ) register_cls = cls.register_apply_map( register_cls, register_class_map, map_kwargs ) cls._base_store.append(register_cls) return register_cls
[docs] def register(self, entity, map_kwargs=None): """Register routed entity, applying mapping from :func:`get_register_map` where required :argument entity: Entity to register :type entity: :class:`django_crucrudile.entities.Entity` :argument map_kwargs: Argument to pass to mapping value if entity gets transformed. :type map_kwargs: dict :returns: The registered entity, transformed by register mappings if there was a matching mapping :rtype: :class:`django_crucrudile.entities.Entity` >>> from mock import Mock >>> mock_entity_instance = Mock() >>> mock_mapping_func = Mock() >>> mock_mapping_func.side_effect = [mock_entity_instance] >>> >>> class Class: ... pass >>> instance = Class() >>> >>> class Store(EntityStore): ... @classmethod ... def get_register_map(self): ... return {Class: mock_mapping_func} >>> >>> store = Store() >>> store.register(instance) == mock_entity_instance True >>> store._store == [mock_entity_instance] True """ register_map = self.get_register_map() if register_map: if map_kwargs is None: map_kwargs = {} map_kwargs = dict( self.get_register_map_kwargs(), **map_kwargs ) entity = self.register_apply_map( entity, register_map, map_kwargs ) self._store.append(entity) return entity
[docs] def get_base_store_kwargs(self): """Arguments passed when instantiating entity classes in :attr:`_base_store` :returns: Keyword arguments :rtype: dict >>> from mock import Mock >>> mock_entity = Mock() >>> >>> class Store(EntityStore): ... def get_base_store_kwargs(self): ... return {'x': mock_entity} >>> >>> Store.register_class(lambda x: x) is not None True >>> >>> store = Store() >>> store._store == [mock_entity] True """ return {}
[docs] def register_base_store(self): """Instantiate entity classes in _base_store, using arguments from :func:`get_base_store_kwargs` >>> class Store(EntityStore): ... pass >>> >>> Store.register_class(lambda: None) is not None True >>> >>> store = Store() >>> store._store [None] """ kwargs = self.get_base_store_kwargs() for item in self._base_store: self.register( item(**kwargs), )