Repository: reagento/dishka
Pull request #52 (2024-02-17 19:29:12)

support generic providers


Patch:
diff --git a/.ruff.toml b/.ruff.toml
index 2c8fb52..f808599 100644
--- a/.ruff.toml
+++ b/.ruff.toml
@@ -3,7 +3,7 @@ src = ["src"]
 include = ["src/**.py", "tests/**.py"]
 exclude = ["src/dishka/_adaptix/**"]
 
-lint.select = [
+select = [
     "E",
     "F",
     "BLE",
@@ -20,8 +20,8 @@ lint.select = [
 ]
 
 
-[lint.isort]
+[isort]
 no-lines-before = ["local-folder"]
 
-[lint.flake8-tidy-imports]
+[flake8-tidy-imports]
 ban-relative-imports = "parents"
\ No newline at end of file
diff --git a/src/dishka/dependency_source.py b/src/dishka/dependency_source.py
index 92b6d99..9e346c3 100644
--- a/src/dishka/dependency_source.py
+++ b/src/dishka/dependency_source.py
@@ -19,7 +19,11 @@ from typing import (
     overload,
 )
 
-from ._adaptix.type_tools.basic_utils import get_all_type_hints
+from ._adaptix.type_tools.basic_utils import (
+    get_all_type_hints,
+    get_type_vars,
+    is_bare_generic,
+)
 from ._adaptix.type_tools.generic_resolver import (
     GenericResolver,
     MembersStorage,
@@ -98,6 +102,8 @@ def make_factory(
         scope: Optional[BaseScope],
         source: Callable,
 ) -> Factory:
+    if is_bare_generic(source):
+        source = source[get_type_vars(source)]
     if isclass(source) or get_origin(source):
         # we need to fix concrete generics and normal classes as well
         # as classes can be children of concrete generics
diff --git a/src/dishka/integrations/aiogram.py b/src/dishka/integrations/aiogram.py
index 19e24ae..fad5868 100644
--- a/src/dishka/integrations/aiogram.py
+++ b/src/dishka/integrations/aiogram.py
@@ -3,6 +3,7 @@ __all__ = [
     "inject",
     "setup_dishka",
 ]
+
 from inspect import Parameter
 from typing import Container, Sequence
 
@@ -12,12 +13,10 @@ from aiogram.types import TelegramObject
 from dishka import Provider, make_async_container
 from .base import Depends, wrap_injection
 
-CONTAINER_NAME = "dishka_container"
-
 
 def inject(func):
     additional_params = [Parameter(
-        name=CONTAINER_NAME,
+        name="dishka_container",
         annotation=Container,
         kind=Parameter.KEYWORD_ONLY,
     )]
@@ -25,7 +24,7 @@ def inject(func):
     return wrap_injection(
         func=func,
         remove_depends=True,
-        container_getter=lambda _, p: p[CONTAINER_NAME],
+        container_getter=lambda _, p: p["dishka_container"],
         additional_params=additional_params,
         is_async=True,
     )
@@ -39,8 +38,8 @@ class ContainerMiddleware(BaseMiddleware):
     async def __call__(
             self, handler, event, data,
     ):
-        async with self.container({TelegramObject: event}) as sub_container:
-            data[CONTAINER_NAME] = sub_container
+        async with self.container({TelegramObject: event}) as subcontainer:
+            data["dishka_container"] = subcontainer
             return await handler(event, data)
 
     async def startup(self):
diff --git a/src/dishka/integrations/telebot.py b/src/dishka/integrations/telebot.py
index d5b6678..cd41f13 100644
--- a/src/dishka/integrations/telebot.py
+++ b/src/dishka/integrations/telebot.py
@@ -13,11 +13,10 @@ from telebot import BaseMiddleware, TeleBot
 from dishka import Container, Provider, make_container
 from .base import Depends, wrap_injection
 
-CONTAINER_NAME = "dishka_container"
 
 def inject(func):
     additional_params = [Parameter(
-        name=CONTAINER_NAME,
+        name="dishka_container",
         annotation=Container,
         kind=Parameter.KEYWORD_ONLY,
     )]
@@ -25,7 +24,7 @@ def inject(func):
     return wrap_injection(
         func=func,
         remove_depends=True,
-        container_getter=lambda _, p: p[CONTAINER_NAME],
+        container_getter=lambda _, p: p["dishka_container"],
         additional_params=additional_params,
         is_async=False,
     )
@@ -40,11 +39,11 @@ class ContainerMiddleware(BaseMiddleware):
 
     def pre_process(self, message, data):
         dishka_container_wrapper = self.container({type(message): message})
-        data[CONTAINER_NAME + "_wrapper"] = dishka_container_wrapper
-        data[CONTAINER_NAME] = dishka_container_wrapper.__enter__()
+        data["dishka_container_wrapper"] = dishka_container_wrapper
+        data["dishka_container"] = dishka_container_wrapper.__enter__()
 
     def post_process(self, message, data, exception):
-        data[CONTAINER_NAME + "_wrapper"].__exit__(None, None, None)
+        data["dishka_container_wrapper"].__exit__(None, None, None)
 
 
 def setup_dishka(providers: Sequence[Provider], bot: TeleBot) -> Container:
diff --git a/src/dishka/py.typed b/src/dishka/py.typed
deleted file mode 100644
index e69de29..0000000
diff --git a/src/dishka/registry.py b/src/dishka/registry.py
index 5bc0412..e59d26e 100644
--- a/src/dishka/registry.py
+++ b/src/dishka/registry.py
@@ -1,6 +1,7 @@
 from collections import defaultdict
-from typing import Any, List, NewType, Type
+from typing import Any, List, NewType, Type, TypeVar, get_args, get_origin
 
+from ._adaptix.type_tools.basic_utils import get_type_vars, is_generic
 from .dependency_source import Factory
 from .exceptions import InvalidGraphError
 from .provider import Provider
@@ -15,10 +16,51 @@ class Registry:
         self.scope = scope
 
     def add_provider(self, factory: Factory):
-        self._factories[factory.provides] = factory
+        if is_generic(factory.provides):
+            self._factories[get_origin(factory.provides)] = factory
+        else:
+            self._factories[factory.provides] = factory
 
-    def get_provider(self, dependency: Any) -> Factory:
-        return self._factories.get(dependency)
+    def get_provider(self, dependency: Any) -> Factory | None:
+        try:
+            return self._factories[dependency]
+        except KeyError:
+            origin = get_origin(dependency)
+            if not origin:
+                return None
+            factory = self._factories.get(origin)
+            if not factory:
+                return None
+
+            factory = self._specialize_generic(factory, dependency)
+            self._factories[dependency] = factory
+            return factory
+
+    def _specialize_generic(
+            self, factory: Factory, dependency: Any,
+    ) -> Factory:
+        params_replacement = dict(zip(
+            get_args(factory.provides),
+            get_args(dependency),
+        ))
+        new_dependencies = []
+        for source_dependency in factory.dependencies:
+            if isinstance(source_dependency, TypeVar):
+                source_dependency = params_replacement[source_dependency]
+            elif get_origin(source_dependency):
+                source_dependency = source_dependency[tuple(
+                    params_replacement[param]
+                    for param in get_type_vars(source_dependency)
+                )]
+            new_dependencies.append(source_dependency)
+        return Factory(
+            source=factory.source,
+            provides=dependency,
+            dependencies=new_dependencies,
+            is_to_bound=factory.is_to_bound,
+            type=factory.type,
+            scope=factory.scope,
+        )
 
 
 def make_registries(


Test patch:
diff --git a/tests/unit/container/test_generic.py b/tests/unit/container/test_generic.py
index 738fbab..7db169b 100644
--- a/tests/unit/container/test_generic.py
+++ b/tests/unit/container/test_generic.py
@@ -65,3 +65,37 @@ def test_concrete_child():
         a = container.get(C)
         assert isinstance(a, C)
         assert a.x == 42
+
+
+def test_generic_class():
+    class MyProvider(Provider):
+        scope = Scope.APP
+
+        @provide
+        def get_int(self) -> int:
+            return 42
+
+        a = provide(A)
+
+    with make_container(MyProvider()) as container:
+        a = container.get(A[int])
+        assert isinstance(a, A)
+        assert a.x == 42
+
+
+def test_generic_func():
+    class MyProvider(Provider):
+        scope = Scope.APP
+
+        @provide
+        def get_int(self) -> int:
+            return 42
+
+        @provide
+        def a(self, param: T) -> A[T]:
+            return A(param)
+
+    with make_container(MyProvider()) as container:
+        a = container.get(A[int])
+        assert isinstance(a, A)
+        assert a.x == 42