Repository: reagento/dishka
Pull request #157 (2024-05-20 21:19:01)

v1.2


Patch:
diff --git a/.github/workflows/setup.yml b/.github/workflows/setup.yml
index 25312d0..16e517f 100644
--- a/.github/workflows/setup.yml
+++ b/.github/workflows/setup.yml
@@ -1,4 +1,4 @@
-# This workflow will install Python dependencies, run tests and lint with a single version of Python
+# This workflow will install Python dependencies, run tests and lint
 # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
 
 name: CI
diff --git a/README.md b/README.md
index 4b77b72..01dab05 100644
--- a/README.md
+++ b/README.md
@@ -44,7 +44,7 @@ from dishka import Provider
 provider = Provider()
 ```
 
-3. Register functions which provide dependencies. Do not forget to place correct typehints for parameters and result. We use `scope=Scope.APP` for dependencies which ar created only once in applicaiton lifetime, and `scope=Scope.REQUEST` for those which should be recreated for each processing request/event/etc.
+3. Register functions which provide dependencies. Do not forget to place correct typehints for parameters and result. We use `scope=Scope.APP` for dependencies which ar created only once in application lifetime, and `scope=Scope.REQUEST` for those which should be recreated for each processing request/event/etc.
 
 ```python
 from dishka import Provider, Scope
@@ -144,7 +144,7 @@ Provider itself is a class with some attributes and methods. Each of them is eit
 
 `@provide` can be used as a decorator for some method. This method will be called when corresponding dependency has to be created. Name of the method is not important: just check that it is different form other `Provider` attributes. Type hints do matter: they show what this method creates and what does it require. All method parameters are treated as other dependencies and created using container.
 
-If `provide` is used with some class then that class itself is treated as a factory (`__init__` is analyzed for parameters). But do not forget to assing that call to some attribute otherwise it will be ignored.
+If `provide` is used with some class then that class itself is treated as a factory (`__init__` is analyzed for parameters). But do not forget to assign that call to some attribute otherwise it will be ignored.
 
 **Component** - is an isolated group of providers within the same container identified by a string. When dependency is requested it is searched only within the same component as its dependant, unless it is declared explicitly.
 
@@ -252,13 +252,13 @@ with container(context={RequestClass: request_instance}) as request_container:
     pass
 ```
 
-* Having to many dependencies? Or maybe want to replace only part of them in tests keeping others? Create multiple `Provider` classes
+* Having too many dependencies? Or maybe want to replace only part of them in tests keeping others? Create multiple `Provider` classes
 
 ```python
 container = make_container(MyProvider(), OtherProvider())
 ```
 
-* Tired of providing `scope==` for each depedency? Set it inside your `Provider` class and all dependencies with no scope will use it.
+* Tired of providing `scope` for each dependency? Set it inside your `Provider` class and all dependencies with no scope will use it.
 
 ```python
 class MyProvider(Provider):
diff --git a/docs/integrations/index.rst b/docs/integrations/index.rst
index 810d8d3..c4eee1c 100644
--- a/docs/integrations/index.rst
+++ b/docs/integrations/index.rst
@@ -1,3 +1,5 @@
+.. include:: <isonum.txt>
+
 Using with frameworks
 *******************************
 
@@ -111,7 +113,7 @@ With some frameworks we provide an option to inject dependencies in handlers wit
 
     setup_dishka(container, app)
 
-* For **FasStream** (**0.5.0** version and higher) you need to provide ``auto_inject=True`` when calling ``setup_dishka``. It is important here to call it before registering any subscribers or router include:
+* For **FasStream** (**0.5.0** version and higher) you need to provide ``auto_inject=True`` when calling ``setup_dishka``. E.g:
 
 .. code-block:: python
 
@@ -159,9 +161,9 @@ These objects are passed to context:
 
 * aiohttp - ``aiohttp.web_request.Request``
 * Flask - ``flask.Request``
-* Fastapi - ``fastapi.Request``
+* Fastapi - ``fastapi.Request`` or ``fastapi.WebSocket`` if you are using web sockets
 * Litestar - ``litestar.Request``
-* Starlette - ``starlette.requests.Request``
+* Starlette - ``starlette.requests.Request`` or ``starlette.websockets.WebSocket`` if you are using web sockets
 * Aiogram - ``aiogram.types.TelegramObject``
 * pyTelegramBotAPI - actual type of event (like ``Message``) is used.
 * Arq - no objects
@@ -171,6 +173,53 @@ These objects are passed to context:
 
 To use such objects you need to declare them in your provider using :ref:`from-context` and then they will be available as factories params.
 
+Websocket support
+=============================
+
+Injection is working with webosckets in these frameworks:
+
+* FastAPI
+* Starlette
+* aiohttp
+
+For most cases we operate single events like HTTP-requests. In this case we operate only 2 scopes: ``APP`` and ``REQUEST``. Websockets are different: for one application you have multiple connections (one per client) and each connection delivers multiple messages. To support this we use additional scope: ``SESSION``:
+
+    ``APP`` |rarr| ``SESSION`` |rarr| ``REQUEST``
+
+In frameworks like FastAPI and Starlette your view function is called once per connection and then you retrieve messages in loop. So, ``inject`` decorator can be only used to retrieve SESSION-scoped objects. To achieve REQUEST-scope you can enter in manually:
+
+.. code-block:: python
+
+    @inject
+    async def get_with_request(
+        websocket: WebSocket,
+        a: FromDishka[A],  # object with Scope.SESSION
+        container: FromDishka[AsyncContainer],  # container for Scope.SESSION
+    ) -> None:
+        await websocket.accept()
+        while True:
+            data = await websocket.receive_text()
+            # enter the nested scope, which is Scope.REQUEST
+            async with container() as request_container:
+                b = await request_container.get(B)  # object with Scope.REQUEST
+
+This is how it works with aiohttp
+
+.. code-block:: python
+
+    @inject
+    async def get_with_request(
+        request: web.Request, 
+        a: FromDishka[A],  # object with Scope.SESSION
+        container: FromDishka[AsyncContainer],  # container for Scope.SESSION
+    ) -> web.WebsocketResponse:
+        websocket = web.WebsocketResponse()
+        await websocket.prepare(request)
+
+        async for message in weboscket:
+            # enter the nested scope, which is Scope.REQUEST
+            async with container() as request_container:
+                b = await request_container.get(B)  # object with Scope.REQUEST
 
 Adding integrations
 ===========================
diff --git a/pyproject.toml b/pyproject.toml
index 4e70a3d..9a06ec2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,7 +10,7 @@ where = ["src"]
 
 [project]
 name = "dishka"
