Repository: reagento/dishka
Pull request #452: feat: allow to extend container context on calling injected func
Author: login: gurobokum
Created: 2025-04-28T12:58:31Z | Merged: 2025-05-10T11:37:08Z

Relates #450 

## Description
In this PR `wrap_injection` function accepts `provide_context` that allows to provide injected variables on injected function call
It could be useful for building integrations where there is no support for middleware and/or request hooks, as well as for more granular deeper scopes
it works only with `manage_scope=Tru`

## Notes (UPD)
1. I tried not create @ovlerload to minimize the code, but check it during runtime with the corresponding error message
2. [x] Shall I add documentation?
3. [x] There is a problem with function `wrap_injection` complexity - C901 and PLR0912 (needs to discuss)



Diff:
diff --git a/docs/integrations/adding_new.rst b/docs/integrations/adding_new.rst
index 50dc43b6b..aa11f455b 100644
--- a/docs/integrations/adding_new.rst
+++ b/docs/integrations/adding_new.rst
@@ -11,6 +11,8 @@ The main points are:
 2. Find a place to enter and exit request scope and how to pass the container to a handler. Usually, it is entered in a middleware and container is stored in some kind of request context. 
 
    Alternatively, you can use the ``wrap_injection`` function with ``manage_scope=True`` to automate entering and exiting the request scope without relying on middleware. When enabled, ``manage_scope`` ensures that the container passed to ``wrap_injection`` enters and exits the next scope.
+
+   When you use ``manage_scope=True``, you can pass ``provide_context`` function that allows to populate the container context with passed arguments. This is useful when you want to pass the function scoped arguments to other dependencies without relying on middleware.
 3. Configure a decorator. The main option here is to provide a way for retrieving container. Often, need to modify handler signature adding additional parameters. It is also available.
 4. Check if you can apply decorator automatically.
 
diff --git a/src/dishka/integrations/base.py b/src/dishka/integrations/base.py
index 31f35bd58..680d1cab5 100644
--- a/src/dishka/integrations/base.py
+++ b/src/dishka/integrations/base.py
@@ -35,12 +35,19 @@
 from dishka.entities.component import DEFAULT_COMPONENT
 from dishka.entities.depends_marker import FromDishka
 from dishka.entities.key import DependencyKey, _FromComponent
-from dishka.integrations.exceptions import InvalidInjectedFuncTypeError
+from dishka.integrations.exceptions import (
+    ImproperProvideContextUsageError,
+    InvalidInjectedFuncTypeError,
+)
 
 T = TypeVar("T")
 P = ParamSpec("P")
 DependencyParser: TypeAlias = Callable[[Parameter, Any], DependencyKey | None]
 ContainerGetter: TypeAlias = Callable[[tuple[Any, ...], dict[str, Any]], T]
