diff --git a/anydi/_container.py b/anydi/_container.py index 74d9330..90969c1 100644 --- a/anydi/_container.py +++ b/anydi/_container.py @@ -890,6 +890,7 @@ def release(self, dependency_type: Any, /) -> None: def reset(self) -> None: """Reset resolved instances.""" self._invalidate_refs() + failure: BaseException | None = None for dependency_type, provider in self._providers.items(): if provider.scope == "transient": continue @@ -897,7 +898,12 @@ def reset(self) -> None: context = self._get_instance_context(provider.scope) except LookupError: continue - context.release(dependency_type) + try: + context.release(dependency_type) + except Exception as exc: # the rest still resets + failure = failure or exc + if failure is not None: + raise failure # == Lazy References == diff --git a/anydi/_context.py b/anydi/_context.py index 3b96fd7..0f9bf36 100644 --- a/anydi/_context.py +++ b/anydi/_context.py @@ -108,9 +108,11 @@ def release(self, key: Any) -> None: "with `aclose()` instead." ) closer = self._closers.pop(key, None) + # Dropped first: a finaliser that raises must not leave the instance + # behind, or the next resolution hands out a closed one. + self._items.pop(key, None) if closer is not None: closer(None, None, None) - self._items.pop(key, None) def __setitem__(self, key: Any, value: Any) -> None: self._items[key] = value diff --git a/tests/test_container.py b/tests/test_container.py index 02c681c..8c89591 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -2023,6 +2023,54 @@ def test_release_transient_instance(self, container: Container) -> None: assert not container.is_resolved(str) + def test_release_forgets_an_instance_whose_finaliser_raises( + self, container: Container + ) -> None: + events = [] + + def provide_conn() -> Iterator[str]: + events.append("open") + yield "conn" + raise RuntimeError("no") + + container.register(str, provide_conn, scope="singleton") + container.resolve(str) + + with pytest.raises(RuntimeError, match="no"): + container.release(str) + + assert not container.is_resolved(str) + + container.resolve(str) + + assert events == ["open", "open"] + + def test_reset_releases_them_all_when_one_finaliser_raises( + self, container: Container + ) -> None: + events = [] + + def provide_first() -> Iterator[str]: + yield "first" + events.append("first") + raise RuntimeError("no") + + def provide_second() -> Iterator[int]: + yield 1 + events.append("second") + + container.register(str, provide_first, scope="singleton") + container.register(int, provide_second, scope="singleton") + container.resolve(str) + container.resolve(int) + + with pytest.raises(RuntimeError, match="no"): + container.reset() + + assert events == ["first", "second"] + assert not container.is_resolved(str) + assert not container.is_resolved(int) + def test_release_closes_the_resource(self, container: Container) -> None: events = []