-version = "1.1.1"
+version = "0.1"
 readme = "README.md"
 authors = [
     { name = "Andrey Tikhonov", email = "17@itishka.org" },
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 4a2edba..66f19a2 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,2 +1,2 @@
-ruff==0.3.*
-tox==4.12.*
+ruff==0.4.*
+tox==4.15.*
diff --git a/src/dishka/async_container.py b/src/dishka/async_container.py
index c832ba1..f1e1faf 100644
--- a/src/dishka/async_container.py
+++ b/src/dishka/async_container.py
@@ -1,11 +1,13 @@
+import warnings
 from asyncio import Lock
-from collections.abc import Callable
+from collections.abc import Callable, MutableMapping
 from typing import Any, Optional, TypeVar
 
 from dishka.entities.component import DEFAULT_COMPONENT, Component
 from dishka.entities.key import DependencyKey
 from dishka.entities.scope import BaseScope, Scope
 from .container_objects import Exit
+from .context_proxy import ContextProxy
 from .dependency_source import FactoryType
 from .exceptions import (
     ExitError,
@@ -19,8 +21,8 @@ T = TypeVar("T")
 
 class AsyncContainer:
     __slots__ = (
-        "registry", "child_registries", "context", "parent_container",
-        "lock", "_exits", "close_parent",
+        "registry", "child_registries", "parent_container",
+        "lock", "_exits", "close_parent", "_cache", "_context",
     )
 
     def __init__(
@@ -34,14 +36,13 @@ class AsyncContainer:
     ):
         self.registry = registry
         self.child_registries = child_registries
-        self.context = {DependencyKey(type(self), DEFAULT_COMPONENT): self}
+        self._context = {DependencyKey(type(self), DEFAULT_COMPONENT): self}
         if context:
             for key, value in context.items():
-                if isinstance(key, DependencyKey):
-                    self.context[key] = value
-                else:
-                    self.context[DependencyKey(key, DEFAULT_COMPONENT)] = value
-
+                if not isinstance(key, DependencyKey):
+                    key = DependencyKey(key, DEFAULT_COMPONENT)
+                self._context[key] = value
+        self._cache = {**self._context}
         self.parent_container = parent_container
         if lock_factory:
             self.lock = lock_factory()
@@ -50,6 +51,15 @@ class AsyncContainer:
         self._exits: list[Exit] = []
         self.close_parent = close_parent
 
+    @property
+    def context(self) -> MutableMapping[DependencyKey, Any]:
+        warnings.warn(
+            "`container.context` is deprecated",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return ContextProxy(cache=self._cache, context=self._context)
+
     def __call__(
             self,
             context: dict | None = None,
@@ -110,8 +120,8 @@ class AsyncContainer:
             return await self._get_unlocked(key)
 
     async def _get_unlocked(self, key: DependencyKey) -> Any:
-        if key in self.context:
-            return self.context[key]
+        if key in self._cache:
+            return self._cache[key]
         compiled = self.registry.get_compiled_async(key)
         if not compiled:
             if not self.parent_container:
@@ -120,8 +130,7 @@ class AsyncContainer:
                 key.type_hint, key.component,
             )
         try:
-            return await compiled(self._get_unlocked, self._exits,
-                                  self.context)
+            return await compiled(self._get_unlocked, self._exits, self._cache)
         except NoFactoryError as e:
             e.add_path(self.registry.get_factory(key))
             raise
@@ -140,6 +149,7 @@ class AsyncContainer:
                 pass
             except Exception as err:  # noqa: BLE001
                 errors.append(err)
+        self._cache = {**self._context}
         if self.close_parent:
             try:
                 await self.parent_container.close(exception)
diff --git a/src/dishka/container.py b/src/dishka/container.py
index 000c1fa..dad73bb 100644
--- a/src/dishka/container.py
+++ b/src/dishka/container.py
@@ -1,4 +1,5 @@
-from collections.abc import Callable
+import warnings
+from collections.abc import Callable, MutableMapping
 from threading import Lock
 from typing import Any, Optional, TypeVar
 
@@ -6,6 +7,7 @@ from dishka.entities.component import DEFAULT_COMPONENT, Component
 from dishka.entities.key import DependencyKey
 from dishka.entities.scope import BaseScope, Scope
 from .container_objects import Exit
+from .context_proxy import ContextProxy
 from .dependency_source import FactoryType
 from .exceptions import (
     ExitError,
@@ -19,8 +21,8 @@ T = TypeVar("T")
 
 class Container:
     __slots__ = (
-        "registry", "child_registries", "context", "parent_container",
-        "lock", "_exits", "close_parent",
+        "registry", "child_registries", "parent_container",
+        "lock", "_exits", "close_parent", "_cache", "_context",
     )
 
     def __init__(
@@ -34,13 +36,13 @@ class Container:
     ):
         self.registry = registry
         self.child_registries = child_registries
-        self.context = {DependencyKey(type(self), DEFAULT_COMPONENT): self}
+        self._context = {DependencyKey(type(self), DEFAULT_COMPONENT): self}
         if context:
             for key, value in context.items():
-                if isinstance(key, DependencyKey):
-                    self.context[key] = value
-                else:
-                    self.context[DependencyKey(key, DEFAULT_COMPONENT)] = value
+                if not isinstance(key, DependencyKey):
+                    key = DependencyKey(key, DEFAULT_COMPONENT)
+                self._context[key] = value
+        self._cache = {**self._context}
         self.parent_container = parent_container
         if lock_factory:
             self.lock = lock_factory()
@@ -49,6 +51,15 @@ class Container:
         self._exits: list[Exit] = []
         self.close_parent = close_parent
 
+    @property
+    def context(self) -> MutableMapping[DependencyKey, Any]:
+        warnings.warn(
+            "`container.context` is deprecated",
+            DeprecationWarning,
+            stacklevel=2,
+        )
+        return ContextProxy(cache=self._cache, context=self._context)
+
     def __call__(
             self,
             context: dict | None = None,
@@ -108,8 +119,8 @@ class Container:
             return self._get_unlocked(key)
 
     def _get_unlocked(self, key: DependencyKey) -> Any:
-        if key in self.context:
-            return self.context[key]
+        if key in self._cache:
+            return self._cache[key]
         compiled = self.registry.get_compiled(key)
         if not compiled:
             if not self.parent_container:
@@ -118,7 +129,7 @@ class Container:
                 key.type_hint, key.component,
             )
         try:
-            return compiled(self._get_unlocked, self._exits, self.context)
+            return compiled(self._get_unlocked, self._exits, self._cache)
         except NoFactoryError as e:
             e.add_path(self.registry.get_factory(key))
             raise
@@ -133,6 +144,7 @@ class Container:
                 pass
             except Exception as err:  # noqa: BLE001
                 errors.append(err)
+        self._cache = {**self._context}
         if self.close_parent:
             try:
                 self.parent_container.close(exception)
diff --git a/src/dishka/context_proxy.py b/src/dishka/context_proxy.py
new file mode 100644
index 0000000..3a73a9b
--- /dev/null
+++ b/src/dishka/context_proxy.py
@@ -0,0 +1,30 @@
+from collections.abc import Iterator, MutableMapping
+from typing import Any, NoReturn
+
+from .entities.key import DependencyKey
+
+
+class ContextProxy(MutableMapping[DependencyKey, Any]):
+    def __init__(
+            self,
+            context: dict[DependencyKey, Any],
+            cache: dict[DependencyKey, Any],
+    ) -> None:
+        self._cache = cache
+        self._context = context
+
+    def __setitem__(self, key: DependencyKey, value: Any) -> None:
+        self._cache[key] = value
+        self._context[key] = value
+
+    def __delitem__(self, __key) -> NoReturn:
+        raise RuntimeError("Cannot delete anything from context")
+
+    def __getitem__(self, key) -> Any:
+        return self._cache[key]
+
+    def __len__(self) -> int:
+        return len(self._cache)
+
+    def __iter__(self) -> Iterator[DependencyKey]:
+        return iter(self._cache)
diff --git a/src/dishka/dependency_source/alias.py b/src/dishka/dependency_source/alias.py
index 7660a5d..1b74bc1 100644
--- a/src/dishka/dependency_source/alias.py
+++ b/src/dishka/dependency_source/alias.py
@@ -32,6 +32,7 @@ class Alias:
             provides=self.provides.with_component(component),
             is_to_bind=False,
             dependencies=[self.source.with_component(component)],
+            kw_dependencies={},
             type_=FactoryType.ALIAS,
             cache=self.cache,
         )
diff --git a/src/dishka/dependency_source/context_var.py b/src/dishka/dependency_source/context_var.py
index 401b1a1..98b2d32 100644
--- a/src/dishka/dependency_source/context_var.py
+++ b/src/dishka/dependency_source/context_var.py
@@ -32,6 +32,7 @@ class ContextVariable:
                 provides=self.provides,
                 is_to_bind=False,
                 dependencies=[],
+                kw_dependencies={},
                 type_=FactoryType.CONTEXT,
                 cache=False,
             )
diff --git a/src/dishka/dependency_source/decorator.py b/src/dishka/dependency_source/decorator.py
index 5fd9a9c..7c01300 100644
--- a/src/dishka/dependency_source/decorator.py
+++ b/src/dishka/dependency_source/decorator.py
@@ -36,6 +36,14 @@ class Decorator:
                 ).with_component(component)
                 for dep in self.factory.dependencies
             ],
+            kw_dependencies={
+                name: (
+                    new_dependency
+                    if dep.type_hint == self.provides.type_hint
+                    else dep
+                ).with_component(component)
+                for name, dep in self.factory.kw_dependencies.items()
+            },
             type_=self.factory.type,
             cache=cache,
         )
diff --git a/src/dishka/dependency_source/factory.py b/src/dishka/dependency_source/factory.py
index 004455a..510f6df 100644
--- a/src/dishka/dependency_source/factory.py
+++ b/src/dishka/dependency_source/factory.py
@@ -1,10 +1,9 @@
 from collections.abc import (
+    Mapping,
     Sequence,
 )
 from enum import Enum
