An editor has several settings objects, and every one of them should support undo. Rather than write the history code into each class, write a class decorator undoable that adds it to any class:
@undoable
class Settings:
def __init__(self, theme, font_size):
self.theme = theme
self.font_size = font_size
After decoration:
- Assignments and deletions made while the object is being constructed (inside
__init__) are not recorded. Every laterobj.name = valueanddel obj.namethat succeeds is recorded; one that raises is not. An assignment made while a recorded change is running (a property setter storing its value inself._font_size, say) is part of that change and is not recorded separately. obj.undo()reverts the most recent recorded change that has not been undone and returns the attribute's name. Reverting assigns the value the attribute had before (as it read then, so through a property's setter if there is one), or deletes the attribute if reading it had failed. Nothing an undo does is recorded. With nothing left to undo,undo()raisesIndexError.obj.changes()returns how many recorded changes can still be undone.- The class keeps working as before: its properties, its own
__setattr__if it has one, its other methods, and its name and docstring.undoablereturns the same class object, modified.
The tests decorate classes inside setup helpers you can call with Run: settings_class() (a class-level default tab_width and a checked property font_size), session(ops) (applies ("set", name, value), ("del", name) and ("undo",) to Settings("dark", 12)), same_class(), separate_histories(), own_setattr() and long_session(n, seed). outcome(call) returns a result or the exception's name.
Examples
Input: session([("set", "theme", "light"), ("set", "font_size", 0), ("set", "font_size", 14), ("undo",), ("undo",), ("undo",)])
Output: ([None, "ValueError", None, "font_size", "theme", "IndexError"], [("theme", "dark"), ("font_size", 12), ("tab_width", 4)], 0)
Input: session([("set", "margin", 2), ("del", "theme"), ("undo",)])
Output: ([None, None, "theme"], [("margin", 2), ("theme", "dark"), ("font_size", 12), ("tab_width", 4)], 1)
Constraints
- Decorated classes store their attributes in an instance
__dict__(no__slots__); you may add one attribute of your own for the history.
Goals
- Write a class decorator that changes a class's behaviour and returns the class itself
- Wrap a class's `__init__`, `__setattr__` and `__delattr__` while keeping what they did before
- Record the old value of every change, so that each change can be reverted in reverse order