mirror of
https://github.com/ets-labs/python-dependency-injector.git
synced 2025-02-24 15:47:29 +03:00
Merge pull request #132 from ets-labs/130_thread_local_singleton
130 thread local singleton
This commit is contained in:
commit
50e4de89b4
|
@ -2,3 +2,4 @@ Dependency Injector Contributors
|
||||||
================================
|
================================
|
||||||
|
|
||||||
+ Roman Mogilatov
|
+ Roman Mogilatov
|
||||||
|
+ Konstantin vz'One Enchant
|
||||||
|
|
|
@ -17,6 +17,8 @@ from dependency_injector.providers.creational import (
|
||||||
DelegatedFactory,
|
DelegatedFactory,
|
||||||
Singleton,
|
Singleton,
|
||||||
DelegatedSingleton,
|
DelegatedSingleton,
|
||||||
|
ThreadLocalSingleton,
|
||||||
|
DelegatedThreadLocalSingleton
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -34,6 +36,10 @@ __all__ = (
|
||||||
|
|
||||||
'Factory',
|
'Factory',
|
||||||
'DelegatedFactory',
|
'DelegatedFactory',
|
||||||
|
|
||||||
'Singleton',
|
'Singleton',
|
||||||
'DelegatedSingleton',
|
'DelegatedSingleton',
|
||||||
|
|
||||||
|
'ThreadLocalSingleton',
|
||||||
|
'DelegatedThreadLocalSingleton',
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"""Dependency injector creational providers."""
|
"""Dependency injector creational providers."""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from dependency_injector.providers.callable import Callable
|
from dependency_injector.providers.callable import Callable
|
||||||
|
@ -247,3 +249,103 @@ class DelegatedSingleton(Singleton):
|
||||||
:rtype: object
|
:rtype: object
|
||||||
"""
|
"""
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadLocalSingleton(Factory):
|
||||||
|
""":py:class:`ThreadLocalSingleton` is singleton based on thread locals.
|
||||||
|
|
||||||
|
:py:class:`ThreadLocalSingleton` provider creates instance once for each
|
||||||
|
thread and returns it on every call. :py:class:`ThreadLocalSingleton`
|
||||||
|
extends :py:class:`Factory`, so, please follow :py:class:`Factory`
|
||||||
|
documentation for getting familiar with injections syntax.
|
||||||
|
|
||||||
|
:py:class:`ThreadLocalSingleton` is thread-safe and could be used in
|
||||||
|
multithreading environment without any negative impact.
|
||||||
|
|
||||||
|
Retrieving of provided instance can be performed via calling
|
||||||
|
:py:class:`ThreadLocalSingleton` object:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
singleton = ThreadLocalSingleton(SomeClass)
|
||||||
|
some_object = singleton()
|
||||||
|
|
||||||
|
.. py:attribute:: provided_type
|
||||||
|
|
||||||
|
If provided type is defined, provider checks that providing class is
|
||||||
|
its subclass.
|
||||||
|
|
||||||
|
:type: type | None
|
||||||
|
|
||||||
|
.. py:attribute:: cls
|
||||||
|
|
||||||
|
Class that provides object.
|
||||||
|
Alias for :py:attr:`provides`.
|
||||||
|
|
||||||
|
:type: type
|
||||||
|
"""
|
||||||
|
|
||||||
|
__slots__ = ('local_storage',)
|
||||||
|
|
||||||
|
def __init__(self, provides, *args, **kwargs):
|
||||||
|
"""Initializer.
|
||||||
|
|
||||||
|
:param provides: Class or other callable that provides object
|
||||||
|
for creation.
|
||||||
|
:type provides: type | callable
|
||||||
|
"""
|
||||||
|
self.local_storage = threading.local()
|
||||||
|
super(ThreadLocalSingleton, self).__init__(provides, *args, **kwargs)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Reset cached instance, if any.
|
||||||
|
|
||||||
|
:rtype: None
|
||||||
|
"""
|
||||||
|
self.local_storage.instance = None
|
||||||
|
|
||||||
|
def _provide(self, *args, **kwargs):
|
||||||
|
"""Return provided instance.
|
||||||
|
|
||||||
|
:param args: Tuple of context positional arguments.
|
||||||
|
:type args: tuple[object]
|
||||||
|
|
||||||
|
:param kwargs: Dictionary of context keyword arguments.
|
||||||
|
:type kwargs: dict[str, object]
|
||||||
|
|
||||||
|
:rtype: object
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
instance = self.local_storage.instance
|
||||||
|
except AttributeError:
|
||||||
|
instance = super(ThreadLocalSingleton, self)._provide(*args,
|
||||||
|
**kwargs)
|
||||||
|
self.local_storage.instance = instance
|
||||||
|
finally:
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
class DelegatedThreadLocalSingleton(ThreadLocalSingleton):
|
||||||
|
""":py:class:`ThreadLocalSingleton` that is injected "as is".
|
||||||
|
|
||||||
|
.. py:attribute:: provided_type
|
||||||
|
|
||||||
|
If provided type is defined, provider checks that providing class is
|
||||||
|
its subclass.
|
||||||
|
|
||||||
|
:type: type | None
|
||||||
|
|
||||||
|
.. py:attribute:: cls
|
||||||
|
|
||||||
|
Class that provides object.
|
||||||
|
Alias for :py:attr:`provides`.
|
||||||
|
|
||||||
|
:type: type
|
||||||
|
"""
|
||||||
|
|
||||||
|
def provide_injection(self):
|
||||||
|
"""Injection strategy implementation.
|
||||||
|
|
||||||
|
:rtype: object
|
||||||
|
"""
|
||||||
|
return self
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 139 KiB |
|
@ -9,7 +9,8 @@ follows `Semantic versioning`_
|
||||||
|
|
||||||
Development version
|
Development version
|
||||||
-------------------
|
-------------------
|
||||||
- No features.
|
- Add ``ThreadLocalSingleton`` and ``DelegatedThreadLocalSingleton`` providers.
|
||||||
|
- Add documentation section about singleton providers and multi-threading.
|
||||||
|
|
||||||
2.0.0
|
2.0.0
|
||||||
------
|
------
|
||||||
|
|
|
@ -77,3 +77,22 @@ declaring its subclasses.
|
||||||
Specialization of :py:class:`Singleton` providers is the same as
|
Specialization of :py:class:`Singleton` providers is the same as
|
||||||
:py:class:`Factory` providers specialization, please follow
|
:py:class:`Factory` providers specialization, please follow
|
||||||
:ref:`factory_providers_specialization` section for examples.
|
:ref:`factory_providers_specialization` section for examples.
|
||||||
|
|
||||||
|
Singleton providers and multi-threading
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
:py:class:`Singleton` provider is thread-safe and could be used in
|
||||||
|
multi-threading applications without any negative impact. Race condition on
|
||||||
|
singleton's initialization is escaped by using a global reentrant mutex -
|
||||||
|
:py:obj:`dependency_injector.utils.GLOBAL_LOCK`.
|
||||||
|
|
||||||
|
Also there could be a need to use thread-scoped singletons and there is a
|
||||||
|
special provider for such case - :py:class:`ThreadLocalSingleton`.
|
||||||
|
:py:class:`ThreadLocalSingleton` provider creates instance once for each
|
||||||
|
thread and returns it on every call.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. literalinclude:: ../../examples/providers/singleton_thread_locals.py
|
||||||
|
:language: python
|
||||||
|
:linenos:
|
||||||
|
|
50
examples/providers/singleton_thread_locals.py
Normal file
50
examples/providers/singleton_thread_locals.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
"""`ThreadLocalSingleton` providers example."""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import Queue
|
||||||
|
|
||||||
|
import dependency_injector.providers as providers
|
||||||
|
|
||||||
|
|
||||||
|
def example(example_object, queue):
|
||||||
|
"""Example function that puts provided object in the provided queue."""
|
||||||
|
queue.put(example_object)
|
||||||
|
|
||||||
|
# Create thread-local singleton provider for some object (main thread):
|
||||||
|
thread_local_object = providers.ThreadLocalSingleton(object)
|
||||||
|
|
||||||
|
# Create singleton provider for thread-safe queue:
|
||||||
|
queue = providers.Singleton(Queue.Queue)
|
||||||
|
|
||||||
|
# Create callable provider for example(), inject dependencies:
|
||||||
|
example = providers.DelegatedCallable(example,
|
||||||
|
example_object=thread_local_object,
|
||||||
|
queue=queue)
|
||||||
|
|
||||||
|
# Create factory provider for threads that are targeted to execute example():
|
||||||
|
thread_factory = providers.Factory(threading.Thread,
|
||||||
|
target=example)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Create 10 threads for concurrent execution of example():
|
||||||
|
threads = []
|
||||||
|
for thread_number in xrange(10):
|
||||||
|
threads.append(thread_factory(name='Thread{0}'.format(thread_number)))
|
||||||
|
|
||||||
|
# Start execution of all created threads:
|
||||||
|
for thread in threads:
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
# Wait while threads would complete their work:
|
||||||
|
for thread in threads:
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
# Making some asserts (main thread):
|
||||||
|
all_objects = set()
|
||||||
|
|
||||||
|
while not queue().empty():
|
||||||
|
all_objects.add(queue().get())
|
||||||
|
|
||||||
|
assert len(all_objects) == len(threads)
|
||||||
|
# Queue contains same number of objects as number of threads where
|
||||||
|
# thread-local singleton provider was used.
|
|
@ -484,6 +484,250 @@ class DelegatedSingletonTests(unittest.TestCase):
|
||||||
self.assertIs(provider.provide_injection(), provider)
|
self.assertIs(provider.provide_injection(), provider)
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadLocalSingletonTests(unittest.TestCase):
|
||||||
|
"""ThreadLocalSingleton test cases."""
|
||||||
|
|
||||||
|
def test_is_provider(self):
|
||||||
|
"""Test `is_provider` check."""
|
||||||
|
self.assertTrue(
|
||||||
|
utils.is_provider(providers.ThreadLocalSingleton(Example)))
|
||||||
|
|
||||||
|
def test_init_with_callable(self):
|
||||||
|
"""Test creation of provider with a callable."""
|
||||||
|
self.assertTrue(providers.ThreadLocalSingleton(credits))
|
||||||
|
|
||||||
|
def test_init_with_not_callable(self):
|
||||||
|
"""Test creation of provider with not a callable."""
|
||||||
|
self.assertRaises(errors.Error, providers.ThreadLocalSingleton, 123)
|
||||||
|
|
||||||
|
def test_init_with_valid_provided_type(self):
|
||||||
|
"""Test creation with not valid provided type."""
|
||||||
|
class ExampleProvider(providers.ThreadLocalSingleton):
|
||||||
|
"""Example provider."""
|
||||||
|
|
||||||
|
provided_type = Example
|
||||||
|
|
||||||
|
example_provider = ExampleProvider(Example, 1, 2)
|
||||||
|
|
||||||
|
self.assertIsInstance(example_provider(), Example)
|
||||||
|
|
||||||
|
def test_init_with_valid_provided_subtype(self):
|
||||||
|
"""Test creation with not valid provided type."""
|
||||||
|
class ExampleProvider(providers.ThreadLocalSingleton):
|
||||||
|
"""Example provider."""
|
||||||
|
|
||||||
|
provided_type = Example
|
||||||
|
|
||||||
|
class NewExampe(Example):
|
||||||
|
"""Example class subclass."""
|
||||||
|
|
||||||
|
example_provider = ExampleProvider(NewExampe, 1, 2)
|
||||||
|
|
||||||
|
self.assertIsInstance(example_provider(), NewExampe)
|
||||||
|
|
||||||
|
def test_init_with_invalid_provided_type(self):
|
||||||
|
"""Test creation with not valid provided type."""
|
||||||
|
class ExampleProvider(providers.ThreadLocalSingleton):
|
||||||
|
"""Example provider."""
|
||||||
|
|
||||||
|
provided_type = Example
|
||||||
|
|
||||||
|
with self.assertRaises(errors.Error):
|
||||||
|
ExampleProvider(list)
|
||||||
|
|
||||||
|
def test_call(self):
|
||||||
|
"""Test getting of instances."""
|
||||||
|
provider = providers.ThreadLocalSingleton(Example)
|
||||||
|
|
||||||
|
instance1 = provider()
|
||||||
|
instance2 = provider()
|
||||||
|
|
||||||
|
self.assertIs(instance1, instance2)
|
||||||
|
self.assertIsInstance(instance1, Example)
|
||||||
|
self.assertIsInstance(instance2, Example)
|
||||||
|
|
||||||
|
def test_call_with_init_positional_args(self):
|
||||||
|
"""Test getting of instances with init positional args."""
|
||||||
|
provider = providers.ThreadLocalSingleton(Example, 'i1', 'i2')
|
||||||
|
|
||||||
|
instance1 = provider()
|
||||||
|
instance2 = provider()
|
||||||
|
|
||||||
|
self.assertEqual(instance1.init_arg1, 'i1')
|
||||||
|
self.assertEqual(instance1.init_arg2, 'i2')
|
||||||
|
|
||||||
|
self.assertEqual(instance2.init_arg1, 'i1')
|
||||||
|
self.assertEqual(instance2.init_arg2, 'i2')
|
||||||
|
|
||||||
|
self.assertIs(instance1, instance2)
|
||||||
|
self.assertIsInstance(instance1, Example)
|
||||||
|
self.assertIsInstance(instance2, Example)
|
||||||
|
|
||||||
|
def test_call_with_init_keyword_args(self):
|
||||||
|
"""Test getting of instances with init keyword args."""
|
||||||
|
provider = providers.ThreadLocalSingleton(Example,
|
||||||
|
init_arg1='i1',
|
||||||
|
init_arg2='i2')
|
||||||
|
|
||||||
|
instance1 = provider()
|
||||||
|
instance2 = provider()
|
||||||
|
|
||||||
|
self.assertEqual(instance1.init_arg1, 'i1')
|
||||||
|
self.assertEqual(instance1.init_arg2, 'i2')
|
||||||
|
|
||||||
|
self.assertEqual(instance2.init_arg1, 'i1')
|
||||||
|
self.assertEqual(instance2.init_arg2, 'i2')
|
||||||
|
|
||||||
|
self.assertIs(instance1, instance2)
|
||||||
|
self.assertIsInstance(instance1, Example)
|
||||||
|
self.assertIsInstance(instance2, Example)
|
||||||
|
|
||||||
|
def test_call_with_init_positional_and_keyword_args(self):
|
||||||
|
"""Test getting of instances with init positional and keyword args."""
|
||||||
|
provider = providers.ThreadLocalSingleton(Example,
|
||||||
|
'i1',
|
||||||
|
init_arg2='i2')
|
||||||
|
|
||||||
|
instance1 = provider()
|
||||||
|
instance2 = provider()
|
||||||
|
|
||||||
|
self.assertEqual(instance1.init_arg1, 'i1')
|
||||||
|
self.assertEqual(instance1.init_arg2, 'i2')
|
||||||
|
|
||||||
|
self.assertEqual(instance2.init_arg1, 'i1')
|
||||||
|
self.assertEqual(instance2.init_arg2, 'i2')
|
||||||
|
|
||||||
|
self.assertIs(instance1, instance2)
|
||||||
|
self.assertIsInstance(instance1, Example)
|
||||||
|
self.assertIsInstance(instance2, Example)
|
||||||
|
|
||||||
|
def test_call_with_attributes(self):
|
||||||
|
"""Test getting of instances with attribute injections."""
|
||||||
|
provider = providers.ThreadLocalSingleton(Example)
|
||||||
|
provider.add_attributes(attribute1='a1', attribute2='a2')
|
||||||
|
|
||||||
|
instance1 = provider()
|
||||||
|
instance2 = provider()
|
||||||
|
|
||||||
|
self.assertEqual(instance1.attribute1, 'a1')
|
||||||
|
self.assertEqual(instance1.attribute2, 'a2')
|
||||||
|
|
||||||
|
self.assertEqual(instance2.attribute1, 'a1')
|
||||||
|
self.assertEqual(instance2.attribute2, 'a2')
|
||||||
|
|
||||||
|
self.assertIs(instance1, instance2)
|
||||||
|
self.assertIsInstance(instance1, Example)
|
||||||
|
self.assertIsInstance(instance2, Example)
|
||||||
|
|
||||||
|
def test_call_with_context_args(self):
|
||||||
|
"""Test getting of instances with context args."""
|
||||||
|
provider = providers.ThreadLocalSingleton(Example)
|
||||||
|
|
||||||
|
instance = provider(11, 22)
|
||||||
|
|
||||||
|
self.assertEqual(instance.init_arg1, 11)
|
||||||
|
self.assertEqual(instance.init_arg2, 22)
|
||||||
|
|
||||||
|
def test_call_with_context_kwargs(self):
|
||||||
|
"""Test getting of instances with context kwargs."""
|
||||||
|
provider = providers.ThreadLocalSingleton(Example, init_arg1=1)
|
||||||
|
|
||||||
|
instance1 = provider(init_arg2=22)
|
||||||
|
self.assertEqual(instance1.init_arg1, 1)
|
||||||
|
self.assertEqual(instance1.init_arg2, 22)
|
||||||
|
|
||||||
|
# Instance is created earlier
|
||||||
|
instance1 = provider(init_arg1=11, init_arg2=22)
|
||||||
|
self.assertEqual(instance1.init_arg1, 1)
|
||||||
|
self.assertEqual(instance1.init_arg2, 22)
|
||||||
|
|
||||||
|
def test_call_with_context_args_and_kwargs(self):
|
||||||
|
"""Test getting of instances with context args and kwargs."""
|
||||||
|
provider = providers.ThreadLocalSingleton(Example, 11)
|
||||||
|
|
||||||
|
instance = provider(22, init_arg3=33, init_arg4=44)
|
||||||
|
|
||||||
|
self.assertEqual(instance.init_arg1, 11)
|
||||||
|
self.assertEqual(instance.init_arg2, 22)
|
||||||
|
self.assertEqual(instance.init_arg3, 33)
|
||||||
|
self.assertEqual(instance.init_arg4, 44)
|
||||||
|
|
||||||
|
def test_fluent_interface(self):
|
||||||
|
"""Test injections definition with fluent interface."""
|
||||||
|
provider = providers.ThreadLocalSingleton(Example) \
|
||||||
|
.add_args(1, 2) \
|
||||||
|
.add_kwargs(init_arg3=3, init_arg4=4) \
|
||||||
|
.add_attributes(attribute1=5, attribute2=6)
|
||||||
|
|
||||||
|
instance = provider()
|
||||||
|
|
||||||
|
self.assertEqual(instance.init_arg1, 1)
|
||||||
|
self.assertEqual(instance.init_arg2, 2)
|
||||||
|
self.assertEqual(instance.init_arg3, 3)
|
||||||
|
self.assertEqual(instance.init_arg4, 4)
|
||||||
|
self.assertEqual(instance.attribute1, 5)
|
||||||
|
self.assertEqual(instance.attribute2, 6)
|
||||||
|
|
||||||
|
def test_call_overridden(self):
|
||||||
|
"""Test getting of instances on overridden provider."""
|
||||||
|
provider = providers.ThreadLocalSingleton(Example)
|
||||||
|
overriding_provider1 = providers.ThreadLocalSingleton(dict)
|
||||||
|
overriding_provider2 = providers.ThreadLocalSingleton(object)
|
||||||
|
|
||||||
|
provider.override(overriding_provider1)
|
||||||
|
provider.override(overriding_provider2)
|
||||||
|
|
||||||
|
instance1 = provider()
|
||||||
|
instance2 = provider()
|
||||||
|
|
||||||
|
self.assertIs(instance1, instance2)
|
||||||
|
self.assertIsInstance(instance1, object)
|
||||||
|
self.assertIsInstance(instance2, object)
|
||||||
|
|
||||||
|
def test_reset(self):
|
||||||
|
"""Test creation and reset of single object."""
|
||||||
|
provider = providers.ThreadLocalSingleton(object)
|
||||||
|
|
||||||
|
instance1 = provider()
|
||||||
|
self.assertIsInstance(instance1, object)
|
||||||
|
|
||||||
|
provider.reset()
|
||||||
|
|
||||||
|
instance2 = provider()
|
||||||
|
self.assertIsInstance(instance1, object)
|
||||||
|
|
||||||
|
self.assertIsNot(instance1, instance2)
|
||||||
|
|
||||||
|
def test_repr(self):
|
||||||
|
"""Test representation of provider."""
|
||||||
|
provider = providers.ThreadLocalSingleton(Example)
|
||||||
|
|
||||||
|
self.assertEqual(repr(provider),
|
||||||
|
'<dependency_injector.providers.creational.'
|
||||||
|
'ThreadLocalSingleton({0}) at {1}>'.format(
|
||||||
|
repr(Example),
|
||||||
|
hex(id(provider))))
|
||||||
|
|
||||||
|
|
||||||
|
class DelegatedThreadLocalSingletonTests(unittest.TestCase):
|
||||||
|
"""DelegatedThreadLocalSingleton test cases."""
|
||||||
|
|
||||||
|
def test_inheritance(self):
|
||||||
|
"""Test inheritance."""
|
||||||
|
self.assertIsInstance(providers.DelegatedThreadLocalSingleton(object),
|
||||||
|
providers.ThreadLocalSingleton)
|
||||||
|
|
||||||
|
def test_is_provider(self):
|
||||||
|
"""Test is_provider."""
|
||||||
|
self.assertTrue(utils.is_provider(
|
||||||
|
providers.DelegatedThreadLocalSingleton(object)))
|
||||||
|
|
||||||
|
def test_is_delegated_provider(self):
|
||||||
|
"""Test is_delegated_provider."""
|
||||||
|
provider = providers.DelegatedThreadLocalSingleton(object)
|
||||||
|
self.assertIs(provider.provide_injection(), provider)
|
||||||
|
|
||||||
|
|
||||||
class FactoryAsDecoratorTests(unittest.TestCase):
|
class FactoryAsDecoratorTests(unittest.TestCase):
|
||||||
"""Factory as decorator tests."""
|
"""Factory as decorator tests."""
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user