Repository: reagento/dishka
Pull request #438 (2025-04-17 19:28:14)

automatic context provider
* [x] validation
* [x] Docs


Patch:
diff --git a/docs/advanced/context.rst b/docs/advanced/context.rst
index ac40ed3..5981948 100644
--- a/docs/advanced/context.rst
+++ b/docs/advanced/context.rst
@@ -8,6 +8,10 @@ The difference from normal factories is that they are not created inside some ``
 Working with context data consists of three parts:
 
 1. Declaration that object is received from context using :ref:`from-context`. You need to provide the type and scope.
+    * For the context passed to ``make_container`` and ``make_async_container`` functions it is done automatically in default component.
+    * Context is shared across all providers. You do not need to specify it in each provider.
+    * For the frameworks integrations you can use predefined providers instead of defining context data manually
+    * To access context from additional components you need to use ``from_context`` is each of them in addition to default component.
 2. Usage of that object in providers.
 3. Passing actual values on scope entrance. It can be container creation for top level scope or container calls for nested ones. Use it in form ``context={Type: value,...}``.
 
@@ -20,16 +24,15 @@ Working with context data consists of three parts:
     class MyProvider(Provider):
         scope = Scope.REQUEST
 
-        # declare source
+        # declare context data for nested scope
         request = from_context(provides=Request, scope=Scope.REQUEST)
-        event_broker = from_context(provides=Broker, scope=Scope.APP)
 
         # use objects as usual
         @provide
         def a(self, request: Request, broker: Broker) -> A:
             return A(data=request.contents)
 
-    # provide APP-scoped context variable
+    # passed APP-scoped context variable is automatically available as a dependency
     container = make_container(MyProvider(), context={Broker: broker})
 
     while True:
@@ -40,4 +43,4 @@ Working with context data consists of three parts:
 
 .. note::
 
-    If you are using *multiple components*, you need to specify ``from_context`` in them separately though the context is shared. Context data is always stored in default component, so, other components may not use it and have factories instead.
+    If you are using *multiple components*, you need to specify ``from_context`` in them separately though the context is share.
diff --git a/src/dishka/async_container.py b/src/dishka/async_container.py
index 3627f1a..f95ff1b 100644
--- a/src/dishka/async_container.py
+++ b/src/dishka/async_container.py
@@ -22,7 +22,7 @@ from .exceptions import (
     NoFactoryError,
     NoNonSkippedScopesError,
 )
-from .provider import BaseProvider
+from .provider import BaseProvider, make_root_context_provider
 from .registry import Registry
 from .registry_builder import RegistryBuilder
 
@@ -243,10 +243,11 @@ def make_async_container(
         start_scope: BaseScope | None = None,
         validation_settings: ValidationSettings = DEFAULT_VALIDATION,
 ) -> AsyncContainer:
+    context_provider = make_root_context_provider(context, scopes)
     registries = RegistryBuilder(
         scopes=scopes,
         container_key=CONTAINER_KEY,
-        providers=providers,
+        providers=(*providers, context_provider),
         skip_validation=skip_validation,
         validation_settings=validation_settings,
     ).build()
diff --git a/src/dishka/container.py b/src/dishka/container.py
index e87bea3..b62e3d4 100644
--- a/src/dishka/container.py
+++ b/src/dishka/container.py
@@ -22,7 +22,7 @@ from .exceptions import (
     NoFactoryError,
     NoNonSkippedScopesError,
 )
-from .provider import BaseProvider
+from .provider import BaseProvider, make_root_context_provider
 from .registry import Registry
 from .registry_builder import RegistryBuilder
 
@@ -239,10 +239,11 @@ def make_container(
         start_scope: BaseScope | None = None,
         validation_settings: ValidationSettings = DEFAULT_VALIDATION,
 ) -> Container:
+    context_provider = make_root_context_provider(context, scopes)
     registries = RegistryBuilder(
         scopes=scopes,
         container_key=CONTAINER_KEY,
-        providers=providers,
+        providers=(*providers, context_provider),
         skip_validation=skip_validation,
         validation_settings=validation_settings,
     ).build()
