Repository: reagento/dishka
Pull request #222 (2024-08-24 20:47:20)

Provide recursive
Todo:
* [x] Call on instance
* [x] Docs


Patch:
diff --git a/docs/provider/provide.rst b/docs/provider/provide.rst
index 81b8df4..04669d4 100644
--- a/docs/provider/provide.rst
+++ b/docs/provider/provide.rst
@@ -138,3 +138,18 @@ It works similar to :ref:`alias`.
 
 
 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
+
+.. code-block:: python
+
+    class A: ...
+
+    class B:
+        def __init__(self, a: A): ...
+
+    class C:
+        def __init__(self, b: B): ...
+
+    class MyProvider(Provider):
+        c = provide(C, scope=Scope.APP, recursive=True)
diff --git a/src/dishka/dependency_source/make_factory.py b/src/dishka/dependency_source/make_factory.py
index 2aa63f8..84b83c3 100644
--- a/src/dishka/dependency_source/make_factory.py
+++ b/src/dishka/dependency_source/make_factory.py
@@ -45,6 +45,7 @@ from dishka._adaptix.type_tools.generic_resolver import (
     MembersStorage,
 )
 from dishka.entities.key import (
+    dependency_key_to_hint,
     hint_to_dependency_key,
     hints_to_dependency_keys,
 )
