Undo/Redo

Undo is a pretty common application functionality. I present a Python implementation where a History object is used to perform any action that may be undone. This object can go back and forth in the actions history. The idea is that, for every action, you provide one way to perform the action and one way to revert the action.

Implementation

The History class definition follows. An instance maintains a list of actions, each action being composed of a way to perform something and a way to revert it:

[download history.py]

class Action(object):
    """Describes an action, and a way to revert that action"""
 
    def __init__(self, do, undo):
        """Both do and undo are in the form (function, [arg1, arg2, ...])."""
        self._do = do
        self._undo = undo
 
    def do(self):
        fun, args = self._do
        return fun(*args)
 
    def undo(self):
        fun, args = self._undo
        return fun(*args)
 
 
class History(object):
    "Maintains a list of actions that can be undone and redone."
 
    def __init__(self):
        self._actions = []
        self._last = -1
 
    def _push(self, action):
        if self._last < len(self._actions) - 1:
            # erase previously undone actions
            del self._actions[self._last + 1:]
        self._actions.append(action)
        self._last = self._last + 1
 
    def undo(self):
        if self._last < 0:
            return None
        else:
            action = self._actions[self._last]
            self._last = self._last - 1
            return action.undo()
 
    def redo(self):
        if self._last == len(self._actions) - 1:
            return None
        else:
            self._last = self._last + 1
            action = self._actions[self._last]
            return action.do()
 
    def add(self, do, undo):
        """Does an action and adds it to history.
 
        Both do and undo are in the form (function, [arg1, arg2, ...]).
 
        """
        action = Action(do, undo)
        self._push(action)
        return action.do()

Usage

Download the history module and place it in your working directory to try out the examples.

First, a History instance must be created.

from history import History
 
h = History()

You can then add an action to the history…

h.add(perform, revert)

… and revert or redo that action later:

h.undo()
h.redo()

The add() method accepts as arguments two functions and their corresponding list of arguments:

def foo(x, y):
    ....
 
def unfoo(z):
    ....
 
perform = (foo, [arg1, arg2])
revert = (unfoo, [arg3])
 
h.add(perform, revert)

I take as an example the modification of a dictionary d:

import history
 
d = {"x": 0, "y": 0}
h = history.History()
 
def modify(d, key, value):
    d[key] = value
 
def reset(d, saved):
    d.clear()
    d.update(saved)
 
saved = d.copy()
perform = (modify, [d, "x", 9999])
revert = (reset, [d, saved])
 
print "Original:  ", d
 
h.add(perform, revert)
print "Modified:  ", d
 
h.undo()
print "After undo:", d
 
h.redo()
print "After redo:", d
Original:   {'y': 0, 'x': 0}
Modified:   {'y': 0, 'x': 9999}
After undo: {'y': 0, 'x': 0}
After redo: {'y': 0, 'x': 9999}

Advanced Example

In the code below, undo/redo is provided to a Calculator class through a Python decorator.

from history import History
 
class Calculator(object):
    def __init__(self):
	self.value = 0.
        self._history = History()
        self.undo = self._history.undo
        self.redo = self._history.redo
 
    def _operation(method):
        def decorated(self, n):
            saved = self.value
            perform = (method, (self, n))
            revert = (self._set_value, (saved,))
            self._history.add(perform, revert)
        return decorated
 
    def _set_value(self, n):
        self.value = n
 
    @_operation
    def add(self, n):
	self.value += n
 
    @_operation
    def sub(self, n):
	self.value -= n
 
    @_operation
    def mult(self, n):
        self.value *= n
 
    @_operation
    def div(self, n):
	self.value /= n
 
 
calculator = Calculator()
print "Initial value:", calculator.value
calculator.add(2)
print "+ 2 =", calculator.value
calculator.sub(3)
print "- 3 =", calculator.value
calculator.mult(5)
print "* 5 =", calculator.value
calculator.div(-2)
print "/ -2 =", calculator.value
 
print
 
for _ in range(4):
    calculator.undo()
    print "undo:", calculator.value
 
print
 
for _ in range(4):
    calculator.redo()
    print "redo:", calculator.value

Output:

Initial value: 0.0
+ 2 = 2.0
- 3 = -1.0
* 5 = -5.0
/ -2 = 2.5

undo: -5.0
undo: -1.0
undo: 2.0
undo: 0.0

redo: 2.0
redo: -1.0
redo: -5.0
redo: 2.5

Posted by pierre on 16 December 2010 in python

Leave a Reply

*