Repository: reagento/dishka (python)

Issue:
AnyOf["SomeObject", "AnotherObject"] don't work
I tried to specify types via strings but it gives an error.
MRE:
```python
@provide(scope=Scope.REQUEST)
async def new_async_session(
    self, session_factory: async_sessionmaker[AsyncSession]
) -> AsyncIterator[AnyOf["AsyncSession", "UoWProtocol"]]:
    async with session_factory() as session:
        yield session
```
Error:
```dishka.exceptions.GraphMissingFactoryError: Cannot find factory for (<class 'sqlalchemy.ext.asyncio.session.AsyncSession'>, component=''). It is missing or has invalid scope.```


Pull request:
Fix ForwardRef used in AnyOf
/closes #290 

Patch:
diff --git a/src/dishka/async_container.py b/src/dishka/async_container.py
index 1993ffb..f0b0da6 100644
--- a/src/dishka/async_container.py
+++ b/src/dishka/async_container.py
@@ -51,7 +51,7 @@ class AsyncContainer:
     ):
         self.registry = registry
         self.child_registries = child_registries
-        self._context = {CONTAINER_KEY: self}
+        self._context = {DependencyKey(type(self), DEFAULT_COMPONENT): self}
         if context:
             for key, value in context.items():
                 if not isinstance(key, DependencyKey):
@@ -227,7 +227,7 @@ def make_async_container(
 ) -> AsyncContainer:
     registries = RegistryBuilder(
         scopes=scopes,
-        container_key=CONTAINER_KEY,
+        container_type=AsyncContainer,
         providers=providers,
         skip_validation=skip_validation,
         validation_settings=validation_settings,
@@ -257,6 +257,3 @@ def make_async_container(
                 close_parent=True,
             )
     return container
-
-
-CONTAINER_KEY = DependencyKey(AsyncContainer, DEFAULT_COMPONENT)
diff --git a/src/dishka/container.py b/src/dishka/container.py
index 45d0a3d..a7a4acc 100644
--- a/src/dishka/container.py
+++ b/src/dishka/container.py
@@ -51,7 +51,7 @@ class Container:
     ):
         self.registry = registry
         self.child_registries = child_registries
-        self._context = {CONTAINER_KEY: self}
+        self._context = {DependencyKey(type(self), DEFAULT_COMPONENT): self}
         if context:
             for key, value in context.items():
                 if not isinstance(key, DependencyKey):
