Repository: reagento/dishka
Pull request #426 (2025-04-08 19:18:06)

Issue 417: forbid Protocol in dependency source
Closes:
- #417



Patch:
diff --git a/src/dishka/provider/exceptions.py b/src/dishka/provider/exceptions.py
index c1f9def..c989cf8 100644
--- a/src/dishka/provider/exceptions.py
+++ b/src/dishka/provider/exceptions.py
@@ -6,11 +6,20 @@ from dishka.text_rendering import get_name
 
 
 class NotAFactoryError(TypeError, DishkaError):
-    def __init__(self, attempted_factory_type: type) -> None:
-        self.type = attempted_factory_type
+    def __init__(self, attempted_factory: Any) -> None:
+        self.attempted = attempted_factory
 
     def __str__(self) -> str:
-        return f"Cannot use {self.type} as a factory"
+        return f"Cannot use {self.attempted!r} as a factory."
+
+
+class CannotUseProtocolError(NotAFactoryError):
+    def __str__(self) -> str:
+        return (
+            f"Cannot use {self.attempted} as a factory.\n"
+            f"Tip: seems that this is a Protocol. "
+            "Please subclass it and provide the subclass."
+        )
 
 
 class UnsupportedGeneratorReturnTypeError(TypeError, DishkaError):
diff --git a/src/dishka/provider/make_factory.py b/src/dishka/provider/make_factory.py
index 468920e..0d5fa4d 100644
--- a/src/dishka/provider/make_factory.py
+++ b/src/dishka/provider/make_factory.py
@@ -37,6 +37,7 @@ from typing import (
 from dishka._adaptix.type_tools.basic_utils import (  # type: ignore[attr-defined]
     get_type_vars,
     is_bare_generic,
+    is_protocol,
     strip_alias,
 )
 from dishka._adaptix.type_tools.fundamentals import (
@@ -64,6 +65,7 @@ from dishka.entities.type_alias_type import (
 )
 from dishka.text_rendering import get_name
 from .exceptions import (
+    CannotUseProtocolError,
     MissingHintsError,
     MissingReturnHintError,
     NotAFactoryError,
@@ -445,6 +447,9 @@ def make_factory(
         is_in_class: bool,
         override: bool,
 ) -> Factory:
+    if source and is_protocol(source):
+        raise CannotUseProtocolError(source)
+
     if get_origin(source) is ProvideMultiple:
         if provides is None:
             provides = source
@@ -498,7 +503,7 @@ def make_factory(
             override=override,
         )
     else:
-        raise NotAFactoryError(type(source))
+        raise NotAFactoryError(source)
 
 
 def _provide(


Test patch:
diff --git a/tests/unit/test_provider.py b/tests/unit/test_provider.py
index 1d036f2..1c010a4 100644
--- a/tests/unit/test_provider.py
+++ b/tests/unit/test_provider.py
@@ -19,6 +19,7 @@ from dishka.entities.key import (
     hint_to_dependency_key,
 )
 from dishka.provider.exceptions import (
+    CannotUseProtocolError,
     MissingHintsError,
     MissingReturnHintError,
     NoScopeSetInContextError,
@@ -26,7 +27,12 @@ from dishka.provider.exceptions import (
     NotAFactoryError,
     UndefinedTypeAnalysisError,
 )
-from dishka.provider.make_factory import make_factory, provide_all
+from dishka.provider.make_factory import (
+    make_factory,
+    provide_all,
+    provide_all_on_instance,
+    provide_on_instance,
+)
 from .sample_providers import (
     ClassA,
     async_func_a,
@@ -523,3 +529,23 @@ def test_factory_error_hints(source):
 def test_not_a_factory():
     with pytest.raises(NotAFactoryError):
         make_factory_by_source(source=None)
+
+
+@pytest.mark.parametrize(
+    "provide_func",
+    [
+        provide,
+        provide_all,
+        lambda source: provide_on_instance(source=source),
+        provide_all_on_instance,
+    ],
+)
+def test_protocol_cannot_be_source_in_provide(provide_func):
+    class AProtocol(Protocol): ...
+
+    with pytest.raises(
+        CannotUseProtocolError,
+        match="Cannot use.*\n.*seems that this is a Protocol.*",
+    ):
+        class P(Provider):
+            p = provide_func(AProtocol)