diff --git a/src/dishka/provider/__init__.py b/src/dishka/provider/__init__.py
index 2f95d96..dbb16b0 100644
--- a/src/dishka/provider/__init__.py
+++ b/src/dishka/provider/__init__.py
@@ -5,6 +5,7 @@ __all__ = [
     "alias",
     "decorate",
     "from_context",
+    "make_root_context_provider",
     "provide",
     "provide_all",
 ]
@@ -15,3 +16,4 @@ from .make_context_var import from_context
 from .make_decorator import decorate
 from .make_factory import provide, provide_all
 from .provider import Provider
+from .root_context import make_root_context_provider
diff --git a/src/dishka/provider/root_context.py b/src/dishka/provider/root_context.py
new file mode 100644
index 0000000..0c010ad
--- /dev/null
+++ b/src/dishka/provider/root_context.py
@@ -0,0 +1,18 @@
+from typing import Any
+
+from dishka.entities.scope import BaseScope
+from .base_provider import BaseProvider
+from .provider import Provider
+
+
+def make_root_context_provider(
+        context: dict[Any, Any] | None,
+        scopes: type[BaseScope],
+) -> BaseProvider:
+    p = Provider()
+    if not context:
+        return p
+    root_scope = next(iter(scopes))
+    for key in context:
+        p.from_context(provides=key, scope=root_scope)
+    return p
diff --git a/src/dishka/registry_builder.py b/src/dishka/registry_builder.py
index e0fe0f1..7f74c81 100644
--- a/src/dishka/registry_builder.py
+++ b/src/dishka/registry_builder.py
@@ -387,27 +387,26 @@ class RegistryBuilder:
                     "Define it explicitly in Provider or from_context"
                 ),
             )
-        for component in self.components:
-            factory = context_var.as_factory(component)
-            if (
-                self.validation_settings.nothing_overridden
-                and not self.skip_validation
-                and factory.override
-                and factory.provides not in self.processed_factories
-            ):
-                raise NothingOverriddenError(factory)
-
-            if (
-                self.validation_settings.implicit_override
-                and not self.skip_validation
-                and not factory.override
-                and factory.provides in self.processed_factories
-            ):
-                raise ImplicitOverrideDetectedError(
-                    factory,
-                    self.processed_factories[factory.provides],
-                )
-            self.processed_factories[factory.provides] = factory
+        factory = context_var.as_factory(provider.component)
+        if (
+            self.validation_settings.nothing_overridden
+            and not self.skip_validation
+            and factory.override
+            and factory.provides not in self.processed_factories
+        ):
+            raise NothingOverriddenError(factory)
+
+        if (
+            self.validation_settings.implicit_override
+            and not self.skip_validation
+            and not factory.override
+            and factory.provides in self.processed_factories
+        ):
+            raise ImplicitOverrideDetectedError(
+                factory,
+                self.processed_factories[factory.provides],
+            )
+        self.processed_factories[factory.provides] = factory
 
     def build(self) -> tuple[Registry, ...]:
         self._collect_components()


Test patch:
diff --git a/tests/unit/container/test_context_vars.py b/tests/unit/container/test_context_vars.py
index f2002ee..bb51653 100644
--- a/tests/unit/container/test_context_vars.py
+++ b/tests/unit/container/test_context_vars.py
@@ -75,7 +75,7 @@ async def test_2components():
 
 
 @pytest.mark.asyncio
-async def test_2components_factory():
+async def test_2components_factory_auto_context():
     class MyProvider(Provider):
         scope = Scope.APP
         component = "XXX"
@@ -109,3 +109,28 @@ def test_decorate():
     p2.provide(lambda: 2, provides=int)
     with pytest.raises(InvalidGraphError):
         make_container(MyProvider(), p2)
+
+
+def test_automatic_context():
+    class MyProvider(Provider):
+        scope = Scope.APP
+
+        @provide
+        def ii(self, i: int) -> str:
+            return str(i)
+
+    c = make_container(MyProvider(), context={int: 1})
+    assert c.get(str) == "1"
+
+
+@pytest.mark.asyncio
+async def test_automatic_context_async():
+    class MyProvider(Provider):
+        scope = Scope.APP
+
+        @provide
+        def ii(self, i: int) -> str:
+            return str(i)
+
+    c = make_async_container(MyProvider(), context={int: 1})
+    assert await c.get(str) == "1"