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:
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
Active Object (Actor) in Python
A simple implementation of the active object design pattern in Python.
import Queue import threading def command(method): """Decorator to enqueue method calls in Actor instances.""" def enqueue_call(self, *args, **kwargs): args = list(args) args.insert(0, self) self._commands.put((method, args, kwargs)) return enqueue_call class Actor(threading.Thread): """A simple implementation of the active object design pattern.""" def __init__(self): threading.Thread.__init__(self) self._commands = Queue.Queue() self._must_stop = False @command def stop(self): self._must_stop = True def run(self): while not self._must_stop: cmd, args, kwargs = self._commands.get() cmd(*args, **kwargs) if __name__ == '__main__': import time class MyActor(Actor): @command def say(self, sentence): time.sleep(0.1) print sentence a = MyActor() a.start() a.say("Hello.") a.say("Bye.") a.stop() print "Main thread finished."
imapfwd: forward mail to a main box
I have several email addresses but I want all messages to arrive in a single mailbox. imapfwd
connects to an imap server and forward all unread mails to an other address through SMTP. Here are the recognized options:
$ ./imapfwd --help Usage: imapfwd [options] USERNAME IMAPHOST SMTPHOST DESTINATION Options: -h, --help show this help message and exit --pass=PASSWORD imap password -p PORT imap port --ssl use SSL for imap --prefix=PREFIX append a string to subject. ex: [box1]
I use the script in a cronjob. To set up the job, type crontab -e
and add a line like this (I’ve saved the script in /home/pierre/bin/):
# m h dom mon dow command */10 * * * * /home/pierre/bin/imapfwd --pass or4nge mylogin imap.hmail.com smtp.isp.com mainbox@fmail.com
Here is the script:
#!/usr/bin/env python # Copyright 2009 (C) Pierre Duquesne <stackp@online.fr> # Licensed under the BSD Revised License. import imaplib import smtplib import sys import optparse import getpass import email.parser USAGE="imapfwd [options] USERNAME IMAPHOST SMTPHOST DESTINATION" def parse_args(): "Parse command-line arguments." parser = optparse.OptionParser(usage=USAGE) parser.add_option('--pass', dest='password', default=None, help="imap password") parser.add_option('-p', dest='port', type='int', default=None, help="imap port") parser.add_option('--ssl', dest='ssl', default=False, action='store_true', help="use SSL for imap") parser.add_option('--prefix', dest='prefix', default=None, action='store', help="append a string to subject. ex: [box1]") options, remainder = parser.parse_args(sys.argv[1:]) return options, remainder options, args = parse_args() try: username = args[0] imaphost = args[1] smtphost = args[2] destination = args[3] except: print "Error: some arguments are missing. Try --help." print USAGE sys.exit(1) # connect to imap print 'Connecting to %s as user %s ...' % (imaphost, username) if options.ssl: IMAP = imaplib.IMAP4_SSL else: IMAP = imaplib.IMAP4 try: if options.port: imap_server = IMAP(imaphost, options.port) else: imap_server = IMAP(imaphost) if not options.password: options.password = getpass.getpass() imap_server.login(username, options.password) except Exception,e: print 'Error:', e; sys.exit(1) # connect to smtp try: smtp_server = smtplib.SMTP(smtphost) except Exception, e: print 'Could not connect to', smtphost, e.__class__, e sys.exit(2) # filter unseen messages imap_server.select("INBOX") resp, items = imap_server.search(None, "UNSEEN") numbers = items[0].split() # forward each message sender = "%s@%s" % (username, imaphost) for num in numbers: resp, data = imap_server.fetch(num, "(RFC822)") text = data[0][1] if options.prefix: parser = email.parser.HeaderParser() msg = parser.parsestr(text) msg['Subject'] = options.prefix + msg['Subject'] text = msg.as_string() smtp_server.sendmail(sender, destination, text) # Flag message as Seen (may have already been done by the server anyway) imap_server.store(num, '+FLAGS', '\\Seen') imap_server.close() smtp_server.quit()
peanut: a JPEG comment editor
It’s useful to store informations about photo files such as where a photo was shot and who appears on it. JPEG files have a comment field but it’s surprisingly difficult to find a Linux application to edit them. So I wrote a script, that is easy to integrate with image viewers that support custom actions such as Mirage and GQview.
Here are the instructions to use peanut. First, you’ve got to install pygtk and pyexiv2, through your distribution package manager. Then, download the script and save it somewhere (personnally, I save scripts in
/home/pierre/bin/
). Make it executable, either through your file navigator or through the command-line (chmod +x peanut
). Now, you should be able to run peanut from the command-line, typingpeanut FILE
, whereFILE
is a JPEG file.And to make editing comments even easier, you can set up a custom action in a picture viewer. I describe this for the Mirage image viewer.
A Custom Action in Mirage
Choose Edit -> Custom Actions -> Configure, press the + button to add a new action. Enter a name such as Edit Comment, fill the Command field with the full path to the peanut script followed by %F (for example, /home/pierre/bin/peanut %F), and finally click on the shortcut box and type a key combination that will launch the comment editor (for instance c). Press OK and you’re good to go.
Now, you can browse pictures through Mirage, and type c when you want to edit a comment: the editor will pop up.
[download peanut]