Coroutine provider (#206)

* Add coroutine provider examples

* Add coroutine provier

* Update changelog

* Update static analysis travis jobs to python 3.7

* Update coroutine provider implementation for python 3.4

* Update static analysis travis jobs to python 3.6

* Make pycode style happy

* Add tests for coroutine providers

* Make coroutine tests python 2 syntax friendly

* Split tests to python2 and python3

* Refactor coroutine provider tests

* Modify pypy tests running command

* Update coroutine provider docs
This commit is contained in:
Roman Mogylatov 2018-10-18 19:39:19 +03:00 committed by GitHub
parent ac0e5eb26a
commit 9a785de4b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 9929 additions and 3143 deletions

View File

@ -7,17 +7,17 @@ language:
- python
matrix:
include:
- python: 2.7
- python: 3.6
env: TOXENV=coveralls DEPENDENCY_INJECTOR_DEBUG_MODE=1
install:
- pip install tox
- pip install cython
- make cythonize
- python: 2.7
- python: 3.6
env: TOXENV=pylint
- python: 2.7
- python: 3.6
env: TOXENV=flake8
- python: 2.7
- python: 3.6
env: TOXENV=pydocstyle
- python: 2.7
env: TOXENV=py27

View File

@ -42,10 +42,17 @@ install: uninstall clean cythonize
uninstall:
- pip uninstall -y -q dependency-injector 2> /dev/null
test: build
test-py2: build
# Unit tests with coverage report
coverage erase
coverage run --rcfile=./.coveragerc -m unittest2 discover tests/unit
coverage run --rcfile=./.coveragerc -m unittest2 discover -s tests/unit/ -p test_*_py2_py3.py
coverage report --rcfile=./.coveragerc
coverage html --rcfile=./.coveragerc
test-py3: build
# Unit tests with coverage report
coverage erase
coverage run --rcfile=./.coveragerc -m unittest2 discover -s tests/unit/ -p test_*py3.py
coverage report --rcfile=./.coveragerc
coverage html --rcfile=./.coveragerc

View File

@ -9,6 +9,10 @@ follows `Semantic versioning`_
Development version
-------------------
- Add ``Coroutine`` provider.
- Add ``DelegatedCoroutine`` provider.
- Add ``AbstractCoroutine`` provider.
- Add ``CoroutineDelegate`` provider.
- Regenerate C sources using Cython 0.28.5.
3.13.2

View File

@ -11,7 +11,7 @@ Callable providers and injections
:py:class:`Callable` provider takes a various number of positional and keyword
arguments that are used as wrapped callable injections. Every time, when
:py:class:`Callable` provider is called, positional and keyword argument
injections would be passed as an callable arguments.
injections would be passed as callable arguments.
Injections are done according to the next rules:

View File

@ -0,0 +1,73 @@
Coroutine providers
-------------------
.. currentmodule:: dependency_injector.providers
:py:class:`Coroutine` provider create wrapped coroutine on every call.
:py:class:`Coroutine` provider is designed for making better integration with
``asyncio`` coroutines. In particular, :py:class:`Coroutine` provider returns
``True`` for ``asyncio.iscoroutinefunction()`` checks.
.. note::
:py:class:`Coroutine` provider works only for Python 3.4+.
Example of usage :py:class:`Coroutine` provider with ``async / await``-based
coroutine:
.. literalinclude:: ../../examples/providers/coroutine_async_await.py
:language: python
:linenos:
Coroutine providers and injections
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:py:class:`Coroutine` provider takes a various number of positional and keyword
arguments that are used as wrapped coroutine injections. Every time, when
:py:class:`Coroutine` provider is called, positional and keyword argument
injections would be passed as coroutine arguments.
Injections are done according to the next rules:
+ All providers (instances of :py:class:`Provider`) are called every time
when injection needs to be done.
+ Providers could be injected "as is" (delegated), if it is defined obviously.
Check out :ref:`coroutine_providers_delegation`.
+ All other injectable values are provided *"as is"*.
+ Positional context arguments will be appended after :py:class:`Coroutine`
positional injections.
+ Keyword context arguments have priority on :py:class:`Coroutine` keyword
injections and will be merged over them.
.. note::
Examples of making injections could be found in API docs -
:py:class:`Coroutine`.
.. _coroutine_providers_delegation:
Coroutine providers delegation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:py:class:`Coroutine` provider could be delegated to any other provider via
any kind of injection.
Delegation of :py:class:`Coroutine` providers is the same as
:py:class:`Factory` providers delegation, please follow
:ref:`factory_providers_delegation` section for examples (with exception
of using :py:class:`DelegatedCoroutine` instead of
:py:class:`DelegatedFactory`).
Abstract coroutine providers
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:py:class:`AbstractCoroutine` provider is a :py:class:`Coroutine` provider that
must be explicitly overridden before calling.
Behaviour of :py:class:`AbstractCoroutine` providers is the same as of
:py:class:`AbstractFactory`, please follow :ref:`abstract_factory_providers`
section for examples (with exception of using :py:class:`AbstractCoroutine`
provider instead of :py:class:`AbstractFactory`).
.. disqus::

View File

@ -22,7 +22,7 @@ Factory providers and __init__ injections
:py:class:`Factory` takes a various number of positional and keyword arguments
that are used as ``__init__()`` injections. Every time, when
:py:class:`Factory` creates new one instance, positional and keyword
argument injections would be passed as an instance's arguments.
argument injections would be passed as instance arguments.
Injections are done according to the next rules:

View File

@ -20,6 +20,7 @@ Providers package API docs - :py:mod:`dependency_injector.providers`
factory
singleton
callable
coroutine
object
dependency
overriding

View File

@ -0,0 +1,26 @@
"""`Coroutine` providers example with @asyncio.coroutine decorator.
Current example works only fot Python 3.4+.
"""
import asyncio
import dependency_injector.providers as providers
@asyncio.coroutine
def coroutine_function(arg1, arg2):
"""Sample coroutine function."""
yield from asyncio.sleep(0.1)
return arg1, arg2
coroutine_provider = providers.Coroutine(coroutine_function, arg1=1, arg2=2)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
arg1, arg2 = loop.run_until_complete(coroutine_provider())
assert (arg1, arg2) == (1, 2)
assert asyncio.iscoroutinefunction(coroutine_provider)

View File

@ -0,0 +1,25 @@
"""`Coroutine` providers example with async / await syntax.
Current example works only fot Python 3.5+.
"""
import asyncio
import dependency_injector.providers as providers
async def coroutine_function(arg1, arg2):
"""Sample coroutine function."""
await asyncio.sleep(0.1)
return arg1, arg2
coroutine_provider = providers.Coroutine(coroutine_function, arg1=1, arg2=2)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
arg1, arg2 = loop.run_until_complete(coroutine_provider())
assert (arg1, arg2) == (1, 2)
assert asyncio.iscoroutinefunction(coroutine_provider)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -69,6 +69,22 @@ cdef class CallableDelegate(Delegate):
pass
# Coroutine providers
cdef class Coroutine(Callable):
pass
cdef class DelegatedCoroutine(Coroutine):
pass
cdef class AbstractCoroutine(Coroutine):
cpdef object _provide(self, tuple args, dict kwargs)
cdef class CoroutineDelegate(Delegate):
pass
# Configuration providers
cdef class Configuration(Object):
cdef str __name

View File

@ -9,6 +9,19 @@ import sys
import types
import threading
try:
import asyncio
except ImportError:
asyncio = None
_is_coroutine_marker = None
else:
if sys.version_info[0:2] == (3, 4):
_is_coroutine_marker = True
else:
import asyncio.coroutines
_is_coroutine_marker = asyncio.coroutines._is_coroutine
from .errors import (
Error,
NoSuchProviderError,
@ -848,6 +861,128 @@ cdef class CallableDelegate(Delegate):
super(Delegate, self).__init__(callable)
cdef class Coroutine(Callable):
r"""Coroutine provider creates wrapped coroutine on every call.
Coroutine supports positional and keyword argument injections:
.. code-block:: python
some_coroutine = Coroutine(some_coroutine,
'positional_arg1', 'positional_arg2',
keyword_argument1=3, keyword_argument=4)
# or
some_coroutine = Coroutine(some_coroutine) \
.add_args('positional_arg1', 'positional_arg2') \
.add_kwargs(keyword_argument1=3, keyword_argument=4)
# or
some_coroutine = Coroutine(some_coroutine)
some_coroutine.add_args('positional_arg1', 'positional_arg2')
some_coroutine.add_kwargs(keyword_argument1=3, keyword_argument=4)
"""
_is_coroutine = _is_coroutine_marker
def __init__(self, provides, *args, **kwargs):
"""Initializer.
:param provides: Wrapped callable.
:type provides: callable
:param args: Tuple of positional argument injections.
:type args: tuple[object]
:param kwargs: Dictionary of context keyword argument injections.
:type kwargs: dict[str, object]
"""
if not asyncio:
raise Error('Module asyncio is not available')
if not asyncio.iscoroutinefunction(provides):
raise Error('Provider {0} expected to get coroutine function, '
'got {1}'.format('.'.join((self.__class__.__module__,
self.__class__.__name__)),
provides))
super(Coroutine, self).__init__(provides, *args, **kwargs)
cdef class DelegatedCoroutine(Coroutine):
"""Coroutine provider that is injected "as is".
DelegatedCoroutine is a :py:class:`Coroutine`, that is injected "as is".
"""
__IS_DELEGATED__ = True
cdef class AbstractCoroutine(Coroutine):
"""Abstract coroutine provider.
:py:class:`AbstractCoroutine` is a :py:class:`Coroutine` provider that must
be explicitly overridden before calling.
Overriding of :py:class:`AbstractCoroutine` is possible only by another
:py:class:`Coroutine` provider.
"""
def __call__(self, *args, **kwargs):
"""Return provided object.
Callable interface implementation.
"""
if self.__last_overriding is None:
raise Error('{0} must be overridden before calling'.format(self))
return self.__last_overriding(*args, **kwargs)
def override(self, provider):
"""Override provider with another provider.
:param provider: Overriding provider.
:type provider: :py:class:`Provider`
:raise: :py:exc:`dependency_injector.errors.Error`
:return: Overriding context.
:rtype: :py:class:`OverridingContext`
"""
if not isinstance(provider, Coroutine):
raise Error('{0} must be overridden only by '
'{1} providers'.format(self, Coroutine))
return super(AbstractCoroutine, self).override(provider)
cpdef object _provide(self, tuple args, dict kwargs):
"""Return result of provided callable's call."""
raise NotImplementedError('Abstract provider forward providing logic '
'to overriding provider')
cdef class CoroutineDelegate(Delegate):
"""Coroutine delegate injects delegating coroutine "as is".
.. py:attribute:: provides
Value that have to be provided.
:type: object
"""
def __init__(self, coroutine):
"""Initializer.
:param coroutine: Value that have to be provided.
:type coroutine: object
"""
if isinstance(coroutine, Coroutine) is False:
raise Error('{0} can wrap only {1} providers'.format(
self.__class__, Callable))
super(CoroutineDelegate, self).__init__(coroutine)
cdef class Configuration(Object):
"""Configuration provider.

View File

@ -0,0 +1,282 @@
"""Dependency injector coroutine providers unit tests."""
import asyncio
import unittest2 as unittest
from dependency_injector import (
providers,
errors,
)
@asyncio.coroutine
def _example(arg1, arg2, arg3, arg4):
future = asyncio.Future()
future.set_result(None)
yield from future
return arg1, arg2, arg3, arg4
def _run(coro):
loop = asyncio.get_event_loop()
return loop.run_until_complete(coro)
class CoroutineTests(unittest.TestCase):
def test_init_with_coroutine(self):
self.assertTrue(providers.Coroutine(_example))
def test_init_with_not_coroutine(self):
self.assertRaises(errors.Error, providers.Coroutine, lambda: None)
def test_call_with_positional_args(self):
provider = providers.Coroutine(_example, 1, 2, 3, 4)
self.assertTupleEqual(_run(provider()), (1, 2, 3, 4))
def test_call_with_keyword_args(self):
provider = providers.Coroutine(_example,
arg1=1, arg2=2, arg3=3, arg4=4)
self.assertTupleEqual(_run(provider()), (1, 2, 3, 4))
def test_call_with_positional_and_keyword_args(self):
provider = providers.Coroutine(_example,
1, 2,
arg3=3, arg4=4)
self.assertTupleEqual(_run(provider()), (1, 2, 3, 4))
def test_call_with_context_args(self):
provider = providers.Coroutine(_example, 1, 2)
self.assertTupleEqual(_run(provider(3, 4)), (1, 2, 3, 4))
def test_call_with_context_kwargs(self):
provider = providers.Coroutine(_example, arg1=1)
self.assertTupleEqual(
_run(provider(arg2=2, arg3=3, arg4=4)),
(1, 2, 3, 4),
)
def test_call_with_context_args_and_kwargs(self):
provider = providers.Coroutine(_example, 1)
self.assertTupleEqual(
_run(provider(2, arg3=3, arg4=4)),
(1, 2, 3, 4),
)
def test_fluent_interface(self):
provider = providers.Coroutine(_example) \
.add_args(1, 2) \
.add_kwargs(arg3=3, arg4=4)
self.assertTupleEqual(_run(provider()), (1, 2, 3, 4))
def test_set_args(self):
provider = providers.Coroutine(_example) \
.add_args(1, 2) \
.set_args(3, 4)
self.assertEqual(provider.args, tuple([3, 4]))
def test_set_kwargs(self):
provider = providers.Coroutine(_example) \
.add_kwargs(init_arg3=3, init_arg4=4) \
.set_kwargs(init_arg3=4, init_arg4=5)
self.assertEqual(provider.kwargs, dict(init_arg3=4, init_arg4=5))
def test_clear_args(self):
provider = providers.Coroutine(_example) \
.add_args(1, 2) \
.clear_args()
self.assertEqual(provider.args, tuple())
def test_clear_kwargs(self):
provider = providers.Coroutine(_example) \
.add_kwargs(init_arg3=3, init_arg4=4) \
.clear_kwargs()
self.assertEqual(provider.kwargs, dict())
def test_call_overridden(self):
provider = providers.Coroutine(_example)
provider.override(providers.Object((4, 3, 2, 1)))
provider.override(providers.Object((1, 2, 3, 4)))
self.assertTupleEqual(provider(), (1, 2, 3, 4))
def test_deepcopy(self):
provider = providers.Coroutine(_example)
provider_copy = providers.deepcopy(provider)
self.assertIsNot(provider, provider_copy)
self.assertIs(provider.provides, provider_copy.provides)
self.assertIsInstance(provider, providers.Coroutine)
def test_deepcopy_from_memo(self):
provider = providers.Coroutine(_example)
provider_copy_memo = providers.Coroutine(_example)
provider_copy = providers.deepcopy(
provider, memo={id(provider): provider_copy_memo})
self.assertIs(provider_copy, provider_copy_memo)
def test_deepcopy_args(self):
provider = providers.Coroutine(_example)
dependent_provider1 = providers.Callable(list)
dependent_provider2 = providers.Callable(dict)
provider.add_args(dependent_provider1, dependent_provider2)
provider_copy = providers.deepcopy(provider)
dependent_provider_copy1 = provider_copy.args[0]
dependent_provider_copy2 = provider_copy.args[1]
self.assertNotEqual(provider.args, provider_copy.args)
self.assertIs(dependent_provider1.provides,
dependent_provider_copy1.provides)
self.assertIsNot(dependent_provider1, dependent_provider_copy1)
self.assertIs(dependent_provider2.provides,
dependent_provider_copy2.provides)
self.assertIsNot(dependent_provider2, dependent_provider_copy2)
def test_deepcopy_kwargs(self):
provider = providers.Coroutine(_example)
dependent_provider1 = providers.Callable(list)
dependent_provider2 = providers.Callable(dict)
provider.add_kwargs(a1=dependent_provider1, a2=dependent_provider2)
provider_copy = providers.deepcopy(provider)
dependent_provider_copy1 = provider_copy.kwargs['a1']
dependent_provider_copy2 = provider_copy.kwargs['a2']
self.assertNotEqual(provider.kwargs, provider_copy.kwargs)
self.assertIs(dependent_provider1.provides,
dependent_provider_copy1.provides)
self.assertIsNot(dependent_provider1, dependent_provider_copy1)
self.assertIs(dependent_provider2.provides,
dependent_provider_copy2.provides)
self.assertIsNot(dependent_provider2, dependent_provider_copy2)
def test_deepcopy_overridden(self):
provider = providers.Coroutine(_example)
object_provider = providers.Object(object())
provider.override(object_provider)
provider_copy = providers.deepcopy(provider)
object_provider_copy = provider_copy.overridden[0]
self.assertIsNot(provider, provider_copy)
self.assertIs(provider.provides, provider_copy.provides)
self.assertIsInstance(provider, providers.Callable)
self.assertIsNot(object_provider, object_provider_copy)
self.assertIsInstance(object_provider_copy, providers.Object)
def test_repr(self):
provider = providers.Coroutine(_example)
self.assertEqual(repr(provider),
'<dependency_injector.providers.'
'Coroutine({0}) at {1}>'.format(
repr(_example),
hex(id(provider))))
class DelegatedCoroutineTests(unittest.TestCase):
def test_inheritance(self):
self.assertIsInstance(providers.DelegatedCoroutine(_example),
providers.Coroutine)
def test_is_provider(self):
self.assertTrue(
providers.is_provider(providers.DelegatedCoroutine(_example)))
def test_is_delegated_provider(self):
provider = providers.DelegatedCoroutine(_example)
self.assertTrue(providers.is_delegated(provider))
def test_repr(self):
provider = providers.DelegatedCoroutine(_example)
self.assertEqual(repr(provider),
'<dependency_injector.providers.'
'DelegatedCoroutine({0}) at {1}>'.format(
repr(_example),
hex(id(provider))))
class AbstractCoroutineTests(unittest.TestCase):
def test_inheritance(self):
self.assertIsInstance(providers.AbstractCoroutine(_example),
providers.Coroutine)
def test_call_overridden_by_coroutine(self):
@asyncio.coroutine
def _abstract_example():
raise RuntimeError('Should not be raised')
provider = providers.AbstractCoroutine(_abstract_example)
provider.override(providers.Coroutine(_example))
self.assertTrue(_run(provider(1, 2, 3, 4)), (1, 2, 3, 4))
def test_call_overridden_by_delegated_coroutine(self):
@asyncio.coroutine
def _abstract_example():
raise RuntimeError('Should not be raised')
provider = providers.AbstractCoroutine(_abstract_example)
provider.override(providers.DelegatedCoroutine(_example))
self.assertTrue(_run(provider(1, 2, 3, 4)), (1, 2, 3, 4))
def test_call_not_overridden(self):
provider = providers.AbstractCoroutine(_example)
with self.assertRaises(errors.Error):
provider(1, 2, 3, 4)
def test_override_by_not_coroutine(self):
provider = providers.AbstractCoroutine(_example)
with self.assertRaises(errors.Error):
provider.override(providers.Factory(object))
def test_provide_not_implemented(self):
provider = providers.AbstractCoroutine(_example)
with self.assertRaises(NotImplementedError):
provider._provide((1, 2, 3, 4), dict())
def test_repr(self):
provider = providers.AbstractCoroutine(_example)
self.assertEqual(repr(provider),
'<dependency_injector.providers.'
'AbstractCoroutine({0}) at {1}>'.format(
repr(_example),
hex(id(provider))))
class CoroutineDelegateTests(unittest.TestCase):
def setUp(self):
self.delegated = providers.Coroutine(_example)
self.delegate = providers.CoroutineDelegate(self.delegated)
def test_is_delegate(self):
self.assertIsInstance(self.delegate, providers.Delegate)
def test_init_with_not_callable(self):
self.assertRaises(errors.Error,
providers.CoroutineDelegate,
providers.Object(object()))

18
tox.ini
View File

@ -6,11 +6,11 @@ envlist=
deps=
unittest2
commands=
unit2 discover tests/unit
unit2 discover -s tests/unit -p test_*_py3.py
[testenv:coveralls]
passenv=TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH DEPENDENCY_INJECTOR_DEBUG_MODE
basepython=python2.7
basepython=python3.6
usedevelop=True
deps=
{[testenv]deps}
@ -19,10 +19,22 @@ deps=
coveralls
commands=
coverage erase
coverage run --rcfile=./.coveragerc -m unittest2 discover tests/unit
coverage run --rcfile=./.coveragerc -m unittest2 discover -s tests/unit/ -p test_*_py3.py
coverage report --rcfile=./.coveragerc
coveralls
[testenv:py26]
commands=
unit2 discover -s tests/unit -p test_*_py2_py3.py
[testenv:py27]
commands=
unit2 discover -s tests/unit -p test_*_py2_py3.py
[testenv:pypy]
commands=
unit2 discover -s tests/unit -p test_*_py2_py3.py
[testenv:pylint]
deps=
pylint