Skip to content

← Everyday Standard Library Tools step 41 of 55

Hard Primitives

Open a List of Resources and Close Them All

Write open_all(names, opener), a context manager that opens one resource per name and closes them all when the caller's with block ends. If one open fails, it closes the ones it already opened.

with open_all(["a", "b"], opener) as handles:
    ...  # handles holds two open handles, a then b
# both are closed here, b first

Handle in the starter is a Protocol. Any object with a name attribute and a close() method counts as a Handle, whatever its class.

What to write

Fix open_all in the starter. It is a generator function decorated with @contextmanager.

  • Call opener(name) once per name, in input order.
  • Register each handle with an ExitStack as soon as opener returns it, before you open the next one.
  • Yield the list of handles only after every open has succeeded.
  • If opener raises, close the handles already opened, newest first, and let the error reach the caller. Do not run the caller's block.
  • When the caller's block ends, normally or with an exception, close every handle in reverse order.

FakeFile has close() but is not a context manager, so wrap each one in contextlib.closing before you pass it to stack.enter_context. Keep the yield inside the with ExitStack() block, so the stack closes the handles when the caller's block ends.

The starter has two bugs. It closes the handles in input order instead of reverse order, and it closes nothing when an open or the caller's block raises.

What solve reports

The starter already has solve(names, fail_on). Keep it as it is. Its opener records "open:<name>" and returns a FakeFile. When the name equals fail_on, the opener records "error:<name>" and raises OSError(name) instead. Each FakeFile records "close:<name>" when it closes. Inside the with block, solve records "body". It returns:

  • log: every open, error, body and close event, in order.
  • opened: the handle names the block received, or [] if an open failed.
  • error: "OSError:<name>" if an open failed, otherwise "".

For names=["a", "b", "c"] with no failure, the log is:

["open:a", "open:b", "open:c", "body", "close:c", "close:b", "close:a"]

Edge cases

  • With fail_on="b", the log is ["open:a", "error:b", "close:a"], opened is [] and error is "OSError:b". open_all never calls the opener for the names after b.
  • An empty names list still runs the block. The result is {"log": ["body"], "opened": [], "error": ""}.
  • The tests pass fail_on="" when no open should fail.
  • No test raises inside the caller's block, but open_all must still close every handle when that happens.