531 Provider import from string (#555)

* Implement string imports for Factory, Callable, Singletons, and Resource

* Refactor the implementation

* Add tests

* Update tests to pass on Python 2

* Update typing and add typing tests

* Update changelog

* Update docs
This commit is contained in:
Roman Mogylatov 2022-01-30 23:16:55 -05:00 committed by GitHub
parent 38ca1cdeed
commit 86df7f91f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 10351 additions and 9084 deletions

View File

@ -21,6 +21,10 @@ Development version
``FactoryAggregate.factories`` attribute.
- Add ``.set_providers()`` method to the ``FactoryAggregate`` provider. It is an alias for
``FactoryAggregate.set_factories()`` method.
- Add string imports for ``Factory``, ``Singleton``, ``Callable``, ``Resource``, and ``Coroutine``
providers, e.g. ``Factory("module.Class")``.
See issue `#531 <https://github.com/ets-labs/python-dependency-injector/issues/531>`_.
Thanks to `@al-stefanitsky-mozdor <https://github.com/al-stefanitsky-mozdor>`_ for suggesting the feature.
- Fix ``Dependency`` provider to don't raise "Dependency is not defined" error when the ``default``
is a falsy value of proper type.
See issue `#550 <https://github.com/ets-labs/python-dependency-injector/issues/550>`_. Thanks to

View File

@ -110,6 +110,45 @@ attribute of the provider that you're going to inject.
.. note:: Any provider has a ``.provider`` attribute.
.. _factory-string-imports:
String imports
--------------
``Factory`` provider can handle string imports:
.. code-block:: python
class Container(containers.DeclarativeContainer):
service = providers.Factory("myapp.mypackage.mymodule.Service")
You can also make a relative import:
.. code-block:: python
# in myapp/container.py
class Container(containers.DeclarativeContainer):
service = providers.Factory(".mypackage.mymodule.Service")
or import a member of the current module just specifying its name:
.. code-block:: python
class Service:
...
class Container(containers.DeclarativeContainer):
service = providers.Factory("Service")
.. note::
``Singleton``, ``Callable``, ``Resource``, and ``Coroutine`` providers handle string imports
the same way as a ``Factory`` provider.
.. _factory-specialize-provided-type:
Specializing the provided type

File diff suppressed because it is too large Load Diff

View File

@ -158,10 +158,10 @@ class DependenciesContainer(Object):
class Callable(Provider[T]):
def __init__(self, provides: Optional[_Callable[..., T]] = None, *args: Injection, **kwargs: Injection) -> None: ...
def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ...
@property
def provides(self) -> Optional[_Callable[..., T]]: ...
def set_provides(self, provides: Optional[_Callable[..., T]]) -> Callable[T]: ...
def set_provides(self, provides: Optional[Union[_Callable[..., T], str]]) -> Callable[T]: ...
@property
def args(self) -> Tuple[Injection]: ...
def add_args(self, *args: Injection) -> Callable[T]: ...
@ -283,12 +283,12 @@ class Configuration(Object[Any]):
class Factory(Provider[T]):
provided_type: Optional[Type]
def __init__(self, provides: Optional[_Callable[..., T]] = None, *args: Injection, **kwargs: Injection) -> None: ...
def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ...
@property
def cls(self) -> Type[T]: ...
@property
def provides(self) -> Optional[_Callable[..., T]]: ...
def set_provides(self, provides: Optional[_Callable[..., T]]) -> Factory[T]: ...
def set_provides(self, provides: Optional[Union[_Callable[..., T], str]]) -> Factory[T]: ...
@property
def args(self) -> Tuple[Injection]: ...
def add_args(self, *args: Injection) -> Factory[T]: ...
@ -326,12 +326,12 @@ class FactoryAggregate(Aggregate[T]):
class BaseSingleton(Provider[T]):
provided_type = Optional[Type]
def __init__(self, provides: Optional[_Callable[..., T]] = None, *args: Injection, **kwargs: Injection) -> None: ...
def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ...
@property
def cls(self) -> Type[T]: ...
@property
def provides(self) -> Optional[_Callable[..., T]]: ...
def set_provides(self, provides: Optional[_Callable[..., T]]) -> BaseSingleton[T]: ...
def set_provides(self, provides: Optional[Union[_Callable[..., T], str]]) -> BaseSingleton[T]: ...
@property
def args(self) -> Tuple[Injection]: ...
def add_args(self, *args: Injection) -> BaseSingleton[T]: ...
@ -377,7 +377,7 @@ class AbstractSingleton(BaseSingleton[T]):
class SingletonDelegate(Delegate):
def __init__(self, factory: BaseSingleton): ...
def __init__(self, singleton: BaseSingleton): ...
class List(Provider[_List]):
@ -410,7 +410,7 @@ class Resource(Provider[T]):
@overload
def __init__(self, provides: Optional[_Callable[..., _Coroutine[Injection, Injection, T]]] = None, *args: Injection, **kwargs: Injection) -> None: ...
@overload
def __init__(self, provides: Optional[_Callable[..., T]] = None, *args: Injection, **kwargs: Injection) -> None: ...
def __init__(self, provides: Optional[Union[_Callable[..., T], str]] = None, *args: Injection, **kwargs: Injection) -> None: ...
@property
def provides(self) -> Optional[_Callable[..., Any]]: ...
def set_provides(self, provides: Optional[Any]) -> Resource[T]: ...

View File

@ -6,6 +6,7 @@ import copy
import errno
import functools
import inspect
import importlib
import os
import re
import sys
@ -18,6 +19,11 @@ try:
except ImportError:
contextvars = None
try:
import builtins
except ImportError:
# Python 2.7
import __builtin__ as builtins
try:
import asyncio
@ -1221,6 +1227,7 @@ cdef class Callable(Provider):
def set_provides(self, provides):
"""Set provider provides."""
provides = _resolve_provides(provides)
if provides and not callable(provides):
raise Error(
"Provider {0} expected to get callable, got {1} instead".format(
@ -1426,9 +1433,10 @@ cdef class Coroutine(Callable):
_is_coroutine = _is_coroutine_marker
def set_provides(self, provides):
"""Set provider"s provides."""
"""Set provider provides."""
if not asyncio:
raise Error("Package asyncio is not available")
provides = _resolve_provides(provides)
if provides and not asyncio.iscoroutinefunction(provides):
raise Error(f"Provider {_class_qualname(self)} expected to get coroutine function, "
f"got {provides} instead")
@ -2452,6 +2460,7 @@ cdef class Factory(Provider):
def set_provides(self, provides):
"""Set provider provides."""
provides = _resolve_provides(provides)
if (provides
and self.__class__.provided_type and
not issubclass(provides, self.__class__.provided_type)):
@ -2742,6 +2751,7 @@ cdef class BaseSingleton(Provider):
def set_provides(self, provides):
"""Set provider provides."""
provides = _resolve_provides(provides)
if (provides
and self.__class__.provided_type and
not issubclass(provides, self.__class__.provided_type)):
@ -3580,6 +3590,7 @@ cdef class Resource(Provider):
def set_provides(self, provides):
"""Set provider provides."""
provides = _resolve_provides(provides)
self.__provides = provides
return self
@ -4882,6 +4893,44 @@ def isasyncgenfunction(obj):
return False
def _resolve_provides(provides):
if provides is None:
return provides
if not isinstance(provides, str):
return provides
segments = provides.split(".")
member_name = segments[-1]
if len(segments) == 1:
if member_name in dir(builtins):
module = builtins
else:
module = _resolve_calling_module()
return getattr(module, member_name)
module_name = ".".join(segments[:-1])
package_name = _resolve_calling_package_name()
if module_name.startswith(".") and package_name is None:
raise ImportError("Attempted relative import with no known parent package")
module = importlib.import_module(module_name, package=package_name)
return getattr(module, member_name)
def _resolve_calling_module():
stack = inspect.stack()
pre_last_frame = stack[0]
return inspect.getmodule(pre_last_frame[0])
def _resolve_calling_package_name():
module = _resolve_calling_module()
return module.__package__
cpdef _copy_parent(object from_, object to, dict memo):
"""Copy and assign provider parent."""
copied_parent = (

View File

@ -66,3 +66,7 @@ assert provides10 is Cat
provider11 = providers.Callable[Animal](Cat)
provides11: Optional[Callable[..., Animal]] = provider11.provides
assert provides11 is Cat
# Test 12: to check string imports
provider12: providers.Callable[dict] = providers.Callable("builtins.dict")
provider12.set_provides("builtins.dict")

View File

@ -9,3 +9,7 @@ async def _coro() -> None:
# Test 1: to check the return type
provider1 = providers.Coroutine(_coro)
var1: Coroutine = provider1()
# Test 2: to check string imports
provider2: providers.Coroutine[None] = providers.Coroutine("_coro")
provider2.set_provides("_coro")

View File

@ -99,3 +99,7 @@ provided_cls13: Type[Animal] = provider13.cls
assert issubclass(provided_cls13, Animal)
provided_provides13: Optional[Callable[..., Animal]] = provider13.provides
assert provided_provides13 is not None and provided_provides13() == Cat()
# Test 14: to check string imports
provider14: providers.Factory[dict] = providers.Factory("builtins.dict")
provider14.set_provides("builtins.dict")

View File

@ -97,3 +97,8 @@ provider8 = providers.Resource(MyResource8)
async def _provide8() -> None:
var1: List[int] = await provider8() # type: ignore
var2: List[int] = await provider8.async_()
# Test 9: to check string imports
provider9: providers.Resource[dict] = providers.Resource("builtins.dict")
provider9.set_provides("builtins.dict")

View File

@ -89,3 +89,7 @@ provided_cls15: Type[Animal] = provider15.cls
assert issubclass(provided_cls15, Animal)
provided_provides15: Optional[Callable[..., Animal]] = provider15.provides
assert provided_provides15 is not None and provided_provides15() == Cat()
# Test 16: to check string imports
provider16: providers.Singleton[dict] = providers.Singleton("builtins.dict")
provider16.set_provides("builtins.dict")

View File

@ -1,9 +1,10 @@
"""Callable provider tests."""
import decimal
import sys
from dependency_injector import providers, errors
from pytest import raises
from pytest import raises, mark
from .common import example
@ -29,6 +30,20 @@ def test_set_provides_returns_():
assert provider.set_provides(object) is provider
@mark.parametrize(
"str_name,cls",
[
("dependency_injector.providers.Factory", providers.Factory),
("decimal.Decimal", decimal.Decimal),
("list", list),
(".common.example", example),
("test_is_provider", test_is_provider),
],
)
def test_set_provides_string_imports(str_name, cls):
assert providers.Callable(str_name).provides is cls
def test_provided_instance_provider():
provider = providers.Callable(example)
assert isinstance(provider.provided, providers.ProvidedInstance)

View File

@ -31,6 +31,17 @@ def test_set_provides_returns_self():
assert provider.set_provides(example) is provider
@mark.parametrize(
"str_name,cls",
[
(".common.example", example),
("example", example),
],
)
def test_set_provides_string_imports(str_name, cls):
assert providers.Coroutine(str_name).provides is cls
@mark.asyncio
async def test_call_with_positional_args():
provider = providers.Coroutine(example, 1, 2, 3, 4)

View File

@ -1,9 +1,10 @@
"""Factory provider tests."""
import decimal
import sys
from dependency_injector import providers, errors
from pytest import raises
from pytest import raises, mark
from .common import Example
@ -29,6 +30,20 @@ def test_set_provides_returns_():
assert provider.set_provides(object) is provider
@mark.parametrize(
"str_name,cls",
[
("dependency_injector.providers.Factory", providers.Factory),
("decimal.Decimal", decimal.Decimal),
("list", list),
(".common.Example", Example),
("test_is_provider", test_is_provider),
],
)
def test_set_provides_string_imports(str_name, cls):
assert providers.Factory(str_name).provides is cls
def test_init_with_valid_provided_type():
class ExampleProvider(providers.Factory):
provided_type = Example

View File

@ -1,10 +1,11 @@
"""Resource provider tests."""
import decimal
import sys
from typing import Any
from dependency_injector import containers, providers, resources, errors
from pytest import raises
from pytest import raises, mark
def init_fn(*args, **kwargs):
@ -27,6 +28,20 @@ def test_set_provides_returns_():
assert provider.set_provides(init_fn) is provider
@mark.parametrize(
"str_name,cls",
[
("dependency_injector.providers.Factory", providers.Factory),
("decimal.Decimal", decimal.Decimal),
("list", list),
(".test_resource_py35.test_is_provider", test_is_provider),
("test_is_provider", test_is_provider),
],
)
def test_set_provides_string_imports(str_name, cls):
assert providers.Resource(str_name).provides is cls
def test_provided_instance_provider():
provider = providers.Resource(init_fn)
assert isinstance(provider.provided, providers.ProvidedInstance)

View File

@ -1,9 +1,9 @@
"""Singleton provider tests."""
import decimal
import sys
from dependency_injector import providers, errors
from pytest import fixture, raises
from pytest import fixture, raises, mark
from .common import Example
@ -49,6 +49,20 @@ def test_set_provides_returns_self(provider):
assert provider.set_provides(object) is provider
@mark.parametrize(
"str_name,cls",
[
("dependency_injector.providers.Factory", providers.Factory),
("decimal.Decimal", decimal.Decimal),
("list", list),
(".common.Example", Example),
("test_is_provider", test_is_provider),
],
)
def test_set_provides_string_imports(str_name, cls):
assert providers.Singleton(str_name).provides is cls
def test_init_with_valid_provided_type(singleton_cls):
class ExampleProvider(singleton_cls):
provided_type = Example