We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Stdlib Mastery step 37 of 55
ExitStack: all-or-nothing acquisition of N resources
Open a runtime-determined number of resources with all-or-nothing semantics: if any acquisition fails, everything already acquired is released and the error propagates.
class Handle(Protocol):
name: str
def close(self) -> None: ...
@contextmanager
def open_all(
names: Sequence[str], opener: Callable[[str], Handle]
) -> Iterator[list[Handle]]: ...
class FakeFile:
def __init__(self, name: str, log: list[str]) -> None: ...
def close(self) -> None: ... # sets self.closed, appends f"close:{name}"
def solve(names: list[str], fail_on: str) -> dict[str, object]:
solve builds a recording opener that appends f"open:{name}" and returns
a FakeFile, except for fail_on, where it appends f"error:{name}" and
raises OSError(name). It then runs open_all(names, opener), appending
"body" inside the block, and returns:
{"log": [...], "opened": [handle.name for each handle], "error": "OSError:<name>" or ""}
On failure, opened stays [] and the error string is recorded.
The log is the whole assertion. For ["a", "b", "c"] with no failure it
must be:
["open:a", "open:b", "open:c", "body", "close:c", "close:b", "close:a"]
Note the reverse unwind order — the same guarantee nested with
statements give, and it matters whenever resources depend on each other (a
transaction has to commit before its connection closes). With fail_on="b"
the log is ["open:a", "error:b", "close:a"]: the partially-acquired handle
is released before the exception leaves.
Why this needs ExitStack. There is no with syntax for a runtime number
of resources. The hand-rolled version is a try/finally around a
partially-populated list, with a cleanup loop that has to tolerate the entries
that were never filled — and it is wrong more often than it is right. The
naive handles = [opener(n) for n in names] followed by a close loop after
the yield leaks every handle opened before the failing one, and leaks all of
them if the body raises.
Two API details worth knowing. stack.callback(fn, ...) registers a
plain function, but such callbacks cannot suppress exceptions — they never
see them, and are called with no arguments regardless of how the block exited.
Cleanup that must distinguish commit from rollback has to be a real context
manager. And ExitStack is reusable but not reentrant: entering the same
stack twice means leaving the inner with fires every callback registered so
far, including the outer block’s.
FakeFile is not itself a context manager, so wrap it — contextlib.closing
turns any object with a close() into one.
The Protocol is doing real work here: open_all never mentions FakeFile,
so the same function sequences any resource with a name and a close().
Stuck?
Python reference solution
Sign in to attempt this problem and reveal the reference solution.