Problem 546814 · medium · Level 05 Advanced Algorithms & Graphs

Borrow the Settings, Then Give Them Back

py-context-managers · contextlib · getattr/setattr · sentinel values · clean-up

A notebook's plotting settings are shared by every chart. One chart needs imperial units and five decimals, and the settings must be exactly as before once it is drawn, even if drawing fails. Write a context manager patched(target, **changes):

with patched(settings, units="imperial", precision=5):
    draw_chart()
  • If target is a dict, the changes are to its keys; otherwise they are to its attributes.
  • On entering, each change is applied in the order given, and the block gets target itself (for as).
  • On leaving, however the block ends, every changed name gets back its old value, and names that did not exist before are removed again. Any exception from the block still reaches the caller.
  • If applying a change fails (for example, an attribute that cannot be assigned raises AttributeError), the changes already applied are undone, and then that error reaches the caller; the block does not run.
  • Restoring does not care what the block did with a patched name in the meantime: it is set back to its value from before the with.

Blocks may be nested. Setup code available with Run: the classes Settings (with precision, units, debug and a state() method listing its attributes) and Panel (with a read-only area), label(settings, metres), and the scenarios basic(), failing_block(), environment(), nested(), read_only(), block_changes_too(), none_values() and many_blocks(n).

Examples

Input:  basic()
Output: (("32.81 ft", True, True),
         [("debug", False), ("precision", 2), ("units", "metric")],
         False)

Input:  environment()
Output: ([("HOME", "/home/ana"), ("LANG", "fr"), ("TZ", "UTC")],
         [("HOME", "/home/ana"), ("LANG", "en")])

Constraints

  • Names are valid identifiers, and the block never deletes a patched name.
  • A value may be None, which is an ordinary old value, not "did not exist".

Goals

  • Change shared state for the duration of a `with` block and restore it afterwards
  • Tell apart "had an old value" from "did not exist" with a sentinel
  • Undo partial changes when setting up fails halfway
Starting Python…