Modular Command Interface in Python

Adolphus implements a neat little interactive command interface in Python. It grew rather organically, so for all I know it’s a bizarre way of doing something available in some library. In any case, it’s pretty light and does what I need, so maybe you’ll find it useful too.

The commands module first defines an empty dictionary of commands. The keys are the command names, and the values are the associated callables.

38
commands = {}

It also defines a simple custom exception class, which currently does nothing interesting besides being identifiable.

40
41
class CommandError(Exception):
    "Command failed (usually non-fatal)."</code>

The interesting part is the @command decorator. This applies to any function taking 3 arguments, viz. a reference to the Experiment object, a list of strings comprising the command arguments, and a string specifying the output format of the command (pickle, csv, or text). The response check is a developer-level assert. Other exceptions arising from the call to the wrapped functions could be user-level errors, and the interface should be able to handle these as run-time CommandError exceptions, hence the try block. The docstring of the wrapped function is expected to provide usage details, so it is adopted by the wrapper function. Finally, the command is added under the name of the function to the commands dictionary.

44
45
46
47
48
49
50
51
52
53
54
55
def command(f):
    def wrapped(ex, args, response='pickle'):
        assert response in ['pickle', 'csv', 'text']
        try:
            return f(ex, args, response)
        except CommandError as e:
            raise e
        except Exception, e:
            raise CommandError('%s: %s' % (type(e).__name__, e))
    wrapped.__doc__ = f.__doc__
    commands[f.__name__] = wrapped
    return wrapped

Since the @command decorator lives in the module, and the ex argument is passed in, the developer can define custom commands outside of the module itself, simply by importing the decorator and (optionally) CommandError. This was the factor that drove the design.

The following is a relatively simple example of a command definition. In this case, it catches a possible exception from its main action to offer a more meaningful error description to the user.

479
480
481
482
483
484
485
486
487
488
489
@command
def setactivelaser(ex, args, response):
    """\
    Set the active laser to the specified laser.
 
    usage %s laser
    """
    try:
        ex.model.active_laser = args[0]
    except AttributeError:
        raise CommandError('not a range coverage model')

The main example of calling a command is given by Experiment.execute(). In this case, any CommandError is re-raised with an appended usage message.

351
352
353
354
355
356
357
358
359
360
361
362
363
364
        cmd, args = cmd.split()[0], cmd.split()[1:]
        if cmd not in commands.commands:
            raise commands.CommandError('invalid command')
        try:
            return commands.commands[cmd](self, args, response=response)
        except commands.CommandError as e:
            es = str(e)
            if commands.commands[cmd].__doc__:
                for line in commands.commands[cmd].__doc__.split('\n'):
                    line = line.strip(' ')
                    if line.startswith('usage'):
                        es += '\n' + line % cmd
                        break
            raise commands.CommandError(es)

It is also possible to define “meta-commands” that operate on the command system itself, as long as they have scope access (e.g. are defined within the commands module). The following example allows the definition of run-time aliases to commands which optionally force some or all of the command parameters to fixed values.

58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
@command
def alias(ex, args, response):
    """\
    Create an alias to another command, optionally forcing some or all
    arguments.
 
    usage: %s alias command [argument]*
    """
    def wrapped(wex, wargs, response):
        assert response in ['pickle', 'csv', 'text']
        try:
            return globals()[args[1]](wex, args[2:] + wargs, response)
        except CommandError as e:
            raise e
        except Exception, e:
            raise CommandError('%s: %s' % (type(e).__name__, e))
    wrapped.__doc__ = globals()[args[1]].__doc__
    commands[args[0]] = wrapped

Simple, clean, and Pythonic.

Aug 16th, 2012
Comments are closed.