Skip to content

Commit e1be6af

Browse files
author
remimd
committed
fix: Prevent silent misuse of exited scopes
1 parent 5e701bf commit e1be6af

3 files changed

Lines changed: 37 additions & 13 deletions

File tree

injection/_core/scope.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,21 @@ def _bind_scope(
212212
stack.close()
213213

214214

215+
class _SealedScopeCache(MutableMapping[Any, Any]):
216+
__slots__ = ()
217+
218+
@staticmethod
219+
def guard(*args: Any, **kwargs: Any) -> NoReturn:
220+
raise ScopeError("Can't access cache of an exited scope.")
221+
222+
__delitem__ = __getitem__ = __iter__ = __len__ = __setitem__ = guard
223+
224+
225+
_sealed_cache = _SealedScopeCache()
226+
227+
del _SealedScopeCache
228+
229+
215230
@runtime_checkable
216231
class Scope(Protocol):
217232
__slots__ = ()
@@ -227,14 +242,13 @@ def enter[T](self, context_manager: ContextManager[T]) -> T:
227242
raise NotImplementedError
228243

229244

230-
@dataclass(repr=False, frozen=True, slots=True)
245+
@dataclass(repr=False, eq=False, slots=True)
231246
class BaseScope[T](Scope, ABC):
232247
delegate: T
233-
cache: MutableMapping[SlotKey[Any], Any] = field(
234-
default_factory=dict,
235-
init=False,
236-
hash=False,
237-
)
248+
cache: MutableMapping[SlotKey[Any], Any] = field(default_factory=dict, init=False)
249+
250+
def close(self) -> None:
251+
self.cache = _sealed_cache
238252

239253

240254
class AsyncScope(BaseScope[AsyncExitStack]):
@@ -253,6 +267,7 @@ async def __aexit__(
253267
exc_value: BaseException | None,
254268
traceback: TracebackType | None,
255269
) -> Any:
270+
self.close()
256271
return await self.delegate.__aexit__(exc_type, exc_value, traceback)
257272

258273
async def aenter[T](self, context_manager: AsyncContextManager[T]) -> T:
@@ -278,6 +293,7 @@ def __exit__(
278293
exc_value: BaseException | None,
279294
traceback: TracebackType | None,
280295
) -> Any:
296+
self.close()
281297
return self.delegate.__exit__(exc_type, exc_value, traceback)
282298

283299
async def aenter[T](self, context_manager: AsyncContextManager[T]) -> NoReturn:

tests/core/test_scope.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pytest
55

6-
from injection import define_scope, find_instance, scoped
6+
from injection import SlotKey, define_scope, find_instance, scoped
77
from injection.exceptions import ScopeAlreadyDefinedError, ScopeError
88

99

@@ -35,3 +35,11 @@ def assertion() -> None:
3535
with ThreadPoolExecutor() as executor:
3636
with define_scope("test"):
3737
executor.submit(assertion)
38+
39+
40+
def test_define_scope_with_sealed_raise_scope_error():
41+
with define_scope("test") as scope:
42+
...
43+
44+
with pytest.raises(ScopeError):
45+
scope.set_slot(SlotKey(), object())

uv.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)