From 9269d4b3d0bd56031968b26b46881715fd9b478f Mon Sep 17 00:00:00 2001 From: Kiran Jonnalagadda Date: Sat, 3 Apr 2021 16:29:56 +0530 Subject: [PATCH] Registry improvements (for #295) --- CHANGES.rst | 1 + coaster/sqlalchemy/registry.py | 191 +++++++-- tests/test_sqlalchemy_registry.py | 647 ++++++++++++++++++++++++++---- 3 files changed, 727 insertions(+), 112 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 313e5464..9d0f6825 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,6 +5,7 @@ * Removed deprecated ``docflow`` module. ``StateManager`` replaces it * Removed deprecated ``make_password`` and ``check_password`` functions * Added ``compress_whitespace`` to mimic browser compression of whitespace +* Registries now support property-like access and caching 0.6.1 - 2021-01-06 ------------------ diff --git a/coaster/sqlalchemy/registry.py b/coaster/sqlalchemy/registry.py index 4ca583e3..6ffa7e6a 100644 --- a/coaster/sqlalchemy/registry.py +++ b/coaster/sqlalchemy/registry.py @@ -3,7 +3,7 @@ --------------------- Provides a :class:`Registry` type and a :class:`RegistryMixin` base class -with two registries, used by other mixin classes. +with three registries, used by other mixin classes. Helper classes such as forms and views can be registered to the model and later accessed from an instance:: @@ -32,66 +32,207 @@ class MyView(ModelView): """ from functools import partial +from threading import Lock +from typing import Optional, Set from sqlalchemy.ext.declarative import declared_attr __all__ = ['Registry', 'InstanceRegistry', 'RegistryMixin'] +_marker = object() + class Registry: - """ - Container for items registered to a model. - """ + """Container for items registered to a model.""" + + _param: Optional[str] + _name: Optional[str] + _lock: Lock + _default_property: bool + _default_cached_property: bool + _members: Set[str] + _properties: Set[str] + _cached_properties: Set[str] + + def __init__( + self, + param: Optional[str] = None, + property: bool = False, # NOQA: A002 + cached_property: bool = False, + ): + """Initialize with config.""" + if property and cached_property: + raise TypeError("Only one of property and cached_property can be True") + object.__setattr__(self, '_param', str(param) if param else None) + object.__setattr__(self, '_name', None) + object.__setattr__(self, '_lock', Lock()) + object.__setattr__(self, '_default_property', property) + object.__setattr__(self, '_default_cached_property', cached_property) + object.__setattr__(self, '_members', set()) + object.__setattr__(self, '_properties', set()) + object.__setattr__(self, '_cached_properties', set()) + + def __set_name__(self, owner, name): + """Set a name for this registry.""" + if self._name is None: + object.__setattr__(self, '_name', name) + elif name != self._name: + raise TypeError( + f"A registry cannot be used under multiple names {self._name} and" + f" {name}" + ) + + def __setattr__(self, name, value): + """Incorporate a new registry member.""" + if name.startswith('_'): + raise ValueError("Registry member names cannot be underscore-prefixed") + if hasattr(self, name): + raise ValueError("%s is already registered" % name) + if not callable(value): + raise ValueError("Registry members must be callable") + self._members.add(name) + object.__setattr__(self, name, value) + + def __call__(self, name=None, property=None, cached_property=None): # NOQA: A002 + """Return decorator to aid class or function registration.""" + use_property = self._default_property if property is None else property + use_cached_property = ( + self._default_cached_property + if cached_property is None + else cached_property + ) + if use_property and use_cached_property: + raise TypeError( + f"Only one of property and cached_property can be True." + f" Provided: property={property}, cached_property={cached_property}." + f" Registry: property={self._default_property}," + f" cached_property={self._default_cached_property}." + f" Conflicting registry settings must be explicitly set to False." + ) + + def decorator(f): + use_name = name or f.__name__ + setattr(self, use_name, f) + if use_property: + self._properties.add(use_name) + if use_cached_property: + self._cached_properties.add(use_name) + return f + + return decorator + + # def __iter__ (here or in instance?) def __get__(self, obj, cls=None): + """Access at runtime.""" if obj is None: return self - else: - return InstanceRegistry(self, obj) - def __call__(self, name=None): - """Decorator to aid class or function registration""" + cache = obj.__dict__ # This assumes a class without __slots__ + name = self._name + with self._lock: + ir = cache.get(name, _marker) + if ir is _marker: + ir = InstanceRegistry(self, obj) + cache[name] = ir - def decorator(f): - use_name = name or f.__name__ - if hasattr(self, use_name): - raise ValueError("%s is already registered" % use_name) - setattr(self, name or f.__name__, f) - return f + # Subsequent accesses will bypass this __get__ method and use the instance + # that was saved to obj.__dict__ + return ir - return decorator + def clear_cache_for(self, obj) -> bool: + """ + Clear cached instance registry from an object. + + Returns `True` if cache was cleared, `False` if it wasn't needed. + """ + with self._lock: + return bool(obj.__dict__.pop(self._name, False)) class InstanceRegistry: """ Container for accessing registered items from an instance of the model. + Used internally by :class:`Registry`. Returns a partial that will pass in an ``obj`` parameter when called. """ def __init__(self, registry, obj): + """Prepare to serve a registry member.""" + # This would previously be cause for a memory leak due to being a cyclical + # reference, and would have needed a weakref. However, this is no longer a + # concern since PEP 442 and Python 3.4. self.__registry = registry self.__obj = obj def __getattr__(self, attr): - return partial(getattr(self.__registry, attr), obj=self.__obj) + """Access a registry member.""" + registry = self.__registry + obj = self.__obj + param = registry._param + func = getattr(registry, attr) + + # If attr is a property, return the result + if attr in registry._properties: + if param is not None: + return func(**{param: obj}) + return func(obj) + + # If attr is a cached property, cache and return the result + if attr in registry._cached_properties: + if param is not None: + val = func(**{param: obj}) + else: + val = func(obj) + setattr(self, attr, val) + return val + + # Not a property or cached_property. Construct a partial, cache and return it + if param is not None: + pfunc = partial(func, **{param: obj}) + else: + pfunc = partial(func, obj) + setattr(self, attr, pfunc) + return pfunc + + def clear_cache(self): + """Clear cache from this registry.""" + with self.__registry.lock: + return bool(self.__obj.__dict__.pop(self.__registry.name, False)) class RegistryMixin: """ - Provides the :attr:`forms` and :attr:`views` registries using - :class:`Registry`. Additional registries, if needed, should be - added directly to the model class. + Adds common registries to a model. + + Included: + + * ``forms`` registry, for WTForms forms + * ``views`` registry for view classes and helper functions + * ``features`` registry for feature availability test functions. + + The forms registry passes the instance to the registered form as an ``obj`` keyword + parameter. The other registries pass it as the first positional parameter. """ @declared_attr - def forms(self): - return Registry() + def forms(cls): + """Registry for forms.""" + r = Registry('obj') + r.__set_name__(cls, 'forms') + return r @declared_attr - def views(self): - return Registry() + def views(cls): + """Registry for views.""" + r = Registry() + r.__set_name__(cls, 'views') + return r @declared_attr - def features(self): - return Registry() + def features(cls): + """Registry for feature tests.""" + r = Registry() + r.__set_name__(cls, 'features') + return r diff --git a/tests/test_sqlalchemy_registry.py b/tests/test_sqlalchemy_registry.py index f0591fbb..49a65341 100644 --- a/tests/test_sqlalchemy_registry.py +++ b/tests/test_sqlalchemy_registry.py @@ -1,91 +1,564 @@ -import unittest +"""Registry and RegistryMixin tests.""" + +from types import SimpleNamespace + +import pytest from coaster.db import db from coaster.sqlalchemy import BaseMixin +from coaster.sqlalchemy.registry import Registry + +# --- Fixtures ------------------------------------------------------------------------- + + +@pytest.fixture() +def CallableRegistry(): # NOQA: N802 + """Callable registry with a positional parameter.""" + + class CallableRegistry: + registry = Registry() + + return CallableRegistry + + +@pytest.fixture() +def PropertyRegistry(): # NOQA: N802 + """Registry with property and a positional parameter.""" + + class PropertyRegistry: + registry = Registry(property=True) + + return PropertyRegistry + + +@pytest.fixture() +def CachedPropertyRegistry(): # NOQA: N802 + """Registry with cached property and a positional parameter.""" + + class CachedPropertyRegistry: + registry = Registry(cached_property=True) + + return CachedPropertyRegistry + + +@pytest.fixture() +def CallableParamRegistry(): # NOQA: N802 + """Callable registry with a keyword parameter.""" + + class CallableParamRegistry: + registry = Registry('kwparam') + + return CallableParamRegistry + + +@pytest.fixture() +def PropertyParamRegistry(): # NOQA: N802 + """Registry with property and a keyword parameter.""" + + class PropertyParamRegistry: + registry = Registry('kwparam', property=True) + + return PropertyParamRegistry + + +@pytest.fixture() +def CachedPropertyParamRegistry(): # NOQA: N802 + """Registry with cached property and a keyword parameter.""" + + class CachedPropertyParamRegistry: + registry = Registry('kwparam', cached_property=True) + + return CachedPropertyParamRegistry + + +@pytest.fixture() +def all_registry_hosts( + CallableRegistry, # NOQA: N803 + PropertyRegistry, + CachedPropertyRegistry, + CallableParamRegistry, + PropertyParamRegistry, + CachedPropertyParamRegistry, +): + """All test registries as a list.""" + return [ + CallableRegistry, + PropertyRegistry, + CachedPropertyRegistry, + CallableParamRegistry, + PropertyParamRegistry, + CachedPropertyParamRegistry, + ] + + +@pytest.fixture(scope='module') +def registry_member(): + """Test registry member function.""" + + def member(pos=None, kwparam=None): + pass + + return member + + +@pytest.fixture(scope='session') +def registrymixin_models(): + """Fixtures for RegistryMixin tests.""" + # We have two sample models and two registered items to test that + # the registry is unique to each model and is not a global registry + # in the base RegistryMixin class. + + # Sample model 1 + class RegistryTest1(BaseMixin, db.Model): + """Registry test model 1.""" + + __tablename__ = 'registry_test1' + + # Sample model 2 + class RegistryTest2(BaseMixin, db.Model): + """Registry test model 2.""" + + __tablename__ = 'registry_test2' + + # Sample registered item (form or view) 1 + class RegisteredItem1: + """Registered item 1.""" + + def __init__(self, obj=None): + """Init class.""" + self.obj = obj + + # Sample registered item 2 + @RegistryTest2.views('test') + class RegisteredItem2: + """Registered item 2.""" + + def __init__(self, obj=None): + """Init class.""" + self.obj = obj + + # Sample registered item 3 + @RegistryTest1.features('is1') + @RegistryTest2.features() + def is1(obj): + """Assert object is instance of RegistryTest1.""" + return isinstance(obj, RegistryTest1) + + RegistryTest1.views.test = RegisteredItem1 + + return SimpleNamespace(**locals()) + + +# --- Tests ---------------------------------------------------------------------------- + +# --- Creating a registry + + +def test_registry_set_name(): + """Registry's __set_name__ gets called.""" + # Registry has no name unless added to a class + assert Registry()._name is None + + class RegistryUser: + reg1 = Registry() + reg2 = Registry() + + assert RegistryUser.reg1._name == 'reg1' + assert RegistryUser.reg2._name == 'reg2' + + +def test_registry_reuse_error(): + """Registries cannot be reused under different names.""" + # Registry raises TypeError from __set_name__, but Python recasts as RuntimeError + with pytest.raises(RuntimeError): + + class RegistryUser: + a = b = Registry() + + +def test_registry_reuse_okay(): + """Registries be reused with the same name under different hosts.""" + reusable = Registry() + + assert reusable._name is None + + class HostA: + registry = reusable + + assert HostA.registry._name == 'registry' + + class HostB: + registry = reusable + + assert HostB.registry._name == 'registry' + assert HostA.registry is HostB.registry + assert HostA.registry is reusable + + +def test_registry_param_type(): + """Registry's param must be string or None.""" + r = Registry() + assert r._param is None + r = Registry('') + assert r._param is None + r = Registry(1) + assert r._param == '1' + r = Registry('obj') + assert r._param == 'obj' + r = Registry(param='foo') + assert r._param == 'foo' + + +def test_registry_property_cached_property(): + """A registry can have property or cached_property set, but not both.""" + r = Registry() + assert r._default_property is False + assert r._default_cached_property is False + + r = Registry(property=True) + assert r._default_property is True + assert r._default_cached_property is False + + r = Registry(cached_property=True) + assert r._default_property is False + assert r._default_cached_property is True + + with pytest.raises(TypeError): + Registry(property=True, cached_property=True) + + +# --- Populating a registry + + +def test_add_to_registry( + CallableRegistry, # NOQA: N803 + PropertyRegistry, + CachedPropertyRegistry, + CallableParamRegistry, + PropertyParamRegistry, + CachedPropertyParamRegistry, +): + """A member can be added to registries and accessed as per registry settings.""" + + @CallableRegistry.registry() + @PropertyRegistry.registry() + @CachedPropertyRegistry.registry() + @CallableParamRegistry.registry() + @PropertyParamRegistry.registry() + @CachedPropertyParamRegistry.registry() + def member(pos=None, kwparam=None): + return (pos, kwparam) + + callable_host = CallableRegistry() + property_host = PropertyRegistry() + cached_property_host = CachedPropertyRegistry() + callable_param_host = CallableParamRegistry() + property_param_host = PropertyParamRegistry() + cached_property_param_host = CachedPropertyParamRegistry() + + assert callable_host.registry.member(1) == (callable_host, 1) + assert property_host.registry.member == (property_host, None) + assert cached_property_host.registry.member == (cached_property_host, None) + assert callable_param_host.registry.member(1) == (1, callable_param_host) + assert property_param_host.registry.member == (None, property_param_host) + assert cached_property_param_host.registry.member == ( + None, + cached_property_param_host, + ) + + +def test_property_cache_mismatch( + PropertyRegistry, CachedPropertyRegistry # NOQA: N803 +): + """A registry's default setting must be explicitly turned off if conflicting.""" + with pytest.raises(TypeError): + + @PropertyRegistry.registry(cached_property=True) + def member1(pos=None, kwparam=None): + return (pos, kwparam) + + with pytest.raises(TypeError): + + @CachedPropertyRegistry.registry(property=True) + def member2(pos=None, kwparam=None): + return (pos, kwparam) + + @PropertyRegistry.registry(cached_property=True, property=False) + @CachedPropertyRegistry.registry(property=True, cached_property=False) + def member(pos=None, kwparam=None): + return (pos, kwparam) + + +def test_add_to_registry_host( + CallableRegistry, # NOQA: N803 + PropertyRegistry, + CachedPropertyRegistry, + CallableParamRegistry, + PropertyParamRegistry, + CachedPropertyParamRegistry, +): + """A member can be added as a function, overriding default settings.""" + + @CallableRegistry.registry() + @PropertyRegistry.registry(property=False) + @CachedPropertyRegistry.registry(cached_property=False) + @CallableParamRegistry.registry() + @PropertyParamRegistry.registry(property=False) + @CachedPropertyParamRegistry.registry(cached_property=False) + def member(pos=None, kwparam=None): + return (pos, kwparam) + + callable_host = CallableRegistry() + property_host = PropertyRegistry() + cached_property_host = CachedPropertyRegistry() + callable_param_host = CallableParamRegistry() + property_param_host = PropertyParamRegistry() + cached_property_param_host = CachedPropertyParamRegistry() + + assert callable_host.registry.member(1) == (callable_host, 1) + assert property_host.registry.member(2) == (property_host, 2) + assert cached_property_host.registry.member(3) == (cached_property_host, 3) + assert callable_param_host.registry.member(4) == (4, callable_param_host) + assert property_param_host.registry.member(5) == (5, property_param_host) + assert cached_property_param_host.registry.member(6) == ( + 6, + cached_property_param_host, + ) + + +def test_add_to_registry_property( + CallableRegistry, # NOQA: N803 + PropertyRegistry, + CachedPropertyRegistry, + CallableParamRegistry, + PropertyParamRegistry, + CachedPropertyParamRegistry, +): + """A member can be added as a property, overriding default settings.""" + + @CallableRegistry.registry(property=True) + @PropertyRegistry.registry(property=True) + @CachedPropertyRegistry.registry(property=True, cached_property=False) + @CallableParamRegistry.registry(property=True) + @PropertyParamRegistry.registry(property=True) + @CachedPropertyParamRegistry.registry(property=True, cached_property=False) + def member(pos=None, kwparam=None): + return (pos, kwparam) + + callable_host = CallableRegistry() + property_host = PropertyRegistry() + cached_property_host = CachedPropertyRegistry() + callable_param_host = CallableParamRegistry() + property_param_host = PropertyParamRegistry() + cached_property_param_host = CachedPropertyParamRegistry() + + assert callable_host.registry.member == (callable_host, None) + assert property_host.registry.member == (property_host, None) + assert cached_property_host.registry.member == (cached_property_host, None) + assert callable_param_host.registry.member == (None, callable_param_host) + assert property_param_host.registry.member == (None, property_param_host) + assert cached_property_param_host.registry.member == ( + None, + cached_property_param_host, + ) + + +def test_add_to_registry_cached_property( + CallableRegistry, # NOQA: N803 + PropertyRegistry, + CachedPropertyRegistry, + CallableParamRegistry, + PropertyParamRegistry, + CachedPropertyParamRegistry, +): + """A member can be added as a property, overriding default settings.""" + + @CallableRegistry.registry(property=True) + @PropertyRegistry.registry(property=True) + @CachedPropertyRegistry.registry(property=True, cached_property=False) + @CallableParamRegistry.registry(property=True) + @PropertyParamRegistry.registry(property=True) + @CachedPropertyParamRegistry.registry(property=True, cached_property=False) + def member(pos=None, kwparam=None): + return (pos, kwparam) + + callable_host = CallableRegistry() + property_host = PropertyRegistry() + cached_property_host = CachedPropertyRegistry() + callable_param_host = CallableParamRegistry() + property_param_host = PropertyParamRegistry() + cached_property_param_host = CachedPropertyParamRegistry() + + assert callable_host.registry.member == (callable_host, None) + assert property_host.registry.member == (property_host, None) + assert cached_property_host.registry.member == (cached_property_host, None) + assert callable_param_host.registry.member == (None, callable_param_host) + assert property_param_host.registry.member == (None, property_param_host) + assert cached_property_param_host.registry.member == ( + None, + cached_property_param_host, + ) + + +def test_add_to_registry_custom_name(all_registry_hosts, registry_member): + """Members can be added to a registry with a custom name.""" + assert registry_member.__name__ == 'member' + for host in all_registry_hosts: + # Mock decorator call + host.registry('custom')(registry_member) + # This adds the member under the custom name + assert host.registry.custom is registry_member + # The default name of the function is not present... + with pytest.raises(AttributeError): + assert host.registry.member is registry_member + # ... but can be added + host.registry()(registry_member) + assert host.registry.member is registry_member + + +def test_add_to_registry_underscore(all_registry_hosts, registry_member): + """Registry member names cannot start with an underscore.""" + for host in all_registry_hosts: + with pytest.raises(ValueError): + host.registry('_new_member')(registry_member) + + +def test_add_to_registry_dupe(all_registry_hosts, registry_member): + """Registry member names cannot be duplicates of an existing name.""" + for host in all_registry_hosts: + host.registry()(registry_member) + with pytest.raises(ValueError): + host.registry()(registry_member) + + host.registry('custom')(registry_member) + with pytest.raises(ValueError): + host.registry('custom')(registry_member) + + +def test_cached_properties_are_cached( + PropertyRegistry, # NOQA: N803 + CachedPropertyRegistry, + PropertyParamRegistry, + CachedPropertyParamRegistry, +): + """Cached properties are truly cached.""" + # Register registry member + @PropertyRegistry.registry() + @CachedPropertyRegistry.registry() + @PropertyParamRegistry.registry() + @CachedPropertyParamRegistry.registry() + def member(pos=None, kwparam=None): + return [pos, kwparam] # Lists are different each call + + property_host = PropertyRegistry() + cached_property_host = CachedPropertyRegistry() + property_param_host = PropertyParamRegistry() + cached_property_param_host = CachedPropertyParamRegistry() + + # The properties and cached properties work + assert property_host.registry.member == [property_host, None] + assert cached_property_host.registry.member == [cached_property_host, None] + assert property_param_host.registry.member == [None, property_param_host] + assert cached_property_param_host.registry.member == [ + None, + cached_property_param_host, + ] + + # The properties and cached properties return equal values on each access + assert property_host.registry.member == property_host.registry.member + assert cached_property_host.registry.member == cached_property_host.registry.member + assert property_param_host.registry.member == property_param_host.registry.member + assert ( + cached_property_param_host.registry.member + == cached_property_param_host.registry.member + ) + + # Only the cached properties return the same value every time + assert property_host.registry.member is not property_host.registry.member + assert cached_property_host.registry.member is cached_property_host.registry.member + assert ( + property_param_host.registry.member is not property_param_host.registry.member + ) + assert ( + cached_property_param_host.registry.member + is cached_property_param_host.registry.member + ) + + +# TODO: +# test_registry_member_cannot_be_called_clear_cache +# test_multiple_positional_and_keyword_arguments +# test_registry_iter +# test_registry_members_must_be_callable +# test_add_by_directly_sticking_in +# test_instance_registry_is_cached +# test_clear_cache_for +# test_clear_cache +# test_registry_mixin_config +# test_registry_mixin_subclasses + +# --- RegistryMixin tests -------------------------------------------------------------- + + +def test_access_item_from_class(registrymixin_models): + """Registered items are available from the model class.""" + assert ( + registrymixin_models.RegistryTest1.views.test + is registrymixin_models.RegisteredItem1 + ) + assert ( + registrymixin_models.RegistryTest2.views.test + is registrymixin_models.RegisteredItem2 + ) + assert ( + registrymixin_models.RegistryTest1.views.test + is not registrymixin_models.RegisteredItem2 + ) + assert ( + registrymixin_models.RegistryTest2.views.test + is not registrymixin_models.RegisteredItem1 + ) + assert registrymixin_models.RegistryTest1.features.is1 is registrymixin_models.is1 + assert registrymixin_models.RegistryTest2.features.is1 is registrymixin_models.is1 + + +def test_access_item_class_from_instance(registrymixin_models): + """Registered items are available from the model instance.""" + r1 = registrymixin_models.RegistryTest1() + r2 = registrymixin_models.RegistryTest2() + # When accessed from the instance, we get a partial that resembles + # the wrapped item, but is not the item itself. + assert r1.views.test is not registrymixin_models.RegisteredItem1 + assert r1.views.test.func is registrymixin_models.RegisteredItem1 + assert r2.views.test is not registrymixin_models.RegisteredItem2 + assert r2.views.test.func is registrymixin_models.RegisteredItem2 + assert r1.features.is1 is not registrymixin_models.is1 + assert r1.features.is1.func is registrymixin_models.is1 + assert r2.features.is1 is not registrymixin_models.is1 + assert r2.features.is1.func is registrymixin_models.is1 + + +def test_access_item_instance_from_instance(registrymixin_models): + """Registered items can be instantiated from the model instance.""" + r1 = registrymixin_models.RegistryTest1() + r2 = registrymixin_models.RegistryTest2() + i1 = r1.views.test() + i2 = r2.views.test() + + assert isinstance(i1, registrymixin_models.RegisteredItem1) + assert isinstance(i2, registrymixin_models.RegisteredItem2) + assert not isinstance(i1, registrymixin_models.RegisteredItem2) + assert not isinstance(i2, registrymixin_models.RegisteredItem1) + assert i1.obj is r1 + assert i2.obj is r2 + assert i1.obj is not r2 + assert i2.obj is not r1 + + +def test_features(registrymixin_models): + """The features registry can be used for feature tests.""" + r1 = registrymixin_models.RegistryTest1() + r2 = registrymixin_models.RegistryTest2() -# We have two sample models and two registered items to test that -# the registry is unique to each model and is not a global registry -# in the base class. - - -# Sample model 1 -class RegistryTest1(BaseMixin, db.Model): - __tablename__ = 'registry_test1' - - -# Sample model 2 -class RegistryTest2(BaseMixin, db.Model): - __tablename__ = 'registry_test2' - - -# Sample registered item (form or view) 1 -class RegisteredItem1: - def __init__(self, obj=None): - self.obj = obj - - -# Sample registered item 2 -@RegistryTest2.views('test') -class RegisteredItem2: - def __init__(self, obj=None): - self.obj = obj - - -# Sample registered item 3 -@RegistryTest1.features('is1') -@RegistryTest2.features() -def is1(obj): - return isinstance(obj, RegistryTest1) - - -RegistryTest1.views.test = RegisteredItem1 - - -class TestRegistry(unittest.TestCase): - def test_access_item_from_class(self): - """Registered items are available from the model class""" - assert RegistryTest1.views.test is RegisteredItem1 - assert RegistryTest2.views.test is RegisteredItem2 - assert RegistryTest1.views.test is not RegisteredItem2 - assert RegistryTest2.views.test is not RegisteredItem1 - assert RegistryTest1.features.is1 is is1 - assert RegistryTest2.features.is1 is is1 - - def test_access_item_class_from_instance(self): - """Registered items are available from the model instance""" - r1 = RegistryTest1() - r2 = RegistryTest2() - # When accessed from the instance, we get a partial that resembles - # the wrapped item, but is not the item itself. - assert r1.views.test is not RegisteredItem1 - assert r1.views.test.func is RegisteredItem1 - assert r2.views.test is not RegisteredItem2 - assert r2.views.test.func is RegisteredItem2 - assert r1.features.is1 is not is1 - assert r1.features.is1.func is is1 - assert r2.features.is1 is not is1 - assert r2.features.is1.func is is1 - - def test_access_item_instance_from_instance(self): - """Registered items can be instantiated from the model instance""" - r1 = RegistryTest1() - r2 = RegistryTest2() - i1 = r1.views.test() - i2 = r2.views.test() - - assert isinstance(i1, RegisteredItem1) - assert isinstance(i2, RegisteredItem2) - assert not isinstance(i1, RegisteredItem2) - assert not isinstance(i2, RegisteredItem1) - assert i1.obj is r1 - assert i2.obj is r2 - assert i1.obj is not r2 - assert i2.obj is not r1 - - def test_features(self): - """The features registry can be used for feature tests""" - r1 = RegistryTest1() - r2 = RegistryTest2() - - assert r1.features.is1() is True - assert r2.features.is1() is False + assert r1.features.is1() is True + assert r2.features.is1() is False