Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WithProtocols marker #276

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/provider/provide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ It works similar to :ref:`alias`.
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``

* You object's dependencies (and their dependencies) can be simply created by calling their constructors. You do not need to register them manually. Use ``recursive=True`` to register them automatically
Expand Down Expand Up @@ -178,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)
2 changes: 2 additions & 0 deletions src/dishka/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"provide",
"provide_all",
"new_scope",
"WithProtocols",
"ValidationSettings",
"STRICT_VALIDATION",
]
Expand All @@ -39,4 +40,5 @@
from .entities.scope import BaseScope, Scope, new_scope
from .entities.validation_settigs import STRICT_VALIDATION, ValidationSettings
from .entities.with_parents import WithParents
from .entities.with_protocols import WithProtocols
from .provider import Provider
5 changes: 4 additions & 1 deletion src/dishka/entities/with_parents.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

__all__ = ["WithParents", "ParentsResolver"]

from dishka.text_rendering import get_name

IGNORE_TYPES: Final = (
type,
object,
Expand Down Expand Up @@ -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)
Expand Down
42 changes: 42 additions & 0 deletions src/dishka/entities/with_protocols.py
Original file line number Diff line number Diff line change
@@ -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]
38 changes: 38 additions & 0 deletions tests/unit/container/test_with_protocols.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from typing import Protocol

import pytest

from dishka import Provider, Scope, WithProtocols, make_container
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])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need either another naming or another logic.
For WithParents we provide class itself together with parents. The same is expected here if we keep the prefix With

container = make_container(provider)

with pytest.raises(NoFactoryError):
container.get(C)