@@ -470,6 +471,7 @@ def _provide(
         provides: Any = None,
         cache: bool = True,
         is_in_class: bool = True,
+        recursive: bool = False,
 ) -> CompositeDependencySource:
     composite = ensure_composite(source)
     factory = make_factory(
@@ -478,6 +480,22 @@ def _provide(
         is_in_class=is_in_class,
     )
     composite.dependency_sources.extend(unpack_factory(factory))
+    if not recursive:
+        return composite
+
+    for src in composite.dependency_sources:
+        if not isinstance(src, Factory):
+            # we expect Factory and Alias here
+            continue
+        for dependency in src.dependencies:
+            additional = _provide(
+                provides=dependency_key_to_hint(dependency),
+                scope=scope,
+                source=dependency.type_hint,
+                cache=cache,
+                is_in_class=is_in_class,
+            )
+            composite.dependency_sources.extend(additional.dependency_sources)
     return composite
 
 
@@ -487,10 +505,12 @@ def provide_on_instance(
         scope: BaseScope | None = None,
         provides: Any = None,
         cache: bool = True,
+        recursive: bool = False,
 ) -> CompositeDependencySource:
     return _provide(
         provides=provides, scope=scope, source=source, cache=cache,
         is_in_class=False,
+        recursive=recursive,
     )
 
 
@@ -500,6 +520,7 @@ def provide(
         scope: BaseScope | None = None,
         provides: Any = None,
         cache: bool = True,
+        recursive: bool = False,
 ) -> Callable[[Callable[..., Any]], CompositeDependencySource]:
     ...
 
@@ -511,6 +532,7 @@ def provide(
         scope: BaseScope | None = None,
         provides: Any = None,
         cache: bool = True,
+        recursive: bool = False,
 ) -> CompositeDependencySource:
     ...
 
@@ -521,6 +543,7 @@ def provide(
         scope: BaseScope | None = None,
         provides: Any = None,
         cache: bool = True,
+        recursive: bool = False,
 ) -> CompositeDependencySource | Callable[
     [Callable[..., Any]], CompositeDependencySource,
 ]:
@@ -543,17 +566,18 @@ def provide(
     :param provides: Dependency type which is provided by this factory
     :return: instance of Factory or a decorator returning it
     :param cache: save created object to scope cache or not
+    :param recursive: register dependencies as factories as well
     """
     if source is not None:
         return _provide(
             provides=provides, scope=scope, source=source, cache=cache,
-            is_in_class=True,
+            is_in_class=True, recursive=recursive,
         )
 
     def scoped(func: Callable[..., Any]) -> CompositeDependencySource:
         return _provide(
             provides=provides, scope=scope, source=func, cache=cache,
-            is_in_class=True,
+            is_in_class=True, recursive=recursive,
         )
 
     return scoped
@@ -565,17 +589,19 @@ def _provide_all(
         scope: BaseScope | None,
         cache: bool,
         is_in_class: bool,
+        recursive: bool,
 ) -> CompositeDependencySource:
     composite = CompositeDependencySource(None)
     for single_provides in provides:
-        factory = make_factory(
+        source = _provide(
             source=single_provides,
             provides=single_provides,
             scope=scope,
             cache=cache,
             is_in_class=is_in_class,
+            recursive=recursive,
         )
-        composite.dependency_sources.extend(unpack_factory(factory))
+        composite.dependency_sources.extend(source.dependency_sources)
     return composite
 
 
@@ -583,10 +609,12 @@ def provide_all(
         *provides: Any,
         scope: BaseScope | None = None,
         cache: bool = True,
+        recursive: bool = False,
 ) -> CompositeDependencySource:
     return _provide_all(
         provides=provides, scope=scope,
         cache=cache, is_in_class=True,
+        recursive=recursive,
     )
 
 
@@ -594,8 +622,10 @@ def provide_all_on_instance(
         *provides: Any,
         scope: BaseScope | None = None,
         cache: bool = True,
+        recursive: bool = False,
 ) -> CompositeDependencySource:
     return _provide_all(
         provides=provides, scope=scope,
         cache=cache, is_in_class=False,
+        recursive=recursive,
     )
diff --git a/src/dishka/entities/key.py b/src/dishka/entities/key.py
index defc057..8fab514 100644
--- a/src/dishka/entities/key.py
+++ b/src/dishka/entities/key.py
@@ -25,6 +25,12 @@ class DependencyKey(NamedTuple):
         return f"({self.type_hint}, component={self.component!r})"
 
 
+def dependency_key_to_hint(key: DependencyKey) -> Any:
+    if key.component is None:
+        return key.type_hint
+    return Annotated[key.type_hint, FromComponent(key.component)]
+
+
 def hint_to_dependency_key(hint: Any) -> DependencyKey:
     if get_origin(hint) is not Annotated:
         return DependencyKey(hint, None)
diff --git a/src/dishka/provider.py b/src/dishka/provider.py
index 8cb29eb..5f2b173 100644
--- a/src/dishka/provider.py
+++ b/src/dishka/provider.py
@@ -136,6 +136,7 @@ class Provider(BaseProvider):
             scope: BaseScope | None = None,
             provides: Any = None,
             cache: bool = True,
+            recursive: bool = False,
     ) -> CompositeDependencySource:
         if scope is None:
             scope = self.scope
@@ -144,6 +145,7 @@ class Provider(BaseProvider):
             scope=scope,
             provides=provides,
             cache=cache,
+            recursive=recursive,
         )
         self._add_dependency_sources(str(source), composite.dependency_sources)
         return composite
@@ -153,11 +155,12 @@ class Provider(BaseProvider):
             *provides: Any,
             scope: BaseScope | None = None,
             cache: bool = True,
+            recursive: bool = False,
     ) -> CompositeDependencySource:
         if scope is None:
             scope = self.scope
         composite = provide_all_on_instance(
-            *provides, scope=scope, cache=cache,
+            *provides, scope=scope, cache=cache, recursive=recursive,
         )
         self._add_dependency_sources("?", composite.dependency_sources)
         return composite


Test patch:
diff --git a/tests/unit/container/test_recursive.py b/tests/unit/container/test_recursive.py
new file mode 100644
index 0000000..a45f951
--- /dev/null
+++ b/tests/unit/container/test_recursive.py
@@ -0,0 +1,86 @@
+from typing import Protocol
+
+from dishka import AnyOf, Provider, Scope, make_container, provide, provide_all
+
+
+class A1:
+    pass
+
+
+class A2:
+    pass
+
+
+class BProto(Protocol):
+    pass
+
+
+class B(BProto):
+    def __init__(self, a1: A1, a2: A2):
+        self.a1 = a1
+        self.a2 = a2
+
+
+class C(B):
+    pass
+
+
+def test_provide_class():
+    class MyProvider(Provider):
+        x = provide(B, scope=Scope.APP, recursive=True)
+
+    container = make_container(MyProvider())
+    b = container.get(B)
+    assert isinstance(b, B)
+    assert isinstance(b.a1, A1)
+    assert isinstance(b.a2, A2)
+
+
+def test_provide_instance():
+    provider = Provider(scope=Scope.APP)
+    provider.provide(B, recursive=True)
+    container = make_container(provider)
+    b = container.get(B)
+    assert isinstance(b, B)
+    assert isinstance(b.a1, A1)
+    assert isinstance(b.a2, A2)
+
+
+def test_provide_any_of():
+    provider = Provider(scope=Scope.APP)
+    provider.provide(source=B, provides=AnyOf[B, BProto], recursive=True)
+    container = make_container(provider)
+    b = container.get(B)
+    assert isinstance(b, B)
+    assert isinstance(b.a1, A1)
+    assert isinstance(b.a2, A2)
+    assert b is container.get(BProto)
+
+
+def test_provide_all_class():
+    class MyProvider(Provider):
+        x = provide_all(B, C, scope=Scope.APP, recursive=True)
+
+    container = make_container(MyProvider())
+    b = container.get(B)
+    assert isinstance(b, B)
+    assert isinstance(b.a1, A1)
+    assert isinstance(b.a2, A2)
+    c = container.get(C)
+    assert isinstance(c, B)
+    assert isinstance(c.a1, A1)
+    assert isinstance(c.a2, A2)
+
+
+def test_provide_all_instance():
+    provider = Provider(scope=Scope.APP)
+    provider.provide_all(B, C, recursive=True)
+    container = make_container(provider)
+    b = container.get(B)
+    assert isinstance(b, B)
+    assert isinstance(b.a1, A1)
+    assert isinstance(b.a2, A2)
+    c = container.get(C)
+    assert isinstance(c, B)
+    assert isinstance(c.a1, A1)
+    assert isinstance(c.a2, A2)