From ec3cda5913fea5b0d38dc46bf3a7ee0b1a03a914 Mon Sep 17 00:00:00 2001 From: IvanKirpichnikov Date: Fri, 18 Oct 2024 17:11:23 +0300 Subject: [PATCH 1/4] add marker WithProtocols --- src/dishka/__init__.py | 2 + src/dishka/dependency_source/make_factory.py | 6 +-- src/dishka/entities/with_parents.py | 5 ++- src/dishka/entities/with_protocols.py | 42 ++++++++++++++++++++ src/dishka/text_rendering/name.py | 15 +++++-- tests/unit/container/test_with_protocols.py | 38 ++++++++++++++++++ 6 files changed, 101 insertions(+), 7 deletions(-) create mode 100644 src/dishka/entities/with_protocols.py create mode 100644 tests/unit/container/test_with_protocols.py diff --git a/src/dishka/__init__.py b/src/dishka/__init__.py index 31bef40b9..38fdb7e49 100644 --- a/src/dishka/__init__.py +++ b/src/dishka/__init__.py @@ -19,6 +19,7 @@ "provide", "provide_all", "new_scope", + "WithProtocols", ] from .async_container import AsyncContainer, make_async_container @@ -36,4 +37,5 @@ from .entities.provides_marker import AnyOf from .entities.scope import BaseScope, Scope, new_scope from .entities.with_parents import WithParents +from .entities.with_protocols import WithProtocols from .provider import Provider diff --git a/src/dishka/dependency_source/make_factory.py b/src/dishka/dependency_source/make_factory.py index 7b83b5956..a24970f94 100644 --- a/src/dishka/dependency_source/make_factory.py +++ b/src/dishka/dependency_source/make_factory.py @@ -220,7 +220,7 @@ def _make_factory_by_class( hints = dict(res.get_resolved_members(source).members) except NameError as e: name = get_name(source, include_module=True) + ".__init__" - raise NameError( + raise NameError( # type: ignore[call-arg] f"Failed to analyze `{name}`. \n" f"Type '{e.name}' is not defined\n\n" f"If your are using `if TYPE_CHECKING` to import '{e.name}' " @@ -276,7 +276,7 @@ def _make_factory_by_function( hints = get_type_hints(source, include_extras=True) except NameError as e: name = get_name(source, include_module=True) - raise NameError( + raise NameError( # type: ignore[call-arg] f"Failed to analyze `{name}`. \n" f"Type '{e.name}' is not defined. \n\n" f"If your are using `if TYPE_CHECKING` to import '{e.name}' " @@ -340,7 +340,7 @@ def _make_factory_by_static_method( hints = get_type_hints(source, include_extras=True) except NameError as e: name = get_name(source, include_module=True) - raise NameError( + raise NameError( # type: ignore[call-arg] f"Failed to analyze `{name}`. \n" f"Type '{e.name}' is not defined. \n\n" f"If your are using `if TYPE_CHECKING` to import '{e.name}' " diff --git a/src/dishka/entities/with_parents.py b/src/dishka/entities/with_parents.py index 395ac89bd..828e509bf 100644 --- a/src/dishka/entities/with_parents.py +++ b/src/dishka/entities/with_parents.py @@ -21,6 +21,8 @@ __all__ = ["WithParents", "ParentsResolver"] +from dishka.text_rendering import get_name + IGNORE_TYPES: Final = ( type, object, @@ -84,8 +86,9 @@ def create_type_vars_map(obj: TypeHint) -> dict[TypeHint, TypeHint]: class ParentsResolver: def get_parents(self, child_type: TypeHint) -> list[TypeHint]: if is_ignored_type(strip_alias(child_type)): + name = get_name(child_type, include_module=False) raise ValueError( - f"The starting class {child_type!r} is in ignored types", + f"The starting class {name} is in ignored types", ) if is_parametrized(child_type) or has_orig_bases(child_type): return self._get_parents_for_generic(child_type) diff --git a/src/dishka/entities/with_protocols.py b/src/dishka/entities/with_protocols.py new file mode 100644 index 000000000..a93b945ca --- /dev/null +++ b/src/dishka/entities/with_protocols.py @@ -0,0 +1,42 @@ +__all__ = ["WithProtocols"] + +from typing import TYPE_CHECKING, TypeVar + +from dishka._adaptix.common import TypeHint +from dishka._adaptix.type_tools import is_protocol, strip_alias +from dishka.entities.provides_marker import ProvideMultiple +from dishka.entities.with_parents import ParentsResolver +from dishka.text_rendering import get_name + + +def get_parents_protocols(type_hint: TypeHint) -> list[TypeHint]: + parents = ParentsResolver().get_parents(type_hint) + new_parents = [ + parent for parent in parents + if is_protocol(strip_alias(parent)) + ] + if new_parents: + return new_parents + + name = get_name(type_hint, include_module=False) + error_msg = ( + f"Not a single parent of the protocol was found in {name}.\n" + "Hint:\n" + f" * Maybe you meant just {name}, not WithProtocols[{name}]\n" + ) + if len(parents) > 1: + error_msg += f" * Perhaps you meant WithParents[{name}]?" + raise ValueError(error_msg) + + +T = TypeVar("T") +if TYPE_CHECKING: + from typing import Union + WithProtocols = Union[T, T] # noqa: UP007,PYI016 +else: + class WithProtocols: + def __class_getitem__(cls, item: TypeHint) -> TypeHint: + parents = get_parents_protocols(item) + if len(parents) > 1: + return ProvideMultiple(parents) + return parents[0] diff --git a/src/dishka/text_rendering/name.py b/src/dishka/text_rendering/name.py index 306b7a9e5..70021e96f 100644 --- a/src/dishka/text_rendering/name.py +++ b/src/dishka/text_rendering/name.py @@ -1,4 +1,6 @@ -from typing import Any +from typing import Any, get_args + +from dishka._adaptix.type_tools import is_generic, strip_alias def get_name(hint: Any, *, include_module: bool) -> str: @@ -20,6 +22,13 @@ def get_name(hint: Any, *, include_module: bool) -> str: getattr(hint, "__qualname__", None) or getattr(hint, "__name__", None) ) + if is_generic(strip_alias(hint)): + str_args = ",".join( + (get_name(args, include_module=False) for args in get_args(hint)), + ) + generic_args = f"[{str_args}]" + else: + generic_args = "" if name: - return f"{module}{name}" - return str(hint) + return f"{module}{name}" + generic_args + return str(hint) + generic_args diff --git a/tests/unit/container/test_with_protocols.py b/tests/unit/container/test_with_protocols.py new file mode 100644 index 000000000..90a8bbd3d --- /dev/null +++ b/tests/unit/container/test_with_protocols.py @@ -0,0 +1,38 @@ +from typing import Protocol + +import pytest + +from dishka import make_container, Provider, Scope, WithProtocols +from dishka.exceptions import NoFactoryError + + +class AProtocol(Protocol): + pass + + +class BProtocol(Protocol): + pass + + +class C(AProtocol, BProtocol): + pass + + +def test_get_parents_protocols() -> None: + provider = Provider(scope=Scope.APP) + provider.provide(C, provides=WithProtocols[C]) + container = make_container(provider) + + assert ( + container.get(BProtocol) + is container.get(AProtocol) + ) + + +def test_get_by_not_protocol() -> None: + provider = Provider(scope=Scope.APP) + provider.provide(C, provides=WithProtocols[C]) + container = make_container(provider) + + with pytest.raises(NoFactoryError): + container.get(C) \ No newline at end of file From fdafc99be719aac180c785191e20e9111495758e Mon Sep 17 00:00:00 2001 From: IvanKirpichnikov Date: Fri, 18 Oct 2024 17:36:08 +0300 Subject: [PATCH 2/4] add docs and fix ruff --- docs/provider/provide.rst | 23 +++++++++++++++++++-- tests/unit/container/test_with_protocols.py | 8 +++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/provider/provide.rst b/docs/provider/provide.rst index 390dda673..7dfe49260 100644 --- a/docs/provider/provide.rst +++ b/docs/provider/provide.rst @@ -132,8 +132,9 @@ It works similar to :ref:`alias`. return A() container = make_async_container(MyProvider()) - await container.get(A) - + a = await container.get(A) + a = await container.get(AImpl) + a is a # True WithParents generates only one factory and many aliases and is equivalent to ``AnyOf[AImpl, A]``. The following parents are ignored: ``type``, ``object``, ``Enum``, ``ABC``, ``ABCMeta``, ``Generic``, ``Protocol``, ``Exception``, ``BaseException`` @@ -176,3 +177,21 @@ WithParents generates only one factory and many aliases and is equivalent to ``A def make_a(self, type_: type[T]) -> A[T]: ... +* Do you want to get "dependencies" by parents which are protocols? Use ``WithProtocols`` as a result hint: + +.. code-block:: python + + from dishka import WithProtocols, provide, Provider, Scope + + class A(Protocol): ... + class AImpl(A): ... + + class MyProvider(Provider): + scope=Scope.APP + + @provide + def a(self) -> WithProtocols[AImpl]: + return A() + + container = make_async_container(MyProvider()) + await container.get(A) diff --git a/tests/unit/container/test_with_protocols.py b/tests/unit/container/test_with_protocols.py index 90a8bbd3d..5eec0f58d 100644 --- a/tests/unit/container/test_with_protocols.py +++ b/tests/unit/container/test_with_protocols.py @@ -2,7 +2,7 @@ import pytest -from dishka import make_container, Provider, Scope, WithProtocols +from dishka import Provider, Scope, WithProtocols, make_container from dishka.exceptions import NoFactoryError @@ -22,7 +22,7 @@ def test_get_parents_protocols() -> None: provider = Provider(scope=Scope.APP) provider.provide(C, provides=WithProtocols[C]) container = make_container(provider) - + assert ( container.get(BProtocol) is container.get(AProtocol) @@ -33,6 +33,6 @@ def test_get_by_not_protocol() -> None: provider = Provider(scope=Scope.APP) provider.provide(C, provides=WithProtocols[C]) container = make_container(provider) - + with pytest.raises(NoFactoryError): - container.get(C) \ No newline at end of file + container.get(C) From b107da4213ff21806d6659d70cd4f94c7f0451e8 Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov <17@itishka.org> Date: Sat, 19 Oct 2024 23:14:37 +0200 Subject: [PATCH 3/4] Update registry.py --- src/dishka/registry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dishka/registry.py b/src/dishka/registry.py index 0c2e5e54d..c7d67b5c4 100644 --- a/src/dishka/registry.py +++ b/src/dishka/registry.py @@ -90,7 +90,7 @@ def _get_type_var_factory(self, dependency: DependencyKey) -> Factory: scope=self.scope, dependencies=[], kw_dependencies={}, - provides=DependencyKey(type[typevar], dependency.component), # type: ignore[misc] + provides=DependencyKey(type[typevar], dependency.component), type_=FactoryType.FACTORY, is_to_bind=False, cache=False, From d8c0ea799740931811a9442b352818348ae8a79f Mon Sep 17 00:00:00 2001 From: Andrey Tikhonov <17@itishka.org> Date: Sat, 19 Oct 2024 23:21:57 +0200 Subject: [PATCH 4/4] revert type ignore --- src/dishka/dependency_source/make_factory.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dishka/dependency_source/make_factory.py b/src/dishka/dependency_source/make_factory.py index 77e06fb92..e3fa0aa44 100644 --- a/src/dishka/dependency_source/make_factory.py +++ b/src/dishka/dependency_source/make_factory.py @@ -221,7 +221,7 @@ def _make_factory_by_class( hints = dict(res.get_resolved_members(source).members) except NameError as e: name = get_name(source, include_module=True) + ".__init__" - raise NameError( # type: ignore[call-arg] + raise NameError( f"Failed to analyze `{name}`. \n" f"Type '{e.name}' is not defined\n\n" f"If your are using `if TYPE_CHECKING` to import '{e.name}' " @@ -279,7 +279,7 @@ def _make_factory_by_function( hints = get_type_hints(source, include_extras=True) except NameError as e: name = get_name(source, include_module=True) - raise NameError( # type: ignore[call-arg] + raise NameError( f"Failed to analyze `{name}`. \n" f"Type '{e.name}' is not defined. \n\n" f"If your are using `if TYPE_CHECKING` to import '{e.name}' " @@ -345,7 +345,7 @@ def _make_factory_by_static_method( hints = get_type_hints(source, include_extras=True) except NameError as e: name = get_name(source, include_module=True) - raise NameError( # type: ignore[call-arg] + raise NameError( f"Failed to analyze `{name}`. \n" f"Type '{e.name}' is not defined. \n\n" f"If your are using `if TYPE_CHECKING` to import '{e.name}' "