+ProvideContext: TypeAlias = Callable[
+    [tuple[Any, ...], dict[str, Any]],
+    dict[Any, Any],
+]
 DependsClass: TypeAlias = cast(
     type | Sequence[type],
     FromDishka | _FromComponent,
@@ -52,6 +59,7 @@ def _get_auto_injected_async_gen_scoped(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, AsyncIterator[T]],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, AsyncIterator[T]]:
     async def auto_injected_generator(
         *args: P.args,
@@ -60,8 +68,11 @@ async def auto_injected_generator(
         for param in additional_params:
             kwargs.pop(param.name)
 
+        additional_context = (
+            {} if provide_context is None else provide_context(args, kwargs)
+        )
         container = container_getter(args, kwargs)
-        async with container() as container:
+        async with container(additional_context) as container:
             solved = {
                 name: await container.get(
                     dep.type_hint,
@@ -80,7 +91,11 @@ def _get_auto_injected_async_gen(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, AsyncIterator[T]],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, AsyncIterator[T]]:
+    if provide_context is not None:
+        raise ImproperProvideContextUsageError
+
     async def auto_injected_generator(
         *args: P.args,
         **kwargs: P.kwargs,
@@ -107,12 +122,17 @@ def _get_auto_injected_async_func_scoped(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, Awaitable[T]],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, Awaitable[T]]:
     async def auto_injected_func(*args: P.args, **kwargs: P.kwargs) -> T:
         container = container_getter(args, kwargs)
         for param in additional_params:
             kwargs.pop(param.name)
-        async with container() as container:
+
+        additional_context = (
+            {} if provide_context is None else provide_context(args, kwargs)
+        )
+        async with container(additional_context) as container:
             solved = {
                 name: await container.get(
                     dep.type_hint,
@@ -130,7 +150,11 @@ def _get_auto_injected_async_func(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, Awaitable[T]],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, Awaitable[T]]:
+    if provide_context is not None:
+        raise ImproperProvideContextUsageError
+
     async def auto_injected_func(*args: P.args, **kwargs: P.kwargs) -> T:
         container = container_getter(args, kwargs)
         for param in additional_params:
@@ -152,6 +176,7 @@ def _get_auto_injected_sync_gen_scoped(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, Iterator[T]],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, Iterator[T]]:
     def auto_injected_generator(
         *args: P.args,
@@ -160,8 +185,11 @@ def auto_injected_generator(
         for param in additional_params:
             kwargs.pop(param.name)
 
+        additional_context = (
+            {} if provide_context is None else provide_context(args, kwargs)
+        )
         container = container_getter(args, kwargs)
-        with container() as container:
+        with container(additional_context) as container:
             solved = {
                 name: container.get(dep.type_hint, component=dep.component)
                 for name, dep in dependencies.items()
@@ -176,7 +204,11 @@ def _get_auto_injected_sync_gen(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, Iterator[T]],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, Iterator[T]]:
+    if provide_context is not None:
+        raise ImproperProvideContextUsageError
+
     def auto_injected_generator(
         *args: P.args,
         **kwargs: P.kwargs,
@@ -198,13 +230,17 @@ def _get_auto_injected_sync_func_scoped(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, T],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, T]:
     def auto_injected_func(*args: P.args, **kwargs: P.kwargs) -> T:
         for param in additional_params:
             kwargs.pop(param.name)
 
+        additional_context = (
+            {} if provide_context is None else provide_context(args, kwargs)
+        )
         container = container_getter(args, kwargs)
-        with container() as container:
+        with container(additional_context) as container:
             solved = {
                 name: container.get(dep.type_hint, component=dep.component)
                 for name, dep in dependencies.items()
@@ -219,7 +255,11 @@ def _get_auto_injected_sync_func(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, T],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, T]:
+    if provide_context is not None:
+        raise ImproperProvideContextUsageError
+
     def auto_injected_func(*args: P.args, **kwargs: P.kwargs) -> T:
         container = container_getter(args, kwargs)
         for param in additional_params:
@@ -239,6 +279,7 @@ def _get_auto_injected_sync_container_async_gen_scoped(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, Iterator[T]],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, AsyncIterator[T]]:
     async def auto_injected_generator(
         *args: P.args,
@@ -247,8 +288,11 @@ async def auto_injected_generator(
         for param in additional_params:
             kwargs.pop(param.name)
 
+        additional_context = (
+            {} if provide_context is None else provide_context(args, kwargs)
+        )
         container = container_getter(args, kwargs)
-        with container() as container:
+        with container(additional_context) as container:
             solved = {
                 name: container.get(dep.type_hint, component=dep.component)
                 for name, dep in dependencies.items()
@@ -264,7 +308,11 @@ def _get_auto_injected_sync_container_async_gen(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, Iterator[T]],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, AsyncIterator[T]]:
+    if provide_context is not None:
+        raise ImproperProvideContextUsageError
+
     async def auto_injected_generator(
         *args: P.args,
         **kwargs: P.kwargs,
@@ -287,13 +335,17 @@ def _get_auto_injected_sync_container_async_func_scoped(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, T],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, T]:
     async def auto_injected_func(*args: P.args, **kwargs: P.kwargs) -> T:
         for param in additional_params:
             kwargs.pop(param.name)
 
+        additional_context = (
+            {} if provide_context is None else provide_context(args, kwargs)
+        )
         container = container_getter(args, kwargs)
-        with container() as container:
+        with container(additional_context) as container:
             solved = {
                 name: container.get(dep.type_hint, component=dep.component)
                 for name, dep in dependencies.items()
@@ -308,7 +360,11 @@ def _get_auto_injected_sync_container_async_func(
     additional_params: Sequence[Parameter],
     dependencies: dict[str, DependencyKey],
     func: Callable[P, T],
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, T]:
+    if provide_context is not None:
+        raise ImproperProvideContextUsageError
+
     async def auto_injected_func(*args: P.args, **kwargs: P.kwargs) -> T:
         container = container_getter(args, kwargs)
         for param in additional_params:
@@ -449,6 +505,7 @@ def wrap_injection(
     additional_params: Sequence[Parameter] = (),
     parse_dependency: DependencyParser = default_parse_dependency,
     manage_scope: bool = False,
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, T]: ...
 
 
@@ -462,6 +519,7 @@ def wrap_injection(
     additional_params: Sequence[Parameter] = (),
     parse_dependency: DependencyParser = default_parse_dependency,
     manage_scope: bool = False,
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, T]: ...
 
 
@@ -474,6 +532,7 @@ def wrap_injection(
     additional_params: Sequence[Parameter] = (),
     parse_dependency: DependencyParser = default_parse_dependency,
     manage_scope: bool = False,
+    provide_context: ProvideContext | None = None,
 ) -> Callable[P, T]:
     hints = get_type_hints(func, include_extras=True)
     func_signature = signature(func)
@@ -541,6 +600,7 @@ def wrap_injection(
 
     auto_injected_func = get_auto_injected_func(
         func=func,
+        provide_context=provide_context,
         dependencies=dependencies,
         additional_params=additional_params,
         container_getter=container_getter,
diff --git a/src/dishka/integrations/exceptions.py b/src/dishka/integrations/exceptions.py
index ef6551bf5..81dee079f 100644
--- a/src/dishka/integrations/exceptions.py
+++ b/src/dishka/integrations/exceptions.py
@@ -3,6 +3,9 @@
 
 class InvalidInjectedFuncTypeError(DishkaError):
     def __str__(self) -> str:
-        return (
-            "An async container cannot be used in a synchronous context."
-        )
+        return "An async container cannot be used in a synchronous context."
+
+
+class ImproperProvideContextUsageError(DishkaError):
+    def __str__(self) -> str:
+        return "provide_context can only be used with manage_scope=True."
diff --git a/tests/integrations/base/test_wrap_injection_provide_context.py b/tests/integrations/base/test_wrap_injection_provide_context.py
new file mode 100644
index 000000000..fd6cd7e95
--- /dev/null
+++ b/tests/integrations/base/test_wrap_injection_provide_context.py
@@ -0,0 +1,134 @@
+from collections.abc import AsyncIterator, Callable, Iterable, Iterator
+from inspect import isasyncgenfunction, isgeneratorfunction
+from typing import Any, cast
+
+import pytest
+
+from dishka.async_container import AsyncContainer
+from dishka.container import Container
+from dishka.entities.depends_marker import FromDishka
+from dishka.integrations.base import wrap_injection
+from dishka.integrations.exceptions import ImproperProvideContextUsageError
+from tests.integrations.common import ContextDep, UserDep
+
+context = ContextDep("context")
+user = UserDep(f"user_id.from_context({context})")
+
+
+def generate_str(
+    user: UserDep,
+    context: ContextDep,
+    prefix: int | None = None,
+) -> str:
+    s = f"{user}, context={context}"
+    if prefix is not None:
+        s = f"{prefix}. {s}"
+    return s
+
+
+def sync_func(
+    context: ContextDep,
+    user: FromDishka[UserDep],
+) -> str:
+    return generate_str(user, context)
+
+
+def sync_gen(
+    context: ContextDep,
+    data: Iterable[int],
+    user: FromDishka[UserDep],
+) -> Iterator[str]:
+    for i in data:
+        yield generate_str(user, context, i)
+
+
+async def async_func(
+    context: ContextDep,
+    user: FromDishka[UserDep],
+) -> str:
+    return generate_str(user, context)
+
+
+async def async_gen(
+    context: ContextDep,
+    data: Iterable[int],
+    user: FromDishka[UserDep],
+) -> AsyncIterator[str]:
+    for i in data:
+        yield generate_str(user, context, i)
+
+
+def provide_context(
+    args: tuple[Any, ...],
+    _: dict[str, Any],
+) -> dict[Any, Any]:
+    context = cast(ContextDep, args[0])
+    return {ContextDep: context}
+
+
+@pytest.mark.parametrize("func", [sync_func, sync_gen])
+def test_sync_provide_context(func: Callable, container: Container) -> None:
+    wrapped_func = wrap_injection(
+        func=func,
+        container_getter=lambda *_: container,
+        is_async=False,
+        manage_scope=True,
+        provide_context=provide_context,
+    )
+
+    if isgeneratorfunction(func):
+        result = list(wrapped_func(context, [1, 2]))
+        assert result == [generate_str(user, context, i) for i in [1, 2]]
+    else:
+        result = wrapped_func(context)
+        assert result == generate_str(user, context)
+
+
+@pytest.mark.parametrize("func", [sync_func, sync_gen])
+def test_invalid_provide_context(func: Callable, container: Container) -> None:
+    with pytest.raises(ImproperProvideContextUsageError):
+        wrap_injection(
+            func=func,
+            container_getter=lambda *_: container,
+            is_async=False,
+            manage_scope=False,
+            provide_context=provide_context,
+        )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("func", [async_func, async_gen])
+async def test_async_provide_context(
+    func: Callable,
+    async_container: AsyncContainer,
+) -> None:
+    wrapped_func = wrap_injection(
+        func=func,
+        container_getter=lambda *_: async_container,
+        is_async=True,
+        manage_scope=True,
+        provide_context=provide_context,
+    )
+
+    if isasyncgenfunction(func):
+        result = [item async for item in wrapped_func(context, [1, 2])]
+        assert result == [generate_str(user, context, i) for i in [1, 2]]
+    else:
+        result = await wrapped_func(context)
+        assert result == generate_str(user, context)
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("func", [async_func, async_gen])
+async def test_invalid_async_provide_context(
+    func: Callable,
+    async_container: AsyncContainer,
+) -> None:
+    with pytest.raises(ImproperProvideContextUsageError):
+        wrap_injection(
+            func=func,
+            container_getter=lambda *_: async_container,
+            is_async=True,
+            manage_scope=False,
+            provide_context=provide_context,
+        )
diff --git a/tests/integrations/common.py b/tests/integrations/common.py
index ee6b5fc85..aeb2ccb74 100644
--- a/tests/integrations/common.py
+++ b/tests/integrations/common.py
@@ -2,7 +2,11 @@
 from typing import NewType
 from unittest.mock import Mock
 
-from dishka import Provider, Scope, provide
+from dishka import Provider, Scope, from_context, provide
+from dishka.entities.depends_marker import FromDishka
+
+ContextDep = NewType("ContextDep", str)
+UserDep = NewType("UserDep", str)
 
 AppDep = NewType("AppDep", str)
 APP_DEP_VALUE = "APP"
@@ -17,11 +21,14 @@
 
 
 class AppProvider(Provider):
+    context = from_context(provides=ContextDep, scope=Scope.REQUEST)
+
     def __init__(self) -> None:
         super().__init__()
         self.app_released = Mock()
         self.request_released = Mock()
         self.websocket_released = Mock()
+        self.db_session_released = Mock()
         self.mock = Mock()
         self.app_mock = AppMock(Mock())
 
@@ -40,6 +47,11 @@ def websocket(self) -> Iterable[WebSocketDep]:
         yield WS_DEP_VALUE
         self.websocket_released()
 
+    @provide(scope=Scope.REQUEST)
+    def user(self, context: FromDishka[ContextDep]) -> Iterable[UserDep]:
+        yield f"user_id.from_context({context})"
+        self.db_session_released()
+
     @provide(scope=Scope.REQUEST)
     def get_mock(self) -> Mock:
         return self.mock


Reviews:
user: login: gurobokum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T13:02:41Z
user: login: gurobokum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T13:05:34Z
user: login: ApostolFet
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T13:32:06Z
user: login: gurobokum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T11:06:52Z
user: login: ApostolFet
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-30T13:32:38Z
user: login: Tishka17
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T14:32:07Z
user: login: Tishka17
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-05T14:32:33Z
user: login: gurobokum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T11:27:59Z
user: login: Tishka17
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T16:17:41Z
user: login: gurobokum
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T09:31:37Z
user: login: Tishka17
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T12:38:51Z

Review comments:
diff_hunk: @@ -539,12 +574,18 @@ def wrap_injection(
     )
     get_auto_injected_func = _GET_AUTO_INJECTED_FUNC_DICT[injected_func_type]
 
-    auto_injected_func = get_auto_injected_func(
-        func=func,
-        dependencies=dependencies,
-        additional_params=additional_params,
-        container_getter=container_getter,
-    )
+    kwargs = {
+        "func": func,
+        "dependencies": dependencies,
+        "additional_params": additional_params,
+        "container_getter": container_getter,
+    }
+    if provide_context is not None:
path: src/dishka/integrations/base.py
user: login: gurobokum
body: It leads that function complexity increases and ruff check throws errors - C901 and PLR0912
There was an option to pass `provide_context ` to `get_auto_injected_func` and extent all `_get_auto_injected..` functions with *args , **kwargs, but I wanted to warn user explicitly about how `provide_context` works
I guess the general refactoring of this function is needed, for example moving getting dependencies above to another function, but it's out of the PR scope, wdyt? 
created_at: 2025-04-28T13:02:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -539,12 +574,18 @@ def wrap_injection(
     )
     get_auto_injected_func = _GET_AUTO_INJECTED_FUNC_DICT[injected_func_type]
 
-    auto_injected_func = get_auto_injected_func(
-        func=func,
-        dependencies=dependencies,
-        additional_params=additional_params,
-        container_getter=container_getter,
-    )
+    kwargs = {
+        "func": func,
+        "dependencies": dependencies,
+        "additional_params": additional_params,
+        "container_getter": container_getter,
+    }
+    if provide_context is not None:
path: src/dishka/integrations/base.py
user: login: gurobokum
body: ```
src/dishka/integrations/base.py:502:5: C901 `wrap_injection` is too complex (12 > 10)
    |
502 | def wrap_injection(
    |     ^^^^^^^^^^^^^^ C901
503 |     *,
504 |     func: Callable[P, T],
    |

src/dishka/integrations/base.py:502:5: PLR0912 Too many branches (13 > 12)
    |
502 | def wrap_injection(
    |     ^^^^^^^^^^^^^^ PLR0912
503 |     *,
504 |     func: Callable[P, T],
```
created_at: 2025-04-28T13:05:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+from collections.abc import AsyncIterator, Callable, Iterable, Iterator
+from inspect import isasyncgenfunction, isgeneratorfunction
+
+import pytest
+
+from dishka.async_container import AsyncContainer
+from dishka.container import Container
+from dishka.entities.depends_marker import FromDishka
+from dishka.integrations.base import wrap_injection
+from dishka.integrations.exceptions import (
+    InvalidInjectedFuncProvideContextError,
+)
+from tests.integrations.common import ContextDep
+
+
+def sync_func(context: FromDishka[ContextDep]) -> str:
+    return context
+
+
+def sync_gen(
+    data: Iterable[int],
+    context: FromDishka[ContextDep],
+) -> Iterator[str]:
+    for i in data:
+        yield f"{i}. {context}"
+
+
+async def async_func(context: FromDishka[ContextDep]) -> str:
+    return context
+
+
+async def async_gen(
+    data: Iterable[int],
+    context: FromDishka[ContextDep],
+) -> AsyncIterator[str]:
+    for i in data:
+        yield f"{i}. {context}"
+
+
+@pytest.mark.parametrize("func", [sync_func, sync_gen])
+def test_sync_provide_context(func: Callable, container: Container) -> None:
+    wrapped_func = wrap_injection(
+        func=func,
+        container_getter=lambda *_: container,
+        is_async=False,
+        manage_scope=True,
+        provide_context=lambda *_: {ContextDep: "context"},
+    )
+
+    if isgeneratorfunction(func):
+        result = list(wrapped_func([1, 2]))
+        assert result == ["1. context", "2. context"]
+    else:
+        result = wrapped_func()
+        assert result == "context"
+
+    with pytest.raises(InvalidInjectedFuncProvideContextError):
+        wrap_injection(
+            func=func,
+            container_getter=lambda *_: container,
+            is_async=False,
+            manage_scope=False,
+            provide_context=lambda *_: {ContextDep: "context"},
+        )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("func", [async_func, async_gen])
+async def test_async_provide_context(
+    func: Callable,
+    async_container: AsyncContainer,
+) -> None:
+    wrapped_func = wrap_injection(
+        func=func,
+        container_getter=lambda *_: async_container,
+        is_async=True,
+        manage_scope=True,
+        provide_context=lambda *_: {ContextDep: "context"},
+    )
+
+    if isasyncgenfunction(func):
+        result = [item async for item in wrapped_func([1, 2])]
+        assert result == ["1. context", "2. context"]
+    else:
+        result = await wrapped_func()
+        assert result == "context"
+
+    with pytest.raises(InvalidInjectedFuncProvideContextError):
+        wrap_injection(
+            func=func,
+            container_getter=lambda *_: async_container,
+            is_async=True,
+            manage_scope=False,
+            provide_context=lambda *_: {ContextDep: "context"},
+        )
path: tests/integrations/base/test_wrap_injection_provide_context.py
user: login: ApostolFet
body: Maybe we should split this test into different ones (generator, function, error), make more tests, but as simple as possible.
created_at: 2025-04-28T13:31:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,95 @@
+from collections.abc import AsyncIterator, Callable, Iterable, Iterator
+from inspect import isasyncgenfunction, isgeneratorfunction
+
+import pytest
+
+from dishka.async_container import AsyncContainer
+from dishka.container import Container
+from dishka.entities.depends_marker import FromDishka
+from dishka.integrations.base import wrap_injection
+from dishka.integrations.exceptions import (
+    InvalidInjectedFuncProvideContextError,
+)
+from tests.integrations.common import ContextDep
+
+
+def sync_func(context: FromDishka[ContextDep]) -> str:
+    return context
+
+
+def sync_gen(
+    data: Iterable[int],
+    context: FromDishka[ContextDep],
+) -> Iterator[str]:
+    for i in data:
+        yield f"{i}. {context}"
+
+
+async def async_func(context: FromDishka[ContextDep]) -> str:
+    return context
+
+
+async def async_gen(
+    data: Iterable[int],
+    context: FromDishka[ContextDep],
+) -> AsyncIterator[str]:
+    for i in data:
+        yield f"{i}. {context}"
+
+
+@pytest.mark.parametrize("func", [sync_func, sync_gen])
+def test_sync_provide_context(func: Callable, container: Container) -> None:
+    wrapped_func = wrap_injection(
+        func=func,
+        container_getter=lambda *_: container,
+        is_async=False,
+        manage_scope=True,
+        provide_context=lambda *_: {ContextDep: "context"},
+    )
+
+    if isgeneratorfunction(func):
+        result = list(wrapped_func([1, 2]))
+        assert result == ["1. context", "2. context"]
+    else:
+        result = wrapped_func()
+        assert result == "context"
+
+    with pytest.raises(InvalidInjectedFuncProvideContextError):
+        wrap_injection(
+            func=func,
+            container_getter=lambda *_: container,
+            is_async=False,
+            manage_scope=False,
+            provide_context=lambda *_: {ContextDep: "context"},
+        )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("func", [async_func, async_gen])
+async def test_async_provide_context(
+    func: Callable,
+    async_container: AsyncContainer,
+) -> None:
+    wrapped_func = wrap_injection(
+        func=func,
+        container_getter=lambda *_: async_container,
+        is_async=True,
+        manage_scope=True,
+        provide_context=lambda *_: {ContextDep: "context"},
+    )
+
+    if isasyncgenfunction(func):
+        result = [item async for item in wrapped_func([1, 2])]
+        assert result == ["1. context", "2. context"]
+    else:
+        result = await wrapped_func()
+        assert result == "context"
+
+    with pytest.raises(InvalidInjectedFuncProvideContextError):
+        wrap_injection(
+            func=func,
+            container_getter=lambda *_: async_container,
+            is_async=True,
+            manage_scope=False,
+            provide_context=lambda *_: {ContextDep: "context"},
+        )
path: tests/integrations/base/test_wrap_injection_provide_context.py
user: login: gurobokum
body: Updated
What do you think regarding lint errors? 
created_at: 2025-04-30T11:06:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -539,12 +574,18 @@ def wrap_injection(
     )
     get_auto_injected_func = _GET_AUTO_INJECTED_FUNC_DICT[injected_func_type]
 
-    auto_injected_func = get_auto_injected_func(
-        func=func,
-        dependencies=dependencies,
-        additional_params=additional_params,
-        container_getter=container_getter,
-    )
+    kwargs = {
+        "func": func,
+        "dependencies": dependencies,
+        "additional_params": additional_params,
+        "container_getter": container_getter,
+    }
+    if provide_context is not None:
path: src/dishka/integrations/base.py
user: login: Tishka17
body: Let's have all functions with the same signature. We can raise Error inside correspoding functions 
created_at: 2025-05-05T14:32:07Z
author_association: MEMBER
diff_hunk: @@ -6,3 +6,9 @@ def __str__(self) -> str:
         return (
             "An async container cannot be used in a synchronous context."
         )
+
+class InvalidInjectedFuncProvideContextError(DishkaError):
path: src/dishka/integrations/exceptions.py
user: login: Tishka17
body: Can we have more obvious name? 
created_at: 2025-05-05T14:32:33Z
author_association: MEMBER
diff_hunk: @@ -6,3 +6,9 @@ def __str__(self) -> str:
         return (
             "An async container cannot be used in a synchronous context."
         )
+
+class InvalidInjectedFuncProvideContextError(DishkaError):
path: src/dishka/integrations/exceptions.py
user: login: gurobokum
body: I wanted to keep `InvalidInjectedFunc` prefix and `Error` suffix for the consistency
changed to `ImproperProvideContextUsageError`, maybe makes sense to remove `Improper`, cause it's an error and keep `ProvideContextUsageError` 
created_at: 2025-05-08T11:27:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -11,6 +11,29 @@ The main points are:
 2. Find a place to enter and exit request scope and how to pass the container to a handler. Usually, it is entered in a middleware and container is stored in some kind of request context. 
 
    Alternatively, you can use the ``wrap_injection`` function with ``manage_scope=True`` to automate entering and exiting the request scope without relying on middleware. When enabled, ``manage_scope`` ensures that the container passed to ``wrap_injection`` enters and exits the next scope.
+
+   When you use ``manage_scope=True``, you can pass ``provide_context`` function that allows to populate the container context with passed arguments. This is useful when you want to pass the function scoped arguments to other dependencies without relying on middleware.
+
+   .. code-block:: python
+
+       from dishka import wrap_injection, Provider, provide
+
+       ...
+       class AppProvider(Provider):
+           request = from_context(provides=Request, scope=Scope.REQUEST)
+
+          @provide(scope=Scope.REQUEST)
+          def request(self, request: FromDishka[Request]) -> Iterable[User]:
+              yield get_user_from_request(request)
+
+       ...
+       wrapped_func = wrap_injection(
+           func=func,
+           is_async=False,
+           manage_scope=True,
+           provide_context=lambda req: Request: {Request: req},
path: docs/integrations/adding_new.rst
user: login: Tishka17
body: is it?

```python
provide_context(args, kwargs)
```


created_at: 2025-05-08T16:17:41Z
author_association: MEMBER
diff_hunk: @@ -11,6 +11,29 @@ The main points are:
 2. Find a place to enter and exit request scope and how to pass the container to a handler. Usually, it is entered in a middleware and container is stored in some kind of request context. 
 
    Alternatively, you can use the ``wrap_injection`` function with ``manage_scope=True`` to automate entering and exiting the request scope without relying on middleware. When enabled, ``manage_scope`` ensures that the container passed to ``wrap_injection`` enters and exits the next scope.
+
+   When you use ``manage_scope=True``, you can pass ``provide_context`` function that allows to populate the container context with passed arguments. This is useful when you want to pass the function scoped arguments to other dependencies without relying on middleware.
+
+   .. code-block:: python
+
+       from dishka import wrap_injection, Provider, provide
+
+       ...
+       class AppProvider(Provider):
+           request = from_context(provides=Request, scope=Scope.REQUEST)
+
+          @provide(scope=Scope.REQUEST)
+          def request(self, request: FromDishka[Request]) -> Iterable[User]:
+              yield get_user_from_request(request)
+
+       ...
+       wrapped_func = wrap_injection(
+           func=func,
+           is_async=False,
+           manage_scope=True,
+           provide_context=lambda req: Request: {Request: req},
path: docs/integrations/adding_new.rst
user: login: gurobokum
body: As was discussed in tg, I've removed example section because it's not obvious
created_at: 2025-05-09T09:31:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,103 @@
+from collections.abc import AsyncIterator, Callable, Iterable, Iterator
+from inspect import isasyncgenfunction, isgeneratorfunction
+
+import pytest
+
+from dishka.async_container import AsyncContainer
+from dishka.container import Container
+from dishka.entities.depends_marker import FromDishka
+from dishka.integrations.base import wrap_injection
+from dishka.integrations.exceptions import ImproperProvideContextUsageError
+from tests.integrations.common import ContextDep
+
+
+def sync_func(context: FromDishka[ContextDep]) -> str:
+    return context
+
+
+def sync_gen(
+    data: Iterable[int],
+    context: FromDishka[ContextDep],
+) -> Iterator[str]:
+    for i in data:
+        yield f"{i}. {context}"
+
+
+async def async_func(context: FromDishka[ContextDep]) -> str:
+    return context
+
+
+async def async_gen(
+    data: Iterable[int],
+    context: FromDishka[ContextDep],
+) -> AsyncIterator[str]:
+    for i in data:
+        yield f"{i}. {context}"
+
+
+@pytest.mark.parametrize("func", [sync_func, sync_gen])
+def test_sync_provide_context(func: Callable, container: Container) -> None:
+    wrapped_func = wrap_injection(
+        func=func,
+        container_getter=lambda *_: container,
+        is_async=False,
+        manage_scope=True,
+        provide_context=lambda *_: {ContextDep: "context"},
+    )
+
+    if isgeneratorfunction(func):
+        result = list(wrapped_func([1, 2]))
+        assert result == ["1. context", "2. context"]
+    else:
+        result = wrapped_func()
+        assert result == "context"
+
+
+@pytest.mark.parametrize("func", [sync_func, sync_gen])
+def test_invalid_provide_context(func: Callable, container: Container) -> None:
+    with pytest.raises(ImproperProvideContextUsageError):
+        wrap_injection(
+            func=func,
+            container_getter=lambda *_: container,
+            is_async=False,
+            manage_scope=False,
+            provide_context=lambda *_: {ContextDep: "context"},
+        )
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("func", [async_func, async_gen])
+async def test_async_provide_context(
+    func: Callable,
+    async_container: AsyncContainer,
+) -> None:
+    wrapped_func = wrap_injection(
+        func=func,
+        container_getter=lambda *_: async_container,
+        is_async=True,
+        manage_scope=True,
+        provide_context=lambda *_: {ContextDep: "context"},
+    )
+
+    if isasyncgenfunction(func):
+        result = [item async for item in wrapped_func([1, 2])]
+        assert result == ["1. context", "2. context"]
+    else:
+        result = await wrapped_func()
+        assert result == "context"
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("func", [async_func, async_gen])
+async def test_invalid_async_provide_context(
+    func: Callable,
+    async_container: AsyncContainer,
+) -> None:
+    with pytest.raises(ImproperProvideContextUsageError):
+        wrap_injection(
+            func=func,
+            container_getter=lambda *_: async_container,
+            is_async=True,
+            manage_scope=False,
+            provide_context=lambda *_: {ContextDep: "context"},
path: tests/integrations/base/test_wrap_injection_provide_context.py
user: login: Tishka17
body: can we have at least on test which actually uses params of `provide_context` function?
created_at: 2025-05-09T12:38:51Z
author_association: MEMBER

PR comments:
user: login: Tishka17
created_at: 2025-05-05T13:57:08Z
body: Docs?

https://dishka.readthedocs.io/en/stable/integrations/adding_new.html
author_association: MEMBER
user: login: gurobokum
created_at: 2025-05-08T11:26:43Z
body: I addressed your notes
at the same time I see docs building errors, I guess it doesn't relate to the commit

```python
Traceback (most recent call last):
  File "/home/docs/checkouts/readthedocs.org/user_builds/dishka/envs/452/lib/python3.12/site-packages/sphinx/cmd/build.py", line 514, in build_main
    app.build(args.force_all, args.filenames)
  File "/home/docs/checkouts/readthedocs.org/user_builds/dishka/envs/452/lib/python3.12/site-packages/sphinx/application.py", line 381, in build
    self.builder.build_update()
  File "/home/docs/checkouts/readthedocs.org/user_builds/dishka/envs/452/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 358, in build_update
    self.build(
  File "/home/docs/checkouts/readthedocs.org/user_builds/dishka/envs/452/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 437, in build
    self.write(docnames, list(updated_docnames), method)
  File "/home/docs/checkouts/readthedocs.org/user_builds/dishka/envs/452/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 706, in write
    self.prepare_writing(docnames)
  File "/home/docs/checkouts/readthedocs.org/user_builds/dishka/envs/452/lib/python3.12/site-packages/sphinx/builders/html/__init__.py", line 430, in prepare_writing
    self.indexer = IndexBuilder(
                   ^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/dishka/envs/452/lib/python3.12/site-packages/sphinx/search/__init__.py", line 303, in __init__
    self.lang = lang_class(options)
                ^^^^^^^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/dishka/envs/452/lib/python3.12/site-packages/sphinx/search/__init__.py", line 78, in __init__
    self.init(options)
  File "/home/docs/checkouts/readthedocs.org/user_builds/dishka/envs/452/lib/python3.12/site-packages/sphinx/search/en.py", line 217, in init
    self.stemmer = snowballstemmer.stemmer('porter')
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/docs/checkouts/readthedocs.org/user_builds/dishka/envs/452/lib/python3.12/site-packages/snowballstemmer/__init__.py", line 27, in stemmer
    raise KeyError("Stemming algorithm '%s' not found" % lang)
KeyError: "Stemming algorithm 'porter' not found"
```
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-08T16:18:27Z
body: ## Coverage report


<img title="Coverage for the whole project is 92.96%. Previous coverage rate is not available, cannot report on evolution." src="https://img.shields.io/badge/Coverage-92%25-brightgreen.svg"> <img title="66.66% of the statement lines added by this PR are covered" src="https://img.shields.io/badge/PR%20Coverage-66%25-red.svg"><details><summary>Click to see where and how coverage changed</summary><table><thead>
  <tr><th>File</th><th>Statements</th><th>Missing</th><th>Coverage</th><th>Coverage<br>(new stmts)</th><th>Lines missing</th></tr>
</thead>
<tbody><tr>
<td colspan="6">&nbsp;&nbsp;<b>src/dishka/integrations</b></td><tr>
<td>&nbsp;&nbsp;<a href="https://github.com/reagento/dishka/pull/452/files#diff-4a83bd93ec1a2199fadb8ce8239d0ef5ad3c5a873849d9dc127d31b64395556c">base.py</a></td>
<td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-4a83bd93ec1a2199fadb8ce8239d0ef5ad3c5a873849d9dc127d31b64395556c"><img title="This PR adds 245 statements to src/dishka/integrations/base.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/245-%28%2B245%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-4a83bd93ec1a2199fadb8ce8239d0ef5ad3c5a873849d9dc127d31b64395556c"><img title="This PR adds 49 statements missing coverage to src/dishka/integrations/base.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/49-%28%2B49%29-red.svg"></a></td><td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-4a83bd93ec1a2199fadb8ce8239d0ef5ad3c5a873849d9dc127d31b64395556c"><img title="The coverage rate of src/dishka/integrations/base.py is 80% (196/245). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/80%25-%28196/245%29-orange.svg"></a></td><td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-4a83bd93ec1a2199fadb8ce8239d0ef5ad3c5a873849d9dc127d31b64395556c"><img title="In this PR, 26 new statements are added to src/dishka/integrations/base.py, 18 of which are covered (69.23%)." src="https://img.shields.io/badge/69%25-%2818/26%29-red.svg"></a></td><td><a href="https://github.com/reagento/dishka/pull/452/files#diff-4a83bd93ec1a2199fadb8ce8239d0ef5ad3c5a873849d9dc127d31b64395556cR291-R295">291-295</a>, <a href="https://github.com/reagento/dishka/pull/452/files#diff-4a83bd93ec1a2199fadb8ce8239d0ef5ad3c5a873849d9dc127d31b64395556cR313-R314">313-314</a>, <a href="https://github.com/reagento/dishka/pull/452/files#diff-4a83bd93ec1a2199fadb8ce8239d0ef5ad3c5a873849d9dc127d31b64395556cR344-R348">344-348</a>, <a href="https://github.com/reagento/dishka/pull/452/files#diff-4a83bd93ec1a2199fadb8ce8239d0ef5ad3c5a873849d9dc127d31b64395556cR365-R366">365-366</a></td><tr>
<td>&nbsp;&nbsp;<a href="https://github.com/reagento/dishka/pull/452/files#diff-62c5ced0dbeadd7fb6b72fcadba3de6aca99023155860b08303fa572641dac19">exceptions.py</a></td>
<td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-62c5ced0dbeadd7fb6b72fcadba3de6aca99023155860b08303fa572641dac19"><img title="This PR adds 7 statements to src/dishka/integrations/exceptions.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/7-%28%2B7%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-62c5ced0dbeadd7fb6b72fcadba3de6aca99023155860b08303fa572641dac19"><img title="This PR adds 2 statements missing coverage to src/dishka/integrations/exceptions.py. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/2-%28%2B2%29-red.svg"></a></td><td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-62c5ced0dbeadd7fb6b72fcadba3de6aca99023155860b08303fa572641dac19"><img title="The coverage rate of src/dishka/integrations/exceptions.py is 71.42% (5/7). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/71%25-%285/7%29-orange.svg"></a></td><td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-62c5ced0dbeadd7fb6b72fcadba3de6aca99023155860b08303fa572641dac19"><img title="In this PR, 4 new statements are added to src/dishka/integrations/exceptions.py, 2 of which are covered (50%)." src="https://img.shields.io/badge/50%25-%282/4%29-red.svg"></a></td><td><a href="https://github.com/reagento/dishka/pull/452/files#diff-62c5ced0dbeadd7fb6b72fcadba3de6aca99023155860b08303fa572641dac19R6-R6">6</a>, <a href="https://github.com/reagento/dishka/pull/452/files#diff-62c5ced0dbeadd7fb6b72fcadba3de6aca99023155860b08303fa572641dac19R11-R11">11</a></td></tbody>
<tfoot>
<tr>
<td><b>Project Total</b></td>
<td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 3253 statements to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/3.3k-%28%2B3253%29-007ec6.svg"></a></td><td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="This PR adds 229 statements missing coverage to the whole project. The file did not seem to exist on the base branch." src="https://img.shields.io/badge/229-%28%2B229%29-red.svg"></a></td><td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="The coverage rate of the whole project is 92.96% (3024/3253). The file did not seem to exist on the base branch." src="https://img.shields.io/badge/92%25-%283.0k/3.3k%29-brightgreen.svg"></a></td><td align="center"><a href="https://github.com/reagento/dishka/pull/452/files#diff-4b0bf2efa3367c0072ac2bf1e234e703dc46b47aaa4fe9d3b01737b1a15752b1"><img title="In this PR, 30 new statements are added to the whole project, 20 of which are covered (66.66%)." src="https://img.shields.io/badge/66%25-%2820/30%29-red.svg"></a></td><td>&nbsp;</td>
</tr>
</tfoot>
</table>

<sub>

This report was generated by [python-coverage-comment-action](https://github.com/py-cov-action/python-coverage-comment-action)

</sub>
</details>



<!-- This comment was produced by python-coverage-comment-action -->
author_association: NONE