Merge pull request #132 from ets-labs/130_thread_local_singleton

130 thread local singleton
This commit is contained in:
Roman 2016-08-19 01:05:53 +03:00 committed by GitHub
commit 50e4de89b4
8 changed files with 424 additions and 1 deletions

View File

@ -2,3 +2,4 @@ Dependency Injector Contributors
================================
+ Roman Mogilatov
+ Konstantin vz'One Enchant

View File

@ -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',
)

View File

@ -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

View File

@ -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
------

View File

@ -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:

View 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.

View File

@ -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."""