Repository: reagento/dishka
Pull request #156 (2024-05-20 20:51:13)

Feature/reset container


Patch:
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)


Test patch:
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_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/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]