Singleton reset context (#417)

* Add implementation and typing stubs

* Make some refactoring and add tests

* Pin ubuntu version to 18.04

* Add docs and example

* Add changelog

* Add container docs
This commit is contained in:
Roman Mogylatov 2021-03-03 08:28:10 -05:00 committed by GitHub
parent e0b0a1e968
commit 2bf3601695
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 13069 additions and 9176 deletions

View File

@ -6,7 +6,7 @@ jobs:
test-on-different-versions: test-on-different-versions:
name: Run tests name: Run tests
runs-on: ubuntu-latest runs-on: ubuntu-18.04
strategy: strategy:
matrix: matrix:
python-version: [2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3] python-version: [2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, pypy2, pypy3]

View File

@ -18,6 +18,14 @@ Method ``.reset_singletons()`` also resets singletons in sub-containers: ``provi
:lines: 3- :lines: 3-
:emphasize-lines: 21 :emphasize-lines: 21
You can use ``.reset_singletons()`` method with a context manager. Singletons will be reset on
both entering and exiting a context.
.. literalinclude:: ../../examples/containers/reset_singletons_with.py
:language: python
:lines: 3-
:emphasize-lines: 14-15
See also: :ref:`singleton-provider`. See also: :ref:`singleton-provider`.
.. disqus:: .. disqus::

View File

@ -7,6 +7,12 @@ that were made in every particular version.
From version 0.7.6 *Dependency Injector* framework strictly From version 0.7.6 *Dependency Injector* framework strictly
follows `Semantic versioning`_ follows `Semantic versioning`_
Development version
-------------------
- Implement context manager interface for resetting a singleton provider.
See issue: `#413 <https://github.com/ets-labs/python-dependency-injector/issues/413>`_.
Thanks to `@Arrowana <https://github.com/Arrowana>`_ for suggesting the improvement.
4.28.1 4.28.1
------ ------
- Fix async mode mode exception handling issue in ``Dependency`` provider. - Fix async mode mode exception handling issue in ``Dependency`` provider.

View File

@ -20,13 +20,12 @@ returns it on the rest of the calls.
:language: python :language: python
:lines: 3- :lines: 3-
``Singleton`` provider handles an injection of the dependencies the same way like a ``Singleton`` provider handles dependencies injection the same way like a :ref:`factory-provider`.
:ref:`factory-provider`.
.. note:: .. note::
``Singleton`` provider does dependencies injection only when creates the object. When the object ``Singleton`` provider makes dependencies injection only when creates an object. When an object
is created and memorized ``Singleton`` provider just returns it without applying the injections. is created and memorized ``Singleton`` provider just returns it without applying injections.
Specialization of the provided type and abstract singletons work the same like like for the Specialization of the provided type and abstract singletons work the same like like for the
factories: factories:
@ -56,6 +55,21 @@ provider.
Resetting of the memorized object clears the reference to it. Further object's lifecycle is Resetting of the memorized object clears the reference to it. Further object's lifecycle is
managed by the garbage collector. managed by the garbage collector.
You can use ``.reset()`` method with a context manager. Memorized instance will be reset on
both entering and exiting a context.
.. literalinclude:: ../../examples/providers/singleton_resetting_with.py
:language: python
:lines: 3-
:emphasize-lines: 18-19
Context manager ``.reset()`` returns resetting singleton provider. You can use it for aliasing.
.. code-block:: python
with container.user_service.reset() as user_service:
...
Method ``.reset()`` resets only current provider. To reset all dependent singleton providers Method ``.reset()`` resets only current provider. To reset all dependent singleton providers
call ``.full_reset()`` method. call ``.full_reset()`` method.
@ -64,6 +78,13 @@ call ``.full_reset()`` method.
:lines: 3- :lines: 3-
:emphasize-lines: 25 :emphasize-lines: 25
Method ``.full_reset()`` supports context manager interface like ``.reset()`` does.
.. code-block:: python
with container.user_service.full_reset() as user_service:
...
See also: :ref:`reset-container-singletons`. See also: :ref:`reset-container-singletons`.
Using singleton with multiple threads Using singleton with multiple threads

View File

@ -0,0 +1,23 @@
"""Container reset singletons context manager example."""
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
service = providers.Singleton(object)
if __name__ == '__main__':
container = Container()
service1 = container.service()
with container.reset_singletons():
service2 = container.service()
service3 = container.service()
assert service1 is not service2
assert service2 is not service3
assert service3 is not service1

View File

@ -0,0 +1,27 @@
"""`Singleton` provider resetting context manager example."""
from dependency_injector import containers, providers
class UserService:
...
class Container(containers.DeclarativeContainer):
user_service = providers.Singleton(UserService)
if __name__ == '__main__':
container = Container()
user_service1 = container.user_service()
with container.user_service.reset():
user_service2 = container.user_service()
user_service3 = container.user_service()
assert user_service1 is not user_service2
assert user_service2 is not user_service3
assert user_service3 is not user_service1

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
from typing import ( from typing import (
Generic,
Type, Type,
Dict, Dict,
Tuple, Tuple,
@ -20,6 +21,7 @@ from .providers import Provider, Self, ProviderParent
C_Base = TypeVar('C_Base', bound='Container') C_Base = TypeVar('C_Base', bound='Container')
C = TypeVar('C', bound='DeclarativeContainer') C = TypeVar('C', bound='DeclarativeContainer')
C_Overriding = TypeVar('C_Overriding', bound='DeclarativeContainer') C_Overriding = TypeVar('C_Overriding', bound='DeclarativeContainer')
T = TypeVar('T')
TT = TypeVar('TT') TT = TypeVar('TT')
@ -44,7 +46,7 @@ class Container:
def init_resources(self) -> Optional[Awaitable]: ... def init_resources(self) -> Optional[Awaitable]: ...
def shutdown_resources(self) -> Optional[Awaitable]: ... def shutdown_resources(self) -> Optional[Awaitable]: ...
def apply_container_providers_overridings(self) -> None: ... def apply_container_providers_overridings(self) -> None: ...
def reset_singletons(self) -> None: ... def reset_singletons(self) -> SingletonResetContext[C_Base]: ...
def check_dependencies(self) -> None: ... def check_dependencies(self) -> None: ...
@overload @overload
def resolve_provider_name(self, provider: Provider) -> str: ... def resolve_provider_name(self, provider: Provider) -> str: ...
@ -72,6 +74,11 @@ class DeclarativeContainer(Container):
def __init__(self, **overriding_providers: Union[Provider, Any]) -> None: ... def __init__(self, **overriding_providers: Union[Provider, Any]) -> None: ...
class SingletonResetContext(Generic[T]):
def __init__(self, container: T): ...
def __enter__(self) -> T: ...
def __exit__(self, *_: Any) -> None: ...
def override(container: Type[C]) -> _Callable[[Type[C_Overriding]], Type[C_Overriding]]: ... def override(container: Type[C]) -> _Callable[[Type[C_Overriding]], Type[C_Overriding]]: ...

View File

@ -304,6 +304,7 @@ class DynamicContainer(Container):
"""Reset container singletons.""" """Reset container singletons."""
for provider in self.traverse(types=[providers.BaseSingleton]): for provider in self.traverse(types=[providers.BaseSingleton]):
provider.reset() provider.reset()
return SingletonResetContext(self)
def check_dependencies(self): def check_dependencies(self):
"""Check if container dependencies are defined. """Check if container dependencies are defined.
@ -639,6 +640,18 @@ class DeclarativeContainer(Container):
provider.reset_override() provider.reset_override()
class SingletonResetContext:
def __init__(self, container):
self._container = container
def __enter__(self):
return self._container
def __exit__(self, *_):
self._container.reset_singletons()
def override(object container): def override(object container):
""":py:class:`DeclarativeContainer` overriding decorator. """:py:class:`DeclarativeContainer` overriding decorator.

File diff suppressed because it is too large Load Diff

View File

@ -54,11 +54,6 @@ cdef class DependenciesContainer(Object):
cpdef object _override_providers(self, object container) cpdef object _override_providers(self, object container)
cdef class OverridingContext(object):
cdef Provider __overridden
cdef Provider __overriding
# Callable providers # Callable providers
cdef class Callable(Provider): cdef class Callable(Provider):
cdef object __provides cdef object __provides
@ -292,6 +287,23 @@ cpdef tuple parse_named_injections(dict kwargs)
# Utils # Utils
cdef class OverridingContext(object):
cdef Provider __overridden
cdef Provider __overriding
cdef class BaseSingletonResetContext(object):
cdef object __singleton
cdef class SingletonResetContext(BaseSingletonResetContext):
pass
cdef class SingletonFullResetContext(BaseSingletonResetContext):
pass
cdef object CLASS_TYPES cdef object CLASS_TYPES

View File

@ -38,12 +38,8 @@ Injection = Any
ProviderParent = Union['Provider', Any] ProviderParent = Union['Provider', Any]
T = TypeVar('T') T = TypeVar('T')
TT = TypeVar('TT') TT = TypeVar('TT')
P = TypeVar('P', bound='Provider')
BS = TypeVar('BS', bound='BaseSingleton')
class OverridingContext:
def __init__(self, overridden: Provider, overriding: Provider): ...
def __enter__(self) -> Provider: ...
def __exit__(self, *_: Any) -> None: ...
class Provider(Generic[T]): class Provider(Generic[T]):
@ -62,7 +58,7 @@ class Provider(Generic[T]):
def overridden(self) -> Tuple[Provider]: ... def overridden(self) -> Tuple[Provider]: ...
@property @property
def last_overriding(self) -> Optional[Provider]: ... def last_overriding(self) -> Optional[Provider]: ...
def override(self, provider: Union[Provider, Any]) -> OverridingContext: ... def override(self, provider: Union[Provider, Any]) -> OverridingContext[P]: ...
def reset_last_overriding(self) -> None: ... def reset_last_overriding(self) -> None: ...
def reset_override(self) -> None: ... def reset_override(self) -> None: ...
def delegate(self) -> Provider: ... def delegate(self) -> Provider: ...
@ -109,7 +105,7 @@ class Dependency(Provider[T]):
def default(self) -> Provider[T]: ... def default(self) -> Provider[T]: ...
@property @property
def is_defined(self) -> bool: ... def is_defined(self) -> bool: ...
def provided_by(self, provider: Provider) -> OverridingContext: ... def provided_by(self, provider: Provider) -> OverridingContext[P]: ...
@property @property
def parent(self) -> Optional[ProviderParent]: ... def parent(self) -> Optional[ProviderParent]: ...
@property @property
@ -153,7 +149,7 @@ class DelegatedCallable(Callable[T]): ...
class AbstractCallable(Callable[T]): class AbstractCallable(Callable[T]):
def override(self, provider: Callable) -> OverridingContext: ... def override(self, provider: Callable) -> OverridingContext[P]: ...
class CallableDelegate(Delegate): class CallableDelegate(Delegate):
@ -167,7 +163,7 @@ class DelegatedCoroutine(Coroutine[T]): ...
class AbstractCoroutine(Coroutine[T]): class AbstractCoroutine(Coroutine[T]):
def override(self, provider: Coroutine) -> OverridingContext: ... def override(self, provider: Coroutine) -> OverridingContext[P]: ...
class CoroutineDelegate(Delegate): class CoroutineDelegate(Delegate):
@ -212,7 +208,7 @@ class Configuration(Object[Any]):
def __getitem__(self, item: Union[str, Provider]) -> ConfigurationOption: ... def __getitem__(self, item: Union[str, Provider]) -> ConfigurationOption: ...
def get_name(self) -> str: ... def get_name(self) -> str: ...
def get(self, selector: str) -> Any: ... def get(self, selector: str) -> Any: ...
def set(self, selector: str, value: Any) -> OverridingContext: ... def set(self, selector: str, value: Any) -> OverridingContext[P]: ...
def reset_cache(self) -> None: ... def reset_cache(self) -> None: ...
def update(self, value: Any) -> None: ... def update(self, value: Any) -> None: ...
def from_ini(self, filepath: Union[Path, str], required: bool = False) -> None: ... def from_ini(self, filepath: Union[Path, str], required: bool = False) -> None: ...
@ -250,7 +246,7 @@ class DelegatedFactory(Factory[T]): ...
class AbstractFactory(Factory[T]): class AbstractFactory(Factory[T]):
def override(self, provider: Factory) -> OverridingContext: ... def override(self, provider: Factory) -> OverridingContext[P]: ...
class FactoryDelegate(Delegate): class FactoryDelegate(Delegate):
@ -293,8 +289,8 @@ class BaseSingleton(Provider[T]):
def add_attributes(self, **kwargs: Injection) -> BaseSingleton[T]: ... def add_attributes(self, **kwargs: Injection) -> BaseSingleton[T]: ...
def set_attributes(self, **kwargs: Injection) -> BaseSingleton[T]: ... def set_attributes(self, **kwargs: Injection) -> BaseSingleton[T]: ...
def clear_attributes(self) -> BaseSingleton[T]: ... def clear_attributes(self) -> BaseSingleton[T]: ...
def reset(self) -> None: ... def reset(self) -> SingletonResetContext[BS]: ...
def full_reset(self) -> None: ... def full_reset(self) -> SingletonFullResetContext[BS]: ...
class Singleton(BaseSingleton[T]): ... class Singleton(BaseSingleton[T]): ...
@ -316,7 +312,7 @@ class DelegatedThreadLocalSingleton(ThreadLocalSingleton[T]): ...
class AbstractSingleton(BaseSingleton[T]): class AbstractSingleton(BaseSingleton[T]):
def override(self, provider: BaseSingleton) -> OverridingContext: ... def override(self, provider: BaseSingleton) -> OverridingContext[P]: ...
class SingletonDelegate(Delegate): class SingletonDelegate(Delegate):
@ -414,6 +410,29 @@ class MethodCaller(Provider, ProvidedInstanceFluentInterface):
def __init__(self, provider: Provider, *args: Injection, **kwargs: Injection) -> None: ... def __init__(self, provider: Provider, *args: Injection, **kwargs: Injection) -> None: ...
class OverridingContext(Generic[T]):
def __init__(self, overridden: Provider, overriding: Provider): ...
def __enter__(self) -> T: ...
def __exit__(self, *_: Any) -> None: ...
class BaseSingletonResetContext(Generic[T]):
def __init__(self, provider: T): ...
def __enter__(self) -> T: ...
def __exit__(self, *_: Any) -> None: ...
class SingletonResetContext(BaseSingletonResetContext):
...
class SingletonFullResetContext(BaseSingletonResetContext):
...
CHILD_PROVIDERS: Tuple[Provider]
def is_provider(instance: Any) -> bool: ... def is_provider(instance: Any) -> bool: ...
@ -444,6 +463,3 @@ if pydantic:
PydanticSettings = pydantic.BaseSettings PydanticSettings = pydantic.BaseSettings
else: else:
PydanticSettings = Any PydanticSettings = Any
CHILD_PROVIDERS: Tuple[Provider]

View File

@ -990,42 +990,6 @@ cdef class DependenciesContainer(Object):
provider.override(dependency_provider) provider.override(dependency_provider)
cdef class OverridingContext(object):
"""Provider overriding context.
:py:class:`OverridingContext` is used by :py:meth:`Provider.override` for
implementing ``with`` contexts. When :py:class:`OverridingContext` is
closed, overriding that was created in this context is dropped also.
.. code-block:: python
with provider.override(another_provider):
assert provider.overridden
assert not provider.overridden
"""
def __init__(self, Provider overridden, Provider overriding):
"""Initializer.
:param overridden: Overridden provider.
:type overridden: :py:class:`Provider`
:param overriding: Overriding provider.
:type overriding: :py:class:`Provider`
"""
self.__overridden = overridden
self.__overriding = overriding
super(OverridingContext, self).__init__()
def __enter__(self):
"""Do nothing."""
return self.__overriding
def __exit__(self, *_):
"""Exit overriding context."""
self.__overridden.reset_last_overriding()
cdef class Callable(Provider): cdef class Callable(Provider):
r"""Callable provider calls wrapped callable on every call. r"""Callable provider calls wrapped callable on every call.
@ -2632,11 +2596,12 @@ cdef class BaseSingleton(Provider):
def full_reset(self): def full_reset(self):
"""Reset cached instance in current and all underlying singletons, if any. """Reset cached instance in current and all underlying singletons, if any.
:rtype: None :rtype: :py:class:`SingletonFullResetContext`
""" """
self.reset() self.reset()
for provider in self.traverse(types=[BaseSingleton]): for provider in self.traverse(types=[BaseSingleton]):
provider.reset() provider.reset()
return SingletonFullResetContext(self)
@property @property
def related(self): def related(self):
@ -2707,6 +2672,7 @@ cdef class Singleton(BaseSingleton):
if __is_future_or_coroutine(self.__storage): if __is_future_or_coroutine(self.__storage):
asyncio.ensure_future(self.__storage).cancel() asyncio.ensure_future(self.__storage).cancel()
self.__storage = None self.__storage = None
return SingletonResetContext(self)
cpdef object _provide(self, tuple args, dict kwargs): cpdef object _provide(self, tuple args, dict kwargs):
"""Return single instance.""" """Return single instance."""
@ -2775,7 +2741,7 @@ cdef class ThreadSafeSingleton(BaseSingleton):
if __is_future_or_coroutine(self.__storage): if __is_future_or_coroutine(self.__storage):
asyncio.ensure_future(self.__storage).cancel() asyncio.ensure_future(self.__storage).cancel()
self.__storage = None self.__storage = None
return SingletonResetContext(self)
cpdef object _provide(self, tuple args, dict kwargs): cpdef object _provide(self, tuple args, dict kwargs):
"""Return single instance.""" """Return single instance."""
@ -2853,10 +2819,18 @@ cdef class ThreadLocalSingleton(BaseSingleton):
:rtype: None :rtype: None
""" """
if __is_future_or_coroutine(self.__storage.instance): try:
asyncio.ensure_future(self.__storage.instance).cancel() instance = self.__storage.instance
except AttributeError:
return SingletonResetContext(self)
if __is_future_or_coroutine(instance):
asyncio.ensure_future(instance).cancel()
del self.__storage.instance del self.__storage.instance
return SingletonResetContext(self)
cpdef object _provide(self, tuple args, dict kwargs): cpdef object _provide(self, tuple args, dict kwargs):
"""Return single instance.""" """Return single instance."""
cdef object instance cdef object instance
@ -4188,6 +4162,67 @@ cpdef tuple parse_named_injections(dict kwargs):
return tuple(injections) return tuple(injections)
cdef class OverridingContext(object):
"""Provider overriding context.
:py:class:`OverridingContext` is used by :py:meth:`Provider.override` for
implementing ``with`` contexts. When :py:class:`OverridingContext` is
closed, overriding that was created in this context is dropped also.
.. code-block:: python
with provider.override(another_provider):
assert provider.overridden
assert not provider.overridden
"""
def __init__(self, Provider overridden, Provider overriding):
"""Initializer.
:param overridden: Overridden provider.
:type overridden: :py:class:`Provider`
:param overriding: Overriding provider.
:type overriding: :py:class:`Provider`
"""
self.__overridden = overridden
self.__overriding = overriding
super(OverridingContext, self).__init__()
def __enter__(self):
"""Do nothing."""
return self.__overriding
def __exit__(self, *_):
"""Exit overriding context."""
self.__overridden.reset_last_overriding()
cdef class BaseSingletonResetContext(object):
def __init__(self, Provider provider):
self.__singleton = provider
super().__init__()
def __enter__(self):
return self.__singleton
def __exit__(self, *_):
raise NotImplementedError()
cdef class SingletonResetContext(BaseSingletonResetContext):
def __exit__(self, *_):
return self.__singleton.reset()
cdef class SingletonFullResetContext(BaseSingletonResetContext):
def __exit__(self, *_):
return self.__singleton.full_reset()
CHILD_PROVIDERS = (Dependency, DependenciesContainer, Container) CHILD_PROVIDERS = (Dependency, DependenciesContainer, Container)

View File

@ -336,6 +336,36 @@ class DeclarativeContainerInstanceTests(unittest.TestCase):
self.assertIs(obj32, obj42) self.assertIs(obj32, obj42)
self.assertIs(obj33, obj43) self.assertIs(obj33, obj43)
def test_reset_singletons_context_manager(self):
class Item:
def __init__(self, dependency):
self.dependency = dependency
class Container(containers.DeclarativeContainer):
dependent = providers.Singleton(object)
singleton = providers.Singleton(Item, dependency=dependent)
container = Container()
instance1 = container.singleton()
with container.reset_singletons():
instance2 = container.singleton()
instance3 = container.singleton()
self.assertEqual(len({instance1, instance2, instance3}), 3)
self.assertEqual(
len({instance1.dependency, instance2.dependency, instance3.dependency}),
3,
)
def test_reset_singletons_context_manager_as_attribute(self):
container = containers.DeclarativeContainer()
with container.reset_singletons() as alias:
pass
self.assertIs(container, alias)
def test_check_dependencies(self): def test_check_dependencies(self):
class SubContainer(containers.DeclarativeContainer): class SubContainer(containers.DeclarativeContainer):
dependency = providers.Dependency() dependency = providers.Dependency()

View File

@ -370,6 +370,23 @@ class _BaseSingletonTestCase(object):
self.assertIsNot(instance1, instance2) self.assertIsNot(instance1, instance2)
def test_reset_context_manager(self):
singleton = self.singleton_cls(object)
instance1 = singleton()
with singleton.reset():
instance2 = singleton()
instance3 = singleton()
self.assertEqual(len({instance1, instance2, instance3}), 3)
def test_reset_context_manager_as_attribute(self):
singleton = self.singleton_cls(object)
with singleton.reset() as alias:
pass
self.assertIs(singleton, alias)
def test_full_reset(self): def test_full_reset(self):
dependent_singleton = providers.Singleton(object) dependent_singleton = providers.Singleton(object)
provider = self.singleton_cls(dict, dependency=dependent_singleton) provider = self.singleton_cls(dict, dependency=dependent_singleton)
@ -386,6 +403,33 @@ class _BaseSingletonTestCase(object):
self.assertIsNot(dependent_instance1, dependent_instance2) self.assertIsNot(dependent_instance1, dependent_instance2)
self.assertIsNot(instance1, instance2) self.assertIsNot(instance1, instance2)
def test_full_reset_context_manager(self):
class Item:
def __init__(self, dependency):
self.dependency = dependency
dependent_singleton = providers.Singleton(object)
singleton = self.singleton_cls(Item, dependency=dependent_singleton)
instance1 = singleton()
with singleton.full_reset():
instance2 = singleton()
instance3 = singleton()
self.assertEqual(len({instance1, instance2, instance3}), 3)
self.assertEqual(
len({instance1.dependency, instance2.dependency, instance3.dependency}),
3,
)
def test_full_reset_context_manager_as_attribute(self):
singleton = self.singleton_cls(object)
with singleton.full_reset() as alias:
pass
self.assertIs(singleton, alias)
class SingletonTests(_BaseSingletonTestCase, unittest.TestCase): class SingletonTests(_BaseSingletonTestCase, unittest.TestCase):
@ -445,6 +489,16 @@ class ThreadLocalSingletonTests(_BaseSingletonTestCase, unittest.TestCase):
self.assertIsNot(instance1, instance2) self.assertIsNot(instance1, instance2)
def test_reset_clean(self):
provider = providers.ThreadLocalSingleton(Example)
instance1 = provider()
provider.reset()
provider.reset()
instance2 = provider()
self.assertIsNot(instance1, instance2)
class DelegatedThreadLocalSingletonTests(_BaseSingletonTestCase, class DelegatedThreadLocalSingletonTests(_BaseSingletonTestCase,
unittest.TestCase): unittest.TestCase):