@@ -223,7 +223,7 @@ def make_container(
 ) -> Container:
     registries = RegistryBuilder(
         scopes=scopes,
-        container_key=CONTAINER_KEY,
+        container_type=Container,
         providers=providers,
         skip_validation=skip_validation,
         validation_settings=validation_settings,
@@ -252,6 +252,3 @@ def make_container(
                 close_parent=True,
             )
     return container
-
-
-CONTAINER_KEY = DependencyKey(Container, DEFAULT_COMPONENT)
diff --git a/src/dishka/dependency_source/make_factory.py b/src/dishka/dependency_source/make_factory.py
index e3fa0aa..d5a97cc 100644
--- a/src/dishka/dependency_source/make_factory.py
+++ b/src/dishka/dependency_source/make_factory.py
@@ -103,10 +103,10 @@ def _type_repr(hint: Any) -> str:
 
 
 def _async_generator_result(hint: Any) -> Any:
-    if isinstance(hint, ProvideMultiple):
-        return ProvideMultiple([
-            _async_generator_result(x) for x in hint.items
-        ])
+    if get_origin(hint) is ProvideMultiple:
+        return ProvideMultiple[tuple(  # type: ignore[misc]
+            _async_generator_result(x) for x in get_args(hint)
+        )]
     origin = get_origin(hint)
     if origin is AsyncIterable:
         return get_args(hint)[0]
@@ -136,10 +136,10 @@ def _async_generator_result(hint: Any) -> Any:
 
 
 def _generator_result(hint: Any) -> Any:
-    if isinstance(hint, ProvideMultiple):
-        return ProvideMultiple([
-            _generator_result(x) for x in hint.items
-        ])
+    if get_origin(hint) is ProvideMultiple:
+        return ProvideMultiple[tuple(  # type: ignore[misc]
+            _generator_result(x) for x in get_args(hint)
+        )]
     origin = get_origin(hint)
     if origin is Iterable:
         return get_args(hint)[0]
diff --git a/src/dishka/dependency_source/unpack_provides.py b/src/dishka/dependency_source/unpack_provides.py
index 173f51e..ed1b62b 100644
--- a/src/dishka/dependency_source/unpack_provides.py
+++ b/src/dishka/dependency_source/unpack_provides.py
@@ -1,4 +1,5 @@
 from collections.abc import Sequence
+from typing import get_args, get_origin
 
 from dishka.entities.key import DependencyKey
 from dishka.entities.provides_marker import ProvideMultiple
@@ -9,10 +10,10 @@ from .factory import Factory
 
 
 def unpack_factory(factory: Factory) -> Sequence[DependencySource]:
-    if not isinstance(factory.provides.type_hint, ProvideMultiple):
+    if get_origin(factory.provides.type_hint) is not ProvideMultiple:
         return [factory]
 
-    provides_first, *provides_others = factory.provides.type_hint.items
+    provides_first, *provides_others = get_args(factory.provides.type_hint)
 
     res: list[DependencySource] = [
         Alias(
@@ -43,7 +44,7 @@ def unpack_factory(factory: Factory) -> Sequence[DependencySource]:
 
 
 def unpack_decorator(decorator: Decorator) -> Sequence[DependencySource]:
-    if not isinstance(decorator.provides.type_hint, ProvideMultiple):
+    if get_origin(decorator.provides.type_hint) is not ProvideMultiple:
         return [decorator]
 
     return [
@@ -51,12 +52,12 @@ def unpack_decorator(decorator: Decorator) -> Sequence[DependencySource]:
             factory=decorator.factory,
             provides=DependencyKey(provides, decorator.provides.component),
         )
-        for provides in decorator.provides.type_hint.items
+        for provides in get_args(decorator.provides.type_hint)
     ]
 
 
 def unpack_alias(alias: Alias) -> Sequence[DependencySource]:
-    if not isinstance(alias.provides.type_hint, ProvideMultiple):
+    if get_origin(alias.provides.type_hint) is not ProvideMultiple:
         return [alias]
 
     return [
@@ -66,5 +67,5 @@ def unpack_alias(alias: Alias) -> Sequence[DependencySource]:
             cache=alias.cache,
             override=alias.override,
         )
-        for provides in alias.provides.type_hint.items
+        for provides in get_args(alias.provides.type_hint)
     ]
diff --git a/src/dishka/entities/provides_marker.py b/src/dishka/entities/provides_marker.py
index d786426..f6b9cf2 100644
--- a/src/dishka/entities/provides_marker.py
+++ b/src/dishka/entities/provides_marker.py
@@ -1,20 +1,32 @@
 from __future__ import annotations
 
-from collections.abc import Sequence
-from typing import TYPE_CHECKING, Any
+import sys
+import threading
+from typing import TYPE_CHECKING, Any, Generic, TypeVar
 
 __all__ = ["AnyOf", "ProvideMultiple"]
 
-if TYPE_CHECKING:
-    from typing import Union as AnyOf
+
+if sys.version_info >= (3, 11):
+    from typing import TypeVarTuple, Unpack
+
+    Variants = TypeVarTuple("Variants")
+    class ProvideMultiple(Generic[Unpack[Variants]]):
+        pass
 else:
-    class AnyOf:
-        def __class_getitem__(cls, item: Any) -> Any:
-            if isinstance(item, tuple):
-                return ProvideMultiple(item)
-            return item
+    # in this case we simulate generics with variadic typevars
+    # by suppressing builtin checks of parameters length
+    Variants = TypeVar("Variants")
+    provides_lock = threading.Lock()
 
+    class ProvideMultiple(Generic[Variants]):
+        def __class_getitem__(cls, item: tuple[Any]):
+            with provides_lock:
+                cls.__parameters__ = [Variants]*len(item)  # type: ignore[attr-defined, misc]
+                return super().__class_getitem__(item)  # type: ignore[misc]
 
-class ProvideMultiple:
-    def __init__(self, items: Sequence[Any]) -> None:
-        self.items = items
+
+if TYPE_CHECKING:
+    from typing import Union as AnyOf
+else:
+    AnyOf = ProvideMultiple
diff --git a/src/dishka/entities/with_parents.py b/src/dishka/entities/with_parents.py
index 1d1b7f1..3452037 100644
--- a/src/dishka/entities/with_parents.py
+++ b/src/dishka/entities/with_parents.py
@@ -174,5 +174,5 @@ else:
         def __class_getitem__(cls, item: TypeHint) -> TypeHint:
             parents = ParentsResolver().get_parents(item)
             if len(parents) > 1:
-                return ProvideMultiple(parents)
+                return ProvideMultiple[tuple(parents)]
             return parents[0]
diff --git a/src/dishka/registry.py b/src/dishka/registry.py
index fd58d3c..c7d67b5 100644
--- a/src/dishka/registry.py
+++ b/src/dishka/registry.py
@@ -14,16 +14,13 @@ from .factory_compiler import compile_factory
 
 
 class Registry:
-    __slots__ = (
-        "scope", "factories", "compiled", "compiled_async", "has_fallback",
-    )
+    __slots__ = ("scope", "factories", "compiled", "compiled_async")
 
-    def __init__(self, scope: BaseScope, *, has_fallback: bool) -> None:
+    def __init__(self, scope: BaseScope):
         self.scope = scope
         self.factories: dict[DependencyKey, Factory] = {}
         self.compiled: dict[DependencyKey, Callable[..., Any]] = {}
         self.compiled_async: dict[DependencyKey, Callable[..., Any]] = {}
-        self.has_fallback = has_fallback
 
     def add_factory(
             self,
@@ -67,7 +64,7 @@ class Registry:
             origin = get_origin(dependency.type_hint)
             if not origin:
                 return None
-            if origin is type and self.has_fallback:
+            if origin is type:
                 return self._get_type_var_factory(dependency)
 
             origin_key = DependencyKey(origin, dependency.component)
@@ -115,10 +112,10 @@ class Registry:
             hint = source_dependency.type_hint
             if isinstance(hint, TypeVar):
                 hint = params_replacement[hint]
-            elif get_origin(hint) and (type_vars:=get_type_vars(hint)):
+            elif get_origin(hint):
                 hint = hint[tuple(
                     params_replacement[param]
-                    for param in type_vars
+                    for param in get_type_vars(hint)
                 )]
             new_dependencies.append(DependencyKey(
                 hint, source_dependency.component,
@@ -128,10 +125,10 @@ class Registry:
             hint = source_dependency.type_hint
             if isinstance(hint, TypeVar):
                 hint = params_replacement[hint]
-            elif get_origin(hint) and (type_vars:=get_type_vars(hint)):
+            elif get_origin(hint):
                 hint = hint[tuple(
                     params_replacement[param]
-                    for param in type_vars
+                    for param in get_type_vars(hint)
                 )]
             new_kw_dependencies[name] = DependencyKey(
                 hint, source_dependency.component,
diff --git a/src/dishka/registry_builder.py b/src/dishka/registry_builder.py
index 8f654cf..ccc7526 100644
--- a/src/dishka/registry_builder.py
+++ b/src/dishka/registry_builder.py
@@ -120,7 +120,7 @@ class RegistryBuilder:
             *,
             scopes: type[BaseScope],
             providers: Sequence[BaseProvider],
-            container_key: DependencyKey,
+            container_type: type,
             skip_validation: bool,
             validation_settings: ValidationSettings,
     ) -> None:
@@ -131,7 +131,7 @@ class RegistryBuilder:
         self.components: set[Component] = {DEFAULT_COMPONENT}
         self.alias_sources: dict[DependencyKey, Any] = {}
         self.aliases: dict[DependencyKey, Alias] = {}
-        self.container_key = container_key
+        self.container_type = container_type
         self.decorator_depth: dict[DependencyKey, int] = defaultdict(int)
         self.skip_validation = skip_validation
         self.validation_settings = validation_settings
@@ -170,18 +170,16 @@ class RegistryBuilder:
                 self.aliases[provides] = alias
 
     def _init_registries(self) -> None:
-        has_fallback = True
         for scope in self.scopes:
-            registry = Registry(scope, has_fallback=has_fallback)
+            registry = Registry(scope)
             context_var = ContextVariable(
-                provides=self.container_key,
+                provides=DependencyKey(self.container_type, DEFAULT_COMPONENT),
                 scope=scope,
                 override=False,
             )
             for component in self.components:
                 registry.add_factory(context_var.as_factory(component))
             self.registries[scope] = registry
-            has_fallback = False
 
     def _process_factory(
             self, provider: BaseProvider, factory: Factory,


Test patch:
diff --git a/tests/unit/container/test_alias.py b/tests/unit/container/test_alias.py
index c079c44..1c69f61 100644
--- a/tests/unit/container/test_alias.py
+++ b/tests/unit/container/test_alias.py
@@ -100,7 +100,7 @@ async def test_implicit_async_generator():
         value = 0
 
         @provide(scope=Scope.APP)
-        async def foo(self) -> AsyncIterable[AnyOf[float, int]]:
+        async def foo(self) -> AsyncIterable[AnyOf[float, "int"]]:
             self.value += 1
             yield self.value
 
diff --git a/tests/unit/container/test_generic.py b/tests/unit/container/test_generic.py
index 4b00e59..79b307f 100644
--- a/tests/unit/container/test_generic.py
+++ b/tests/unit/container/test_generic.py
@@ -207,18 +207,3 @@ def test_passing_type_var_decorator():
     provider.decorate(type_var_decorator)
     container = make_container(provider)
     assert container.get(int) == (int, 1, A(42, y=42))
-
-
-
-def func_with_type(param: type[T], param2: type[int]) -> A[T]:
-    return param, param2
-
-
-def test_provide_type_non_generic():
-    provider = Provider(scope=Scope.APP)
-    provider.provide(func_with_type, scope=Scope.REQUEST)
-    provider.provide(lambda: bool, provides=type[int])
-    container = make_container(provider)
-    with container() as c:
-        assert c.get(A[str]) == (str, bool)
-        assert c.get(A[int]) == (bool, bool)