jump to navigation

A Generic CLI Dispatcher module for Python May 30, 2007

Posted by samwyse in Scripting.
trackback

Here’s a little script that makes it easy to create command line interpreters (CLIs). Creating a CLI framework is one of my usual benchmarks when I learn a new programming language. It usually requires a bit of in-depth knowlege, yet is easy to code in only a day or so. In this case, it took a bit longer but it was worth it in the end because it taught me more about decorators.

I encountered decorators when I was first learning Python about a year ago, and apparently promptly forgot about them. Then, as I was searching for an answer to some question or another, I stumbled across Jeremy Lowery’s blog, THENSYS, where he had just posted “Demystifying Programmable Dispatch with Python“. There I was not only reminded of how decorators worked but given one that doesn’t decorate at all; it exists purely for its side-effects. Of course, Jeremy’s example was for dispatching in web-based applications, so I had to rewrite it for my needs. Still, I tried to make it a bit more general than I needed, so perhaps it will be useful to others as well.
Here’s an example of how to use it to set up a command with two aliases, “help” and “man”.

from GenericDispatcher import GenericDispatcher

command = GenericDispatcher()@command(('help', 'man'))

def cmdHelp(*argv):
    """Usage: %s [command]
List available commands or explain their usage.
"""
    dt = command.dispatch_table
    if len(argv):
        for arg in argv:
            try:
                print dt[arg][0].__doc__ % arg
            except:
                print "No help available for", arg
    else:
        print "Valid commands are:"
        print ', '.join(sorted(dt.keys()))
        print "Use 'help ' for more information."

I chose this as my example because (IMHO) it illustrates some other neat tricks. First of all, the “@command” decorator will add the “cmdHelp” function to the dispatch table twice, once under the “help” key and once under “man” (for any Unix users). The body of the command does one of two things: If you just say “help”, then a list of the dispatch table’s keys is displayed, so the user knows what commands are available. If you say “help” followed by one or more command names, then we look up the entry in the dispatch table, get the code pointer for the procedure (that’s the “[0]” part), and grab the doc-string, which is then printed with the command name interpolated at the appropriate spot. The try statement catches everything from user misspellings to forgotten doc-strings.

Enough talking, here’s the code:

#!/usr/bin/python

"""A class taht wraps a dispatch table, and a function/method decorator
to make it easy to use.

:Copyright:  2007
:Author:     Sam Denton samwyse at gmail dot com)
:Concept by: Jeremy Lowery
:License:    GPL, http://www.gnu.org/copyleft/gpl.html
"""

# based on code from:
# http://www.thensys.com/index.php?title=demystifying_programmable_dispatch
class GenericDispatcher(object):
    """cli_command = GenericDispatcher()
    Creates a dispatch table."""
    def __init__(self):
        """Create an empty table."""
        self.dispatch_table = {}
    def __call__(self, trigger, *argv):
        """@cli_command(trigger, [additional args]...)
	Creates a decorator that adds a procedure to the dispatch table.
	'Trigger' may be a hashable object, which is then used as the key, or
	it may be an iteratable object, in which case the procedure is added
	to the table multiple times.  (This is to support command aliases.)
	The additional args are also saved in the dispatch table, and are
	appended to the argument list when the procedure is dispatched.
	"""
        def stash(proc):
            """Actually add the function to the table"""
            if hasattr(trigger, '__iter__'):
                for item in trigger:
                    self.dispatch_table[item] = (proc,) + argv
            else:
                self.dispatch_table[trigger] = (proc,) + argv
            return proc
        return stash
    def dispatch(self, trigger, *argv):
        """cli_command.dispatch(trigger, [optional args]...)
	Look up a procedure in the table, and invoke it with both the
	optional args and any addtional args stored in the table.
	"""
        entry = self.dispatch_table[trigger]
        proc, more = entry[0], entry[1:]
        return proc(*argv+more)
Advertisements

Comments»

1. Jeremy Lowery - October 6, 2008

Hey Sam,

I’m glad that you had some ideas about customized dispatch routines in Python. A lot of those articles on thensys need quite a bit of clean up. I think it’s called rough draft hell.

Anyway, I like your CLI dispatch example. Nice and clever.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: