Repository: reagento/dishka
Pull request #448 (2025-04-26 20:28:45)

Fix AttributeError when make factory for Union[...], Final[...] and etc. 
Closes: #437

Now the following error is raised: 
```
dishka.provider.exceptions.NotAFactoryError: Cannot use typing.Union[str, int] as a factory.
```


Patch:
diff --git a/src/dishka/provider/make_factory.py b/src/dishka/provider/make_factory.py
index 0d5fa4d..8a0062d 100644
--- a/src/dishka/provider/make_factory.py
+++ b/src/dishka/provider/make_factory.py
@@ -252,8 +252,6 @@ def _make_factory_by_class(
     if not provides:
         provides = source
 
-    if get_origin(source) is Annotated:
-        source = get_args(source)[0]
     init = strip_alias(source).__init__
     if missing_hints := _params_without_hints(init, skip_self=True):
         raise MissingHintsError(source, missing_hints, append_init=True)
@@ -438,6 +436,23 @@ def _make_factory_by_other_callable(
     )
 
 
+def _extract_source(
+    provides: Any,
+    source: ProvideSource,
+) -> tuple[Any, ProvideSource]:
+    if get_origin(source) is Annotated:
+        source = get_args(source)[0]
+
+    if get_origin(source) is ProvideMultiple:
+        if provides is None:
+            provides = source
+        source = get_args(source)[0]
+
+    if is_bare_generic(source):
+        source = source[get_type_vars(source)]  # type: ignore[index]
+    return provides, source
+
+
 def make_factory(
         *,
         provides: Any,
@@ -447,18 +462,14 @@ def make_factory(
         is_in_class: bool,
         override: bool,
 ) -> Factory:
+    provides, source = _extract_source(provides, source)
+
     if source and is_protocol(source):
         raise CannotUseProtocolError(source)
 
-    if get_origin(source) is ProvideMultiple:
-        if provides is None:
-            provides = source
-        source = get_args(source)[0]
-
-    if is_bare_generic(source):
-        source = source[get_type_vars(source)]  # type: ignore[index]
+    source_origin = get_origin(source)
 
-    if isclass(source) or get_origin(source):
+    if isclass(source) or isclass(source_origin):
         return _make_factory_by_class(
             provides=provides,
             scope=scope,
@@ -494,7 +505,7 @@ def make_factory(
             cache=cache,
             override=override,
         )
-    elif callable(source):
+    elif callable(source) and not source_origin:
         return _make_factory_by_other_callable(
             provides=provides,
             scope=scope,


Test patch:
diff --git a/tests/unit/test_provider.py b/tests/unit/test_provider.py
index 1c010a4..f7064d2 100644
--- a/tests/unit/test_provider.py
+++ b/tests/unit/test_provider.py
@@ -9,7 +9,16 @@ from collections.abc import (
 from functools import partial
 from random import random
 from types import NoneType
-from typing import Annotated, Any, Protocol
+from typing import (
+    Annotated,
+    Any,
+    ClassVar,
+    Final,
+    Literal,
+    Optional,
+    Protocol,
+    Union,
+)
 
 import pytest
 
@@ -531,6 +540,9 @@ def test_not_a_factory():
         make_factory_by_source(source=None)
 
 
+class AProtocol(Protocol): ...
+
+
 @pytest.mark.parametrize(
     "provide_func",
     [
@@ -540,12 +552,33 @@ def test_not_a_factory():
         provide_all_on_instance,
     ],
 )
-def test_protocol_cannot_be_source_in_provide(provide_func):
-    class AProtocol(Protocol): ...
+@pytest.mark.parametrize(
+    "proto",
+    [
+        AProtocol,
+        Annotated[AProtocol, "metadata"],
+    ],
+)
+def test_protocol_cannot_be_source_in_provide(provide_func, proto):
 
     with pytest.raises(
         CannotUseProtocolError,
         match="Cannot use.*\n.*seems that this is a Protocol.*",
     ):
         class P(Provider):
-            p = provide_func(AProtocol)
+            p = provide_func(proto)
+
+
+@pytest.mark.parametrize(
+    "type_hint",
+    [
+        Union[str, int],  # noqa: UP007
+        Final[str],
+        ClassVar[str],
+        Optional[str],  # noqa: UP007
+        Literal["5"],
+    ],
+)
+def test_generic_alias_not_a_factory(type_hint):
+    with pytest.raises(NotAFactoryError):
+        make_factory_by_source(source=type_hint)