-from typing import (
-    Any,
-)
+from typing import Any
 
 from dishka.entities.component import Component
 from dishka.entities.key import DependencyKey
@@ -23,7 +22,8 @@ class FactoryType(Enum):
 
 class Factory:
     __slots__ = (
-        "dependencies", "source", "provides", "scope", "type",
+        "dependencies", "kw_dependencies",
+        "source", "provides", "scope", "type",
         "is_to_bind", "cache",
     )
 
@@ -31,6 +31,7 @@ class Factory:
             self,
             *,
             dependencies: Sequence[DependencyKey],
+            kw_dependencies: Mapping[str, DependencyKey],
             source: Any,
             provides: DependencyKey,
             scope: BaseScope | None,
@@ -39,6 +40,7 @@ class Factory:
             cache: bool,
     ):
         self.dependencies = dependencies
+        self.kw_dependencies = kw_dependencies
         self.source = source
         self.provides = provides
         self.scope = scope
@@ -58,6 +60,7 @@ class Factory:
             dependencies = self.dependencies[:]
         return Factory(
             dependencies=dependencies,
+            kw_dependencies=self.kw_dependencies,
             source=source,
             provides=self.provides,
             scope=scope,
@@ -71,6 +74,10 @@ class Factory:
             dependencies=[
                 d.with_component(component) for d in self.dependencies
             ],
+            kw_dependencies={
+                name: d.with_component(component)
+                for name, d in self.kw_dependencies.items()
+            },
             source=self.source,
             provides=self.provides.with_component(component),
             scope=self.scope,
diff --git a/src/dishka/dependency_source/make_factory.py b/src/dishka/dependency_source/make_factory.py
index f9db170..7b4e766 100644
--- a/src/dishka/dependency_source/make_factory.py
+++ b/src/dishka/dependency_source/make_factory.py
@@ -9,6 +9,7 @@ from collections.abc import (
     Sequence,
 )
 from inspect import (
+    Parameter,
     isasyncgenfunction,
     isbuiltin,
     isclass,
@@ -214,10 +215,17 @@ def _make_factory_by_class(
         ) from e
 
     hints.pop("return", _empty)
+    params = signature(init).parameters
+    kw_dependency_keys = {
+        name: hint_to_dependency_key(hints.pop(name))
+        for name, param in params.items()
+        if param.kind is Parameter.KEYWORD_ONLY
+    }
     dependencies = list(hints.values())
 
     return Factory(
         dependencies=hints_to_dependency_keys(dependencies),
+        kw_dependencies=kw_dependency_keys,
         type_=FactoryType.FACTORY,
         source=source,
         scope=scope,
@@ -267,7 +275,14 @@ def _make_factory_by_function(
             # if we will bind factory to provider instance
             hints = {self.name: Any, **hints}
     possible_dependency = hints.pop("return", _empty)
+
+    kw_dependency_keys = {
+        name: hint_to_dependency_key(hints.pop(name))
+        for name, param in params.items()
+        if param.kind is Parameter.KEYWORD_ONLY
+    }
     dependencies = list(hints.values())
+
     if not provides:
         if possible_dependency is _empty:
             name = getattr(source, "__qualname__", "") or str(source)
@@ -280,6 +295,7 @@ def _make_factory_by_function(
             raise TypeError(f"Failed to analyze `{name}`. \n" + str(e)) from e
     return Factory(
         dependencies=hints_to_dependency_keys(dependencies),
+        kw_dependencies=kw_dependency_keys,
         type_=factory_type,
         source=source,
         scope=scope,
@@ -316,8 +332,17 @@ def _make_factory_by_static_method(
             f"Or, create a separate factory with all types imported.",
             name=e.name,
         ) from e
+
     possible_dependency = hints.pop("return", _empty)
+
+    params = signature(source).parameters
+    kw_dependency_keys = {
+        name: hint_to_dependency_key(hints.pop(name))
+        for name, param in params.items()
+        if param.kind is Parameter.KEYWORD_ONLY
+    }
     dependencies = list(hints.values())
+
     if not provides:
         if possible_dependency is _empty:
             name = getattr(source, "__qualname__", "") or str(source)
@@ -330,6 +355,7 @@ def _make_factory_by_static_method(
             raise TypeError(f"Failed to analyze `{name}`. \n" + str(e)) from e
     return Factory(
         dependencies=hints_to_dependency_keys(dependencies),
+        kw_dependencies=kw_dependency_keys,
         type_=factory_type,
         source=source,
         scope=scope,
@@ -363,6 +389,7 @@ def _make_factory_by_other_callable(
         dependencies = factory.dependencies
     return Factory(
         dependencies=dependencies,
+        kw_dependencies=factory.kw_dependencies,
         type_=factory.type,
         source=source,
         scope=scope,
diff --git a/src/dishka/dependency_source/unpack_provides.py b/src/dishka/dependency_source/unpack_provides.py
index de27e40..e1a847e 100644
--- a/src/dishka/dependency_source/unpack_provides.py
+++ b/src/dishka/dependency_source/unpack_provides.py
@@ -24,6 +24,7 @@ def unpack_factory(factory: Factory) -> Sequence[DependencySource]:
     ]
     res.append(Factory(
         dependencies=factory.dependencies,
+        kw_dependencies=factory.kw_dependencies,
         type_=factory.type,
         source=factory.source,
         scope=factory.scope,
diff --git a/src/dishka/factory_compiler.py b/src/dishka/factory_compiler.py
index a7c3638..63b7f13 100644
--- a/src/dishka/factory_compiler.py
+++ b/src/dishka/factory_compiler.py
@@ -10,9 +10,10 @@ For each template we expect global variables:
 * UnsupportedFactoryError
 
 When formatting substituted:
-* await - "async " for async container or empty string
+* await - "await " for async container or empty string
 * async - "async " for async container or empty string
 * args - "getter(arg1), getter(arg2)..." or async version
+* kwargs - "arg1=getter(arg1), arg2=getter(arg2)..." or async version
 * cache - expression to save cache
 """
 
@@ -21,11 +22,20 @@ from .dependency_source import Factory, FactoryType
 from .exceptions import NoContextValueError, UnsupportedFactoryError
 
 
-def make_args(names: list[str]):
-    return ", ".join(
-        "{await} getter(%s)" % arg
-        for arg in names
+def make_args(args: list[str], kwargs: list[str]) -> str:
+    res = ", ".join(
+        f"{{await}}getter({arg})"
+        for arg in args
     )
+    if not kwargs:
+        return res
+    if res:
+        res += ", "
+    res += ", ".join(
+        f"{arg}={{await}}getter({arg})"
+        for arg in kwargs
+    )
+    return res
 
 
 GENERATOR = """
@@ -98,7 +108,12 @@ CACHE = "context[provides] = solved"
 
 
 def compile_factory(*, factory: Factory, is_async: bool) -> CompiledFactory:
-    names = {f"arg{i}": dep for i, dep in enumerate(factory.dependencies)}
+    args = {
+        f"_dishka_arg{i}": dep
+        for i, dep in enumerate(factory.dependencies)
+    }
+    kwargs = factory.kw_dependencies
+
     if is_async:
         async_ = "async "
         await_ = "await "
@@ -112,11 +127,13 @@ def compile_factory(*, factory: Factory, is_async: bool) -> CompiledFactory:
     else:
         cache = ""
 
-    args = make_args(list(names)).format_map({"await": await_})
+    args_str = make_args(list(args), list(kwargs)).format_map({
+        "await": await_,
+    })
     body = body_template.format_map({
         "async": async_,
         "await": await_,
-        "args": args,
+        "args": args_str,
         "cache": cache,
     })
     func_globals = {
@@ -126,7 +143,8 @@ def compile_factory(*, factory: Factory, is_async: bool) -> CompiledFactory:
         "Exit": Exit,
         "NoContextValueError": NoContextValueError,
         "UnsupportedFactoryError": UnsupportedFactoryError,
-        **names,
+        **args,
+        **kwargs,
     }
     exec(body, func_globals)  # noqa: S102
     return func_globals["get"]
diff --git a/src/dishka/integrations/aiohttp.py b/src/dishka/integrations/aiohttp.py
index 086ec6d..8a3e533 100644
--- a/src/dishka/integrations/aiohttp.py
+++ b/src/dishka/integrations/aiohttp.py
@@ -14,8 +14,8 @@ from aiohttp.web_app import Application
 from aiohttp.web_request import Request
 from aiohttp.web_response import StreamResponse
 
-from dishka import AsyncContainer, FromDishka
-from dishka.integrations.base import wrap_injection
+from dishka import AsyncContainer, FromDishka, Scope
+from dishka.integrations.base import is_dishka_injected, wrap_injection
 
 DISHKA_CONTAINER_KEY: Final = web.AppKey("dishka_container", AsyncContainer)
 
@@ -31,14 +31,54 @@ def inject(func: Callable) -> Callable:
 
 @web.middleware
 async def container_middleware(
-    request: Request, handler: Handler,
+    request: Request,
+    handler: Handler,
 ) -> StreamResponse:
     container = request.app[DISHKA_CONTAINER_KEY]
-    async with container(context={Request: request}) as request_container:
+
+    if (
+        request.headers.get("Upgrade") == "websocket"
+        and request.headers.get("Connection") == "Upgrade"
+    ):
+        scope = Scope.SESSION
+
+    else:
+        scope = Scope.REQUEST
+
+    context = {Request: request}
+
+    async with container(context=context, scope=scope) as request_container:
         request[DISHKA_CONTAINER_KEY] = request_container
         return await handler(request)
 
 
-def setup_dishka(container: AsyncContainer, app: Application) -> None:
+def _inject_routes(router: web.UrlDispatcher) -> None:
+    for route in router.routes():
+        _inject_route(route)
+
+    for resource in router.resources():
+        for route in resource._routes:  # noqa: SLF001
+            _inject_route(route)
+
+
+def _inject_route(route: web.AbstractRoute) -> None:
+    if not is_dishka_injected(route.handler):
+        route._handler = inject(route.handler)  # noqa: SLF001
+
+
+async def _on_shutdown(app: web.Application) -> None:
+    await app[DISHKA_CONTAINER_KEY].close()
+
+
+def setup_dishka(
+    container: AsyncContainer,
+    app: Application,
+    *,
+    auto_inject: bool = False,
+) -> None:
     app[DISHKA_CONTAINER_KEY] = container
     app.middlewares.append(container_middleware)
+    app.on_shutdown.append(_on_shutdown)
+
+    if auto_inject:
+        _inject_routes(app.router)
diff --git a/src/dishka/integrations/fastapi.py b/src/dishka/integrations/fastapi.py
index 5014d19..7bc40f2 100644
--- a/src/dishka/integrations/fastapi.py
+++ b/src/dishka/integrations/fastapi.py
@@ -9,7 +9,7 @@ from collections.abc import Callable
 from inspect import Parameter
 from typing import Any, get_type_hints
 
-from fastapi import FastAPI, Request
+from fastapi import FastAPI, Request, WebSocket
 from fastapi.routing import APIRoute
 
 from dishka import AsyncContainer, FromDishka
@@ -17,26 +17,31 @@ from .base import wrap_injection
 from .starlette import ContainerMiddleware
 
 
-def inject(func):
+def inject(func: Callable) -> Callable:
     hints = get_type_hints(func)
-    request_param = next(
+    request_hint = next(
         (name for name, hint in hints.items() if hint is Request),
         None,
     )
-    if request_param:
-        additional_params = []
+    websocket_hint = next(
+        (name for name, hint in hints.items() if hint is WebSocket),
+        None,
+    )
+    if request_hint is None and websocket_hint is None:
+        additional_params = [
+            Parameter(
+                name="___dishka_request",
+                annotation=Request,
+                kind=Parameter.KEYWORD_ONLY,
+            ),
+        ]
     else:
-        request_param = "____dishka_request"
-        additional_params = [Parameter(
-            name=request_param,
-            annotation=Request,
-            kind=Parameter.KEYWORD_ONLY,
-        )]
-
+        additional_params = []
+    param_name = request_hint or websocket_hint or "___dishka_request"
     return wrap_injection(
         func=func,
         remove_depends=True,
-        container_getter=lambda _, p: p[request_param].state.dishka_container,
+        container_getter=lambda _, p: p[param_name].state.dishka_container,
         additional_params=additional_params,
         is_async=True,
     )
@@ -44,7 +49,10 @@ def inject(func):
 
 class DishkaRoute(APIRoute):
     def __init__(
-            self, path: str, endpoint: Callable[..., Any], **kwargs,
+        self,
+        path: str,
+        endpoint: Callable[..., Any],
+        **kwargs,
     ):
         endpoint = inject(endpoint)
         super().__init__(path, endpoint, **kwargs)
diff --git a/src/dishka/integrations/faststream.py b/src/dishka/integrations/faststream.py
index 02cfd84..0a8a313 100644
--- a/src/dishka/integrations/faststream.py
+++ b/src/dishka/integrations/faststream.py
@@ -60,7 +60,7 @@ else:
         async def consume_scope(
                 self,
                 call_next: Callable[[Any], Awaitable[Any]],
-                msg: StreamMessage[any],
+                msg: StreamMessage[Any],
         ) -> AsyncIterator[DecodedMessage]:
             async with self.container(
                 {
@@ -108,6 +108,18 @@ or use @inject at each subscriber manually.
             *app.broker._middlewares,  # noqa: SLF001
         )
 
+        for subscriber in app.broker._subscribers.values():  # noqa: SLF001
+            subscriber._broker_middlewares = (  # noqa: SLF001
+                DishkaMiddleware(container),
+                *subscriber._broker_middlewares,  # noqa: SLF001
+            )
+
+        for publisher in app.broker._publishers.values():  # noqa: SLF001
+            publisher._broker_middlewares = (  # noqa: SLF001
+                DishkaMiddleware(container),
+                *publisher._broker_middlewares,  # noqa: SLF001
+            )
+
         if auto_inject:
             app.broker._call_decorators = (  # noqa: SLF001
                 inject,
diff --git a/src/dishka/integrations/starlette.py b/src/dishka/integrations/starlette.py
index c75dd7b..43cfb8c 100644
--- a/src/dishka/integrations/starlette.py
+++ b/src/dishka/integrations/starlette.py
@@ -7,8 +7,10 @@ __all__ = [
 from starlette.applications import Starlette
 from starlette.requests import Request
 from starlette.types import ASGIApp, Receive, Scope, Send
+from starlette.websockets import WebSocket
 
 from dishka import AsyncContainer, FromDishka
+from dishka import Scope as DIScope
 from .base import wrap_injection
 
 
@@ -29,12 +31,21 @@ class ContainerMiddleware:
     async def __call__(
             self, scope: Scope, receive: Receive, send: Send,
     ) -> None:
-        if scope["type"] != "http":
+        if scope["type"] not in ("http", "websocket"):
             return await self.app(scope, receive, send)
 
-        request = Request(scope)
+        if scope["type"] == "http":
+            request = Request(scope)
+            context = {Request: request}
+            di_scope = DIScope.REQUEST
+
+        else:
+            request = WebSocket(scope, receive, send)
+            context = {WebSocket: request}
+            di_scope = DIScope.SESSION
+
         async with request.app.state.dishka_container(
-                {Request: request},
+            context, scope=di_scope,
         ) as request_container:
             request.state.dishka_container = request_container
             return await self.app(scope, receive, send)
diff --git a/src/dishka/registry.py b/src/dishka/registry.py
index f273943..4e3b09b 100644
--- a/src/dishka/registry.py
+++ b/src/dishka/registry.py
@@ -111,10 +111,24 @@ class Registry:
             new_dependencies.append(DependencyKey(
                 hint, source_dependency.component,
             ))
+        new_kw_dependencies: dict[str, DependencyKey] = {}
+        for name, source_dependency in factory.kw_dependencies.items():
+            hint = source_dependency.type_hint
+            if isinstance(hint, TypeVar):
+                hint = params_replacement[hint]
+            elif get_origin(hint):
+                hint = hint[tuple(
+                    params_replacement[param]
+                    for param in get_type_vars(hint)
+                )]
+            new_kw_dependencies[name] = DependencyKey(
+                hint, source_dependency.component,
+            )
         return Factory(
             source=factory.source,
             provides=dependency_key,
             dependencies=new_dependencies,
+            kw_dependencies=new_kw_dependencies,
             is_to_bind=factory.is_to_bind,
             type_=factory.type,
             scope=factory.scope,
@@ -154,6 +168,11 @@ class GraphValidator:
                 # ignore TypeVar parameters
                 if not isinstance(dep.type_hint, TypeVar):
                     self._validate_key(dep, registry_index)
+            for dep in factory.kw_dependencies.values():
+                # ignore TypeVar parameters
+                if not isinstance(dep.type_hint, TypeVar):
+                    self._validate_key(dep, registry_index)
+
         except NoFactoryError as e:
             e.add_path(factory)
             raise
diff --git a/tox.ini b/tox.ini
index 7168c20..a96f3f3 100644
--- a/tox.ini
+++ b/tox.ini
@@ -62,14 +62,14 @@ commands =
 
 package = editable
 
+install_command =
+    python -m pip install
+    latest: -U
 
 [testenv:unit]
 deps = -r requirements/test.txt
 commands = pytest tests/unit
 
-[testenv:latest]
-install_commands = python -m pip install -U {opts} {packages}
-
 [testenv:real_world_example]
 deps = -r examples/real_world/requirements_test.txt
 commands = pytest examples/real_world/tests/
\ No newline at end of file


Test patch:
diff --git a/.github/workflows/frameworks-latest.yml b/.github/workflows/frameworks-latest.yml
new file mode 100644
index 0000000..ea5e350
--- /dev/null
+++ b/.github/workflows/frameworks-latest.yml
@@ -0,0 +1,37 @@
+# This workflow will install Python dependencies, run tests
+# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
+
+name: TestLatest
+
+on:
+  schedule:
+    - cron: "0 0 * * *"
+
+jobs:
+  cpython:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os:
+          - ubuntu-latest
+        python-version:
+          - "3.10"
+          - "3.11"
+          - "3.12"
+
+    steps:
+      - uses: actions/checkout@v4
+      - name: Set up ${{ matrix.python-version }} on ${{ matrix.os }}
+        uses: actions/setup-python@v5
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install . -r requirements_dev.txt
+
+      - name: Run tests
+        run: |
+          tox -e aiohttp-latest,fastapi-latest,aiogram-latest,telebot-latest,flask-latest,litestar-latest,starlette-latest,faststream-latest,arq-latest,taskiq-latest,sanic-latest
+
diff --git a/requirements/test.txt b/requirements/test.txt
index c8dc278..68b6d03 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -1,4 +1,4 @@
-pytest==7.*
+pytest==8.*
 pytest-asyncio==0.23.*
 pytest-repeat==0.9.*
-pytest-cov==4.1.0
\ No newline at end of file
+pytest-cov==5.0.*
\ No newline at end of file
diff --git a/tests/integrations/aiohttp/test_aiohttp.py b/tests/integrations/aiohttp/test_aiohttp.py
index 19f982b..d3b7a6a 100644
--- a/tests/integrations/aiohttp/test_aiohttp.py
+++ b/tests/integrations/aiohttp/test_aiohttp.py
@@ -1,3 +1,4 @@
+from collections.abc import AsyncIterable
 from contextlib import asynccontextmanager
 from unittest.mock import Mock
 
@@ -23,7 +24,7 @@ from ..common import (
 
 
 @asynccontextmanager
-async def dishka_app(view, provider) -> TestClient:
+async def dishka_app(view, provider) -> AsyncIterable[TestClient]:
     app = Application()
 
     router = RouteTableDef()
@@ -39,6 +40,21 @@ async def dishka_app(view, provider) -> TestClient:
     await container.close()
 
 
+@asynccontextmanager
+async def dishka_auto_app(view, provider) -> AsyncIterable[TestClient]:
+    app = Application()
+
+    app.router.add_get("/", view)
+
+    container = make_async_container(provider)
+    setup_dishka(container, app=app, auto_inject=True)
+    client = TestClient(TestServer(app))
+    await client.start_server()
+    yield client
+    await client.close()
+    await container.close()
+
+
 async def get_with_app(
         _,
         a: FromDishka[AppDep],
@@ -48,9 +64,12 @@ async def get_with_app(
     return Response(text="passed")
 
 
+@pytest.mark.parametrize("app_factory", [
+    dishka_app, dishka_auto_app,
+])
 @pytest.mark.asyncio
-async def test_app_dependency(app_provider: AppProvider):
-    async with dishka_app(get_with_app, app_provider) as client:
+async def test_app_dependency(app_provider: AppProvider, app_factory):
+    async with app_factory(get_with_app, app_provider) as client:
         await client.get("/")
         app_provider.mock.assert_called_with(APP_DEP_VALUE)
         app_provider.app_released.assert_not_called()
@@ -66,18 +85,23 @@ async def get_with_request(
     return Response(text="passed")
 
 
-
+@pytest.mark.parametrize("app_factory", [
+    dishka_app, dishka_auto_app,
+])
 @pytest.mark.asyncio
-async def test_request_dependency(app_provider: AppProvider):
-    async with dishka_app(get_with_request, app_provider) as client:
+async def test_request_dependency(app_provider: AppProvider, app_factory):
+    async with app_factory(get_with_request, app_provider) as client:
         await client.get("/")
         app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
         app_provider.request_released.assert_called_once()
 
 
+@pytest.mark.parametrize("app_factory", [
+    dishka_app, dishka_auto_app,
+])
 @pytest.mark.asyncio
-async def test_request_dependency2(app_provider: AppProvider):
-    async with dishka_app(get_with_request, app_provider) as client:
+async def test_request_dependency2(app_provider: AppProvider, app_factory):
+    async with app_factory(get_with_request, app_provider) as client:
         await client.get("/")
         app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
         app_provider.mock.reset_mock()
diff --git a/tests/integrations/aiohttp/test_aiohttp_websockets.py b/tests/integrations/aiohttp/test_aiohttp_websockets.py
new file mode 100644
index 0000000..4fd6dda
--- /dev/null
+++ b/tests/integrations/aiohttp/test_aiohttp_websockets.py
@@ -0,0 +1,179 @@
+from collections.abc import AsyncGenerator
+from contextlib import asynccontextmanager
+from unittest.mock import Mock
+
+import pytest
+from aiohttp.test_utils import TestClient, TestServer
+from aiohttp.web import WebSocketResponse
+from aiohttp.web_app import Application
+from aiohttp.web_request import Request
+from aiohttp.web_routedef import RouteTableDef
+
+from dishka import make_async_container
+from dishka.integrations.aiohttp import (
+    FromDishka,
+    inject,
+    setup_dishka,
+)
+from ..common import (
+    APP_DEP_VALUE,
+    REQUEST_DEP_VALUE,
+    WS_DEP_VALUE,
+    AppDep,
+    AppProvider,
+    RequestDep,
+    WebSocketDep,
+)
+
+
+@asynccontextmanager
+async def dishka_app(view, provider) -> AsyncGenerator[TestClient, None]:
+    app = Application()
+
+    router = RouteTableDef()
+    router.get("/")(inject(view))
+
+    app.add_routes(router)
+    container = make_async_container(provider)
+    setup_dishka(container, app=app)
+    client = TestClient(TestServer(app))
+    await client.start_server()
+    yield client
+    await client.close()
+    await container.close()
+
+
+@asynccontextmanager
+async def dishka_auto_app(view, provider) -> AsyncGenerator[TestClient, None]:
+    app = Application()
+
+    app.router.add_get("/", view)
+
+    container = make_async_container(provider)
+    setup_dishka(container, app=app, auto_inject=True)
+    client = TestClient(TestServer(app))
+    await client.start_server()
+    yield client
+    await client.close()
+    await container.close()
+
+
+async def get_with_app(
+    request: Request,
+    a: FromDishka[AppDep],
+    mock: FromDishka[Mock],
+) -> WebSocketResponse:
+    websocket = WebSocketResponse()
+    await websocket.prepare(request)
+
+    await websocket.receive()
+    mock(a)
+    await websocket.send_str("passed")
+
+
+@pytest.mark.parametrize(
+    "app_factory",
+    [
+        dishka_app,
+        dishka_auto_app,
+    ],
+)
+@pytest.mark.asyncio
+async def test_app_dependency(
+    ws_app_provider: AppProvider,
+    app_factory,
+):
+    async with app_factory(get_with_app, ws_app_provider) as client:
+        async with client.ws_connect("/") as conn:
+            await conn.send_str("...")
+            assert await conn.receive_str() == "passed"
+        ws_app_provider.mock.assert_called_with(APP_DEP_VALUE)
+        ws_app_provider.app_released.assert_not_called()
+    ws_app_provider.app_released.assert_called()
+
+
+async def get_with_request(
+    request: Request,
+    a: FromDishka[RequestDep],
+    mock: FromDishka[Mock],
+) -> WebSocketResponse:
+    websocket = WebSocketResponse()
+    await websocket.prepare(request)
+
+    await websocket.receive()
+    mock(a)
+    await websocket.send_str("passed")
+
+
+@pytest.mark.parametrize(
+    "app_factory",
+    [
+        dishka_app,
+        dishka_auto_app,
+    ],
+)
+@pytest.mark.asyncio
+async def test_request_dependency(ws_app_provider: AppProvider, app_factory):
+    async with app_factory(get_with_request, ws_app_provider) as client:
+        async with client.ws_connect("/") as conn:
+            await conn.send_str("...")
+            assert await conn.receive_str() == "passed"
+
+        ws_app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        ws_app_provider.request_released.assert_called_once()
+
+
+@pytest.mark.parametrize(
+    "app_factory",
+    [
+        dishka_app,
+        dishka_auto_app,
+    ],
+)
+@pytest.mark.asyncio
+async def test_request_dependency2(ws_app_provider: AppProvider, app_factory):
+    async with app_factory(get_with_request, ws_app_provider) as client:
+        async with client.ws_connect("/") as conn:
+            await conn.send_str("...")
+            assert await conn.receive_str() == "passed"
+
+        ws_app_provider.request_released.assert_called_once()
+        ws_app_provider.request_released.reset_mock()
+
+        async with client.ws_connect("/") as conn:
+            await conn.send_str("...")
+            assert await conn.receive_str() == "passed"
+
+        ws_app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        ws_app_provider.request_released.assert_called_once()
+
+
+async def get_with_websocket(
+    request: Request,
+    ws: FromDishka[WebSocketDep],
+    mock: FromDishka[Mock],
+) -> None:
+    websocket = WebSocketResponse()
+    await websocket.prepare(request)
+
+    await websocket.receive()
+    mock(ws)
+    await websocket.send_str("passed")
+
+
+@pytest.mark.parametrize(
+    "app_factory",
+    [
+        dishka_app,
+        dishka_auto_app,
+    ],
+)
+@pytest.mark.asyncio
+async def test_websocket_dependency(ws_app_provider: AppProvider, app_factory):
+    async with app_factory(get_with_websocket, ws_app_provider) as client:
+        async with client.ws_connect("/") as conn:
+            await conn.send_str("...")
+            assert await conn.receive_str() == "passed"
+
+        ws_app_provider.mock.assert_called_with(WS_DEP_VALUE)
+        ws_app_provider.websocket_released.assert_called_once()
diff --git a/tests/integrations/common.py b/tests/integrations/common.py
index 6d846ad..f6fa344 100644
--- a/tests/integrations/common.py
+++ b/tests/integrations/common.py
@@ -10,12 +10,16 @@ APP_DEP_VALUE = "APP"
 RequestDep = NewType("RequestDep", str)
 REQUEST_DEP_VALUE = "REQUEST"
 
+WebSocketDep = NewType("WebSocketDep", str)
+WS_DEP_VALUE = "WS"
+
 
 class AppProvider(Provider):
     def __init__(self):
         super().__init__()
         self.app_released = Mock()
         self.request_released = Mock()
+        self.websocket_released = Mock()
         self.mock = Mock()
 
     @provide(scope=Scope.APP)
@@ -29,5 +33,38 @@ class AppProvider(Provider):
         self.request_released()
 
     @provide(scope=Scope.REQUEST)
+    def websocket(self) -> Iterable[WebSocketDep]:
+        yield WS_DEP_VALUE
+        self.websocket_released()
+
+    @provide(scope=Scope.REQUEST)
+    def mock(self) -> Mock:
+        return self.mock
+
+
+class WebSocketAppProvider(Provider):
+    def __init__(self):
+        super().__init__()
+        self.app_released = Mock()
+        self.request_released = Mock()
+        self.websocket_released = Mock()
+        self.mock = Mock()
+
+    @provide(scope=Scope.APP)
+    def app(self) -> Iterable[AppDep]:
+        yield APP_DEP_VALUE
+        self.app_released()
+
+    @provide(scope=Scope.SESSION)
+    def request(self) -> Iterable[RequestDep]:
+        yield REQUEST_DEP_VALUE
+        self.request_released()
+
+    @provide(scope=Scope.SESSION)
+    def websocket(self) -> Iterable[WebSocketDep]:
+        yield WS_DEP_VALUE
+        self.websocket_released()
+
+    @provide(scope=Scope.SESSION)
     def mock(self) -> Mock:
         return self.mock
diff --git a/tests/integrations/conftest.py b/tests/integrations/conftest.py
index 8d107d1..bb53324 100644
--- a/tests/integrations/conftest.py
+++ b/tests/integrations/conftest.py
@@ -1,7 +1,7 @@
 import pytest
 
 from dishka import make_async_container, make_container
-from .common import AppProvider
+from .common import AppProvider, WebSocketAppProvider
 
 
 @pytest.fixture
@@ -9,6 +9,10 @@ def app_provider():
     return AppProvider()
 
 
+@pytest.fixture
+def ws_app_provider():
+    return WebSocketAppProvider()
+
 @pytest.fixture
 def async_container(app_provider):
     return make_async_container(app_provider)
diff --git a/tests/integrations/fastapi/test_fastapi_websockets.py b/tests/integrations/fastapi/test_fastapi_websockets.py
new file mode 100644
index 0000000..8fd17f9
--- /dev/null
+++ b/tests/integrations/fastapi/test_fastapi_websockets.py
@@ -0,0 +1,119 @@
+from collections.abc import AsyncGenerator
+from contextlib import asynccontextmanager
+from unittest.mock import Mock
+
+import pytest
+from asgi_lifespan import LifespanManager
+from fastapi import FastAPI, WebSocket
+from fastapi.routing import APIWebSocketRoute
+from fastapi.testclient import TestClient
+
+from dishka import make_async_container
+from dishka.integrations.fastapi import (
+    FromDishka,
+    inject,
+    setup_dishka,
+)
+from ..common import (
+    APP_DEP_VALUE,
+    REQUEST_DEP_VALUE,
+    WS_DEP_VALUE,
+    AppDep,
+    RequestDep,
+    WebSocketAppProvider,
+    WebSocketDep,
+)
+
+
+@asynccontextmanager
+async def dishka_app(view, provider) -> AsyncGenerator[TestClient, None]:
+    app = FastAPI(routes=[APIWebSocketRoute("/", inject(view))])
+    container = make_async_container(provider)
+    setup_dishka(container, app)
+    async with LifespanManager(app):
+        yield TestClient(app)
+    await container.close()
+
+
+async def get_with_app(
+    websocket: WebSocket,
+    a: FromDishka[AppDep],
+    mock: FromDishka[Mock],
+) -> None:
+    await websocket.accept()
+    await websocket.receive()
+    mock(a)
+    await websocket.send_text("passed")
+
+
+@pytest.mark.asyncio
+async def test_app_dependency(ws_app_provider: WebSocketAppProvider):
+    async with dishka_app(get_with_app, ws_app_provider) as client:
+        with client.websocket_connect("/") as connection:
+            connection.send_text("...")
+            assert connection.receive_text() == "passed"
+
+        ws_app_provider.mock.assert_called_with(APP_DEP_VALUE)
+        ws_app_provider.app_released.assert_not_called()
+    ws_app_provider.app_released.assert_called()
+
+
+async def get_with_request(
+    websocket: WebSocket,
+    a: FromDishka[RequestDep],
+    mock: FromDishka[Mock],
+) -> None:
+    await websocket.accept()
+    await websocket.receive()
+    mock(a)
+    await websocket.send_text("passed")
+
+
+@pytest.mark.asyncio
+async def test_request_dependency(ws_app_provider: WebSocketAppProvider):
+    async with dishka_app(get_with_request, ws_app_provider) as client:
+        with client.websocket_connect("/") as connection:
+            connection.send_text("...")
+            assert connection.receive_text() == "passed"
+        ws_app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        ws_app_provider.request_released.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_request_dependency2(ws_app_provider: WebSocketAppProvider):
+    async with dishka_app(get_with_request, ws_app_provider) as client:
+        with client.websocket_connect("/") as connection:
+            connection.send_text("...")
+            assert connection.receive_text() == "passed"
+
+        ws_app_provider.request_released.assert_called_once()
+        ws_app_provider.request_released.reset_mock()
+
+        with client.websocket_connect("/") as connection:
+            connection.send_text("...")
+            assert connection.receive_text() == "passed"
+
+        ws_app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        ws_app_provider.request_released.assert_called_once()
+
+
+async def get_with_websocket(
+    websocket: WebSocket,
+    ws: FromDishka[WebSocketDep],
+    mock: FromDishka[Mock],
+) -> None:
+    await websocket.accept()
+    await websocket.receive()
+    mock(ws)
+    await websocket.send_text("passed")
+
+
+@pytest.mark.asyncio
+async def test_websocket_dependency(ws_app_provider: WebSocketAppProvider):
+    async with dishka_app(get_with_websocket, ws_app_provider) as client:
+        with client.websocket_connect("/") as connection:
+            connection.send_text("...")
+            assert connection.receive_text() == "passed"
+
+        ws_app_provider.mock.assert_called_with(WS_DEP_VALUE)
+        ws_app_provider.websocket_released.assert_called_once()
diff --git a/tests/integrations/faststream/test_faststream.py b/tests/integrations/faststream/test_faststream.py
index f32f103..b8cb739 100644
--- a/tests/integrations/faststream/test_faststream.py
+++ b/tests/integrations/faststream/test_faststream.py
@@ -8,6 +8,7 @@ from faststream.nats import NatsBroker, TestNatsBroker
 
 from dishka import make_async_container
 from dishka.integrations.faststream import (
+    FASTSTREAM_OLD_MIDDLEWARES,
     FromDishka,
     inject,
     setup_dishka,
@@ -63,7 +64,6 @@ async def get_with_request(
     return "passed"
 
 
-
 @pytest.mark.asyncio
 async def test_request_dependency(app_provider: AppProvider):
     async with dishka_app(get_with_request, app_provider) as client:
@@ -71,3 +71,49 @@ async def test_request_dependency(app_provider: AppProvider):
 
         app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
         app_provider.request_released.assert_called_once()
+
+
+@pytest.mark.asyncio
+@pytest.mark.skipif(
+    FASTSTREAM_OLD_MIDDLEWARES,
+    reason="Requires FastStream 0.5.0+",
+)
+async def test_autoinject_before_subscriber(app_provider: AppProvider):
+    broker = NatsBroker()
+    app = FastStream(broker)
+
+    container = make_async_container(app_provider)
+    setup_dishka(container, app=app, auto_inject=True)
+
+    broker.subscriber("test")(get_with_request)
+
+    async with TestNatsBroker(broker) as br:
+        assert await br.publish("", "test", rpc=True) == "passed"
+
+        app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        app_provider.request_released.assert_called_once()
+
+    await container.close()
+
+
+@pytest.mark.asyncio
+@pytest.mark.skipif(
+    FASTSTREAM_OLD_MIDDLEWARES,
+    reason="Requires FastStream 0.5.0+",
+)
+async def test_autoinject_after_subscriber(app_provider: AppProvider):
+    broker = NatsBroker()
+    app = FastStream(broker)
+
+    broker.subscriber("test")(get_with_request)
+
+    container = make_async_container(app_provider)
+    setup_dishka(container, app=app, auto_inject=True)
+
+    async with TestNatsBroker(broker) as br:
+        assert await br.publish("", "test", rpc=True) == "passed"
+
+        app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        app_provider.request_released.assert_called_once()
+
+    await container.close()
diff --git a/tests/integrations/starlette/test_starlette_websockets.py b/tests/integrations/starlette/test_starlette_websockets.py
new file mode 100644
index 0000000..4787281
--- /dev/null
+++ b/tests/integrations/starlette/test_starlette_websockets.py
@@ -0,0 +1,120 @@
+from collections.abc import AsyncGenerator
+from contextlib import asynccontextmanager
+from unittest.mock import Mock
+
+import pytest
+from asgi_lifespan import LifespanManager
+from starlette.applications import Starlette
+from starlette.routing import WebSocketRoute
+from starlette.testclient import TestClient
+from starlette.websockets import WebSocket
+
+from dishka import make_async_container
+from dishka.integrations.starlette import (
+    FromDishka,
+    inject,
+    setup_dishka,
+)
+from ..common import (
+    APP_DEP_VALUE,
+    REQUEST_DEP_VALUE,
+    WS_DEP_VALUE,
+    AppDep,
+    RequestDep,
+    WebSocketAppProvider,
+    WebSocketDep,
+)
+
+
+@asynccontextmanager
+async def dishka_app(view, provider) -> AsyncGenerator[TestClient, None]:
+    app = Starlette(routes=[WebSocketRoute("/", inject(view))])
+    container = make_async_container(provider)
+    setup_dishka(container, app)
+    async with LifespanManager(app):
+        yield TestClient(app)
+    await container.close()
+
+
+async def get_with_app(
+    websocket: WebSocket,
+    a: FromDishka[AppDep],
+    mock: FromDishka[Mock],
+) -> None:
+    await websocket.accept()
+    await websocket.receive()
+    mock(a)
+    await websocket.send_text("passed")
+
+
+@pytest.mark.asyncio
+async def test_app_dependency(ws_app_provider: WebSocketAppProvider):
+    async with dishka_app(get_with_app, ws_app_provider) as client:
+        with client.websocket_connect("/") as connection:
+            connection.send_text("...")
+            assert connection.receive_text() == "passed"
+
+        ws_app_provider.mock.assert_called_with(APP_DEP_VALUE)
+        ws_app_provider.app_released.assert_not_called()
+    ws_app_provider.app_released.assert_called()
+
+
+async def get_with_request(
+    websocket: WebSocket,
+    a: FromDishka[RequestDep],
+    mock: FromDishka[Mock],
+) -> None:
+    await websocket.accept()
+    await websocket.receive()
+    mock(a)
+    await websocket.send_text("passed")
+
+
+@pytest.mark.asyncio
+async def test_request_dependency(ws_app_provider: WebSocketAppProvider):
+    async with dishka_app(get_with_request, ws_app_provider) as client:
+        with client.websocket_connect("/") as connection:
+            connection.send_text("...")
+            assert connection.receive_text() == "passed"
+        ws_app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        ws_app_provider.request_released.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_request_dependency2(ws_app_provider: WebSocketAppProvider):
+    async with dishka_app(get_with_request, ws_app_provider) as client:
+        with client.websocket_connect("/") as connection:
+            connection.send_text("...")
+            assert connection.receive_text() == "passed"
+
+        ws_app_provider.request_released.assert_called_once()
+        ws_app_provider.request_released.reset_mock()
+
+        with client.websocket_connect("/") as connection:
+            connection.send_text("...")
+            assert connection.receive_text() == "passed"
+
+        ws_app_provider.mock.assert_called_with(REQUEST_DEP_VALUE)
+        ws_app_provider.request_released.assert_called_once()
+
+
+async def get_with_websocket(
+    websocket: WebSocket,
+    ws: FromDishka[WebSocketDep],
+    mock: FromDishka[Mock],
+) -> None:
+    await websocket.accept()
+    await websocket.receive()
+    mock(ws)
+    await websocket.send_text("passed")
+
+
+@pytest.mark.asyncio
+async def test_websocket_dependency(ws_app_provider: WebSocketAppProvider):
+    async with dishka_app(get_with_websocket, ws_app_provider) as client:
+        with client.websocket_connect("/") as connection:
+            connection.send_text("...")
+            assert connection.receive_text() == "passed"
+
+        ws_app_provider.mock.assert_called_with(WS_DEP_VALUE)
+        ws_app_provider.websocket_released.assert_called_once()
diff --git a/tests/unit/container/test_context_vars.py b/tests/unit/container/test_context_vars.py
index 0311398..3630452 100644
--- a/tests/unit/container/test_context_vars.py
+++ b/tests/unit/container/test_context_vars.py
@@ -1,6 +1,8 @@
 import pytest
 
 from dishka import (
+    DEFAULT_COMPONENT,
+    DependencyKey,
     Provider,
     Scope,
     decorate,
@@ -15,16 +17,28 @@ from dishka.exceptions import InvalidGraphError, NoContextValueError
 def test_simple():
     provider = Provider()
     provider.from_context(provides=int, scope=Scope.APP)
+    provider.from_context(provides=float, scope=Scope.APP)
     container = make_container(provider, context={int: 1})
+    container.context[DependencyKey(float, DEFAULT_COMPONENT)] = 2
     assert container.get(int) == 1
+    assert container.get(float) == 2
+    container.close()
+    assert container.get(int) == 1
+    assert container.get(float) == 2
 
 
 @pytest.mark.asyncio
 async def test_simple_async():
     provider = Provider()
     provider.from_context(provides=int, scope=Scope.APP)
+    provider.from_context(provides=float, scope=Scope.APP)
     container = make_async_container(provider, context={int: 1})
+    container.context[DependencyKey(float, DEFAULT_COMPONENT)] = 2
+    assert await container.get(int) == 1
+    assert await container.get(float) == 2
+    await container.close()
     assert await container.get(int) == 1
+    assert await container.get(float) == 2
 
 
 def test_not_found():
diff --git a/tests/unit/container/test_decorator.py b/tests/unit/container/test_decorator.py
index 120c437..3236f9b 100644
--- a/tests/unit/container/test_decorator.py
+++ b/tests/unit/container/test_decorator.py
@@ -58,6 +58,22 @@ def test_decorator():
     assert isinstance(a.a, A)
 
 
+def kwarg_decorator(x: int, *, a: A) -> A:
+    return ADecorator(a)
+
+
+def test_kwargs():
+    provider = Provider(scope=Scope.APP)
+    provider.provide(A)
+    provider.decorate(kwarg_decorator)
+    provider.provide(lambda: 1, provides=int)
+
+    container = make_container(provider)
+    a = container.get(A)
+    assert isinstance(a, ADecorator)
+    assert isinstance(a.a, A)
+
+
 def test_decorator_with_provides():
     class MyProvider(Provider):
         a = provide(A, scope=Scope.APP)
diff --git a/tests/unit/container/test_enter_exit.py b/tests/unit/container/test_enter_exit.py
index b28d1ab..759a63a 100644
--- a/tests/unit/container/test_enter_exit.py
+++ b/tests/unit/container/test_enter_exit.py
@@ -41,6 +41,12 @@ def test_sync_create(provide_scope, start_scope, expected_scope):
     container.close()
     assert a.closed
 
+    a2 = container.get(ClassA)
+    assert a2 is not a
+    assert a.dep == 100
+    container.close()
+    assert a2.closed
+
 
 @pytest.mark.parametrize(
     ("provide_scope", "start_scope", "expected_scope"),
@@ -67,6 +73,12 @@ async def test_async_create(provide_scope, start_scope, expected_scope):
     await container.close()
     assert a.closed
 
+    a2 = await container.get(ClassA)
+    assert a2 is not a
+    assert a.dep == 100
+    await container.close()
+    assert a2.closed
+
 
 @pytest.mark.parametrize(
     ("provide_scope", "start_scope", "expected_scope"),
diff --git a/tests/unit/container/test_exceptions.py b/tests/unit/container/test_exceptions.py
index 973634a..9eb74b3 100644
--- a/tests/unit/container/test_exceptions.py
+++ b/tests/unit/container/test_exceptions.py
@@ -88,6 +88,34 @@ async def test_async(dep_type):
     finalizer.assert_called_once()
 
 
+def test_no_factory_cls_sync():
+    class A:
+        def __init__(self, *, x: object):
+            pass
+    provider = Provider(scope=Scope.APP)
+    provider.provide(A)
+    with pytest.raises(NoFactoryError) as e:
+        make_container(provider)
+    assert e.value.requested == DependencyKey(object, DEFAULT_COMPONENT)
+
+def test_invalid_type_sync():
+    class A:
+        def __init__(self, x: "B"):  # noqa: F821
+            pass
+    provider = Provider(scope=Scope.APP)
+    with pytest.raises(NameError):
+        provider.provide(A)
+
+
+def test_no_type_sync():
+    class A:
+        def __init__(self, x):
+            pass
+    provider = Provider(scope=Scope.APP)
+    with pytest.raises(ValueError):  # noqa: PT011
+        provider.provide(A)
+
+
 class MissingFactoryProvider(Provider):
     @provide(scope=Scope.APP)
     def x(self, value: object) -> int:
@@ -98,7 +126,7 @@ class MissingFactoryProvider(Provider):
         return value
 
     @provide(scope=Scope.APP)
-    def b(self, value: float) -> complex:
+    def b(self, *, value: float) -> complex:
         return value
 
 
@@ -107,6 +135,7 @@ def test_no_factory_init_sync():
         make_container(MissingFactoryProvider())
     assert e.value.requested == DependencyKey(object, DEFAULT_COMPONENT)
 
+
 @pytest.mark.asyncio
 async def test_no_factory_init_async():
     with pytest.raises(NoFactoryError) as e:
@@ -128,7 +157,6 @@ def test_no_factory_path_sync():
     assert e.value.requested == DependencyKey(object, DEFAULT_COMPONENT)
 
 
-
 @pytest.mark.asyncio
 async def test_no_factory_async():
     container = make_async_container(Provider())
@@ -137,7 +165,6 @@ async def test_no_factory_async():
     assert e.value.requested == DependencyKey(object, DEFAULT_COMPONENT)
 
 
-
 @pytest.mark.asyncio
 async def test_no_factory_path_async():
     container = make_async_container(
@@ -186,6 +213,7 @@ def test_missing_context_var_sync():
     with pytest.raises(NoContextValueError):
         container.get(int)
 
+
 @pytest.mark.asyncio
 async def test_missing_context_var_async():
     class MyProvider(Provider):
@@ -194,3 +222,13 @@ async def test_missing_context_var_async():
     container = make_async_container(MyProvider())
     with pytest.raises(NoContextValueError):
         await container.get(int)
+
+
+def test_no_scope():
+    class NoScopeProvider(Provider):
+        @provide
+        def x(self) -> int:
+            return 1
+
+    with pytest.raises(ValueError):  # noqa: PT011
+        NoScopeProvider()
diff --git a/tests/unit/container/test_resolve.py b/tests/unit/container/test_resolve.py
index 3954dfa..7f93aac 100644
--- a/tests/unit/container/test_resolve.py
+++ b/tests/unit/container/test_resolve.py
@@ -129,3 +129,18 @@ def test_external_method(method):
 
     container = make_container(provider)
     assert container.get(ClassA) is A_VALUE
+
+
+def kwarg_factory(a: int, /, b: float, c: complex) -> str:
+    return "ok"
+
+
+def test_kwargs():
+    provider = Provider(scope=Scope.APP)
+    provider.provide(kwarg_factory)
+    provider.provide(lambda: 1, provides=int)
+    provider.provide(lambda: 1.0, provides=float)
+    provider.provide(lambda: 1j, provides=complex)
+
+    container = make_container(provider)
+    assert container.get(str) == "ok"
diff --git a/tests/unit/sample_providers.py b/tests/unit/sample_providers.py
index 3390953..99f5cdf 100644
--- a/tests/unit/sample_providers.py
+++ b/tests/unit/sample_providers.py
@@ -68,6 +68,7 @@ value_factory = Factory(
     provides=DependencyKey(ClassA, None),
     source=A_VALUE,
     dependencies=[],
+    kw_dependencies={},
     type_=FactoryType.VALUE,
     scope=Scope.APP,
     is_to_bind=False,
diff --git a/tests/unit/test_context_proxy.py b/tests/unit/test_context_proxy.py
new file mode 100644
index 0000000..671d884
--- /dev/null
+++ b/tests/unit/test_context_proxy.py
@@ -0,0 +1,21 @@
+import pytest
+
+from dishka import DEFAULT_COMPONENT, DependencyKey
+from dishka.context_proxy import ContextProxy
+
+
+def test_simple():
+    int_key = DependencyKey(int, DEFAULT_COMPONENT)
+    context = {int_key: 1}
+    cache = {**context, DependencyKey(float, DEFAULT_COMPONENT): 2}
+    proxy = ContextProxy(context=context, cache=cache)
+    assert len(proxy) == 2
+    assert proxy[int_key] == proxy.get(int_key) == 1
+    assert list(proxy) == list(cache)
+
+    complex_key = DependencyKey(complex, DEFAULT_COMPONENT)
+    proxy[complex_key] = 3
+    assert context[complex_key] == cache[complex_key] == 3
+
+    with pytest.raises(RuntimeError):
+        del proxy[complex_key]