Skip to content

← Everyday Standard Library Tools step 39 of 55

Medium Primitives

Change an Attribute for One Block and Put It Back

Build temp_attr, a context manager that sets an attribute for the length of a with block and then puts back what was there before. Tests use a helper like this to switch a setting for a few lines.

holder = Holder()
setattr(holder, "mode", "prod")
with temp_attr(holder, "mode", "test"):
    getattr(holder, "mode")  # "test"
getattr(holder, "mode")      # "prod"

What to write

Fix temp_attr(obj, name, value) in the starter. It is a generator function decorated with @contextmanager.

  • On entry, save the attribute's current value, then set it to value.
  • On exit, put the saved value back. If the attribute did not exist before, delete it instead.
  • Restore it on a normal exit and when the block raises. After restoring, let the exception reach the caller.
  • Tell a missing attribute from one set to None. Use a sentinel such as _MISSING = object() as the default for getattr, not None.

The starter has both bugs. It uses None as its missing marker, and it restores only after a normal exit.

Then write solve. It creates a Holder, sets each key of initial on it as an attribute, and then does these steps:

  1. Enter temp_attr(obj, name, value) and append the attribute's value to a list called seen.
  2. If nested_value is not None, enter a second temp_attr with nested_value inside the first. Append the value inside it, then append the value again after the inner block ends. Only None skips this step. False, 0 and "" still nest.
  3. If raise_inside is true, raise RuntimeError inside the outer block. Catch it outside the with statement.
  4. If reuse is true, create one more context manager with temp_attr(obj, name, value) and keep it in a variable. Enter it once with an empty body, then try to enter the same object again. Catch any Exception from the second entry. Add nothing to seen in this step.

solve returns:

Key Value
seen The values from steps 1 and 2, in order
propagated True if the RuntimeError came out of the outer block, otherwise False
has_after Whether the attribute exists after all the steps
after Its value then, or None if it does not exist
reuse_rejected True if the second entry raised. False when reuse is false

Edge cases

  • With initial={"mode": "prod"}, value="test" and nested_value="inner", seen is ["test", "inner", "test"] and after is "prod", whether or not raise_inside is true.
  • If the attribute did not exist at the start, has_after is False and after is None.
  • If it held None at the start, has_after is True and after is None.