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
|
||||
+ Konstantin vz'One Enchant
|
||||
|
|
|
@ -17,6 +17,8 @@ from dependency_injector.providers.creational import (
|
|||
DelegatedFactory,
|
||||
Singleton,
|
||||
DelegatedSingleton,
|
||||
ThreadLocalSingleton,
|
||||
DelegatedThreadLocalSingleton
|
||||
)
|
||||
|
||||
|
||||
|
@ -34,6 +36,10 @@ __all__ = (
|
|||
|
||||
'Factory',
|
||||
'DelegatedFactory',
|
||||
|
||||
'Singleton',
|
||||
'DelegatedSingleton',
|
||||
|
||||
'ThreadLocalSingleton',
|
||||
'DelegatedThreadLocalSingleton',
|
||||
)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
"""Dependency injector creational providers."""
|
||||
|
||||
import threading
|
||||
|
||||
import six
|
||||
|
||||
from dependency_injector.providers.callable import Callable
|
||||
|
@ -247,3 +249,103 @@ class DelegatedSingleton(Singleton):
|
|||
:rtype: object
|
||||
"""
|
||||
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
|
||||
-------------------
|
||||
- No features.
|
||||
- Add ``ThreadLocalSingleton`` and ``DelegatedThreadLocalSingleton`` providers.
|
||||
- Add documentation section about singleton providers and multi-threading.
|
||||
|
||||
2.0.0
|
||||
------
|
||||
|
|
|
@ -77,3 +77,22 @@ declaring its subclasses.
|
|||
Specialization of :py:class:`Singleton` providers is the same as
|
||||
:py:class:`Factory` providers specialization, please follow
|
||||
: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)
|
||||
|
||||
|
||||
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):
|
||||
"""Factory as decorator tests."""
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user