Argh is a small library that provides several layers of abstraction on top of argparse. You are free to use any layer that fits given task best. The layers can be mixed. It is always possible to declare a command with the highest possible (and least flexible) layer and then tune the behaviour with any of the lower layers including the native API of argparse.
Assume we need a CLI application which output is modulated by arguments:
$ ./greet.py
Hello unknown user!
$ ./greet.py --name John
Hello John!
This is our business logic:
def main(name='unknown user'):
return 'Hello {0}!'.format(name)
That was plain Python, nothing CLI-specific. Let’s convert the function into a complete CLI application:
argh.dispatch_command(main)
Done. Dead simple.
What about multiple commands? Easy:
argh.dispatch_commands([load, dump])
And then call your script like this:
$ ./app.py dump
$ ./app.py load fixture.json
$ ./app.py load fixture.yaml --format=yaml
I guess you get the picture. The commands are ordinary functions with ordinary signatures:
Still, there’s much more to commands than this.
The examples above raise some questions, including:
Just read on.
You’ve already learned the natural way of declaring commands before even knowing about argh:
def my_command(alpha, beta=1, gamma=False, *delta):
return
When executed as app.py my-command --help, such application prints:
usage: app.py my-command [-h] [-b BETA] [-g] alpha [delta [delta ...]]
positional arguments:
alpha
delta
optional arguments:
-h, --help show this help message and exit
-b BETA, --beta BETA
-g, --gamma
The same result can be achieved with this chunk of argparse code (with the exception that in argh you don’t immediately modify a parser but rather declare what’s to be added to it later):
parser.add_argument('alpha')
parser.add_argument('-b', '--beta', default=1, type=int)
parser.add_argument('-g', '--gamma', default=False, action='store_true')
parser.add_argument('delta', nargs='*')
Verbose, hardly readable, requires learning another API.
Argh allows for more expressive and pythonic code because:
Hey, that’s a lot for such a simple case! But then, that’s why the API feels natural: argh does a lot of work for you.
Well, there’s nothing more elegant than a simple function. But simplicity comes at a cost in terms of flexibility. Fortunately, argh doesn’t stay in the way and offers less natural but more powerful tools.
The function’s docstring is automatically included in the help message. When the script is called as ./app.py my-command --help, the docstring is displayed along with a short overview of the arguments.
However, in many cases it’s a good idea do add extra documentation per argument.
In Python 3 it’s easy:
def load(path : 'file to load', format : 'json or yaml' = 'yaml'):
"Loads given file as YAML (unless other format is specified)"
return loaders[format].load(path)
Python 2 does not support annotations so the above example would raise a SyntaxError. You would need to add help via argparse API:
parser.add_argument('path', help='file to load')
...which is far from DRY and very impractical if the functions are dispatched in a different place. This is when extended declarations become useful.
When function signature isn’t enough to fine-tune the argument declarations, the arg decorator comes in handy:
@arg('path', help='file to load')
@arg('--format', help='json or yaml')
def load(path, format='yaml'):
return loaders[format].load(path)
In this example we have declared a function with arguments path and format and then extended their declarations with help messages.
The decorator mostly mimics argparse‘s add_argument. The name_or_flags argument must match function signature, that is:
The decorator doesn’t modify the function’s behaviour in any way.
Sometimes the function is not likely to be used other than as a CLI command and all of its arguments are duplicated with decorators. Not very DRY. In this case **kwargs can be used as follows:
@arg('number', default=0, help='the number to increment')
def increment(**kwargs):
return kwargs['number'] + 1
In other words, if **something is in the function signature, extra arguments are allowed to be specified via decorators; they all go into that very dictionary.
Mixing **kwargs with straightforward signatures is also possible:
@arg('--bingo')
def cmd(foo, bar=1, *maybe, **extra):
return ...
Note
It is not recommended to mix *args with extra positional arguments declared via decorators because the results can be pretty confusing (though predictable). See argh tests for details.
The default approach of argparse is similar to **kwargs: the function expects a single object and the CLI arguments are defined elsewhere.
In order to dispatch such “argparse-style” command via argh, you need to tell the latter that the function expects a namespace object. This is done by wrapping the function into the expects_obj() decorator:
@expects_obj
def cmd(args):
return args.foo
This way arguments cannot be defined in the Natural Way but the arg decorator works as usual.
Note
In both cases — **kwargs-only and @expects_obj — the arguments must be declared via decorators or directly via the argparse API. Otherwise the command has zero arguments (apart from --help).
Note
Argh decorators introduce a declarative mode for defining commands. You can access the argparse API after a parser instance is created.
After the commands are declared, they should be assembled within a single argument parser. First, create the parser itself:
parser = argparse.ArgumentParser()
Add a couple of commands via add_commands():
argh.add_commands(parser, [load, dump])
The commands will be accessible under the related functions’ names:
$ ./app.py {load,dump}
If the application has too many commands, they can be grouped into namespaces:
argh.add_commands(parser, [serve, ping], namespace='www',
title='Web-related commands')
The resulting CLI is as follows:
$ ./app.py www {serve,ping}
See Subparsers for the gory details.
The last thing is to actually parse the arguments and call the relevant command (function) when our module is called as a script:
if __name__ == '__main__':
argh.dispatch(parser)
The function dispatch() uses the parser to obtain the relevant function and arguments; then it converts arguments to a form digestible by this particular function and calls it. The errors are wrapped if required (see below); the output is processed and written to stdout or a given file object. Special care is given to terminal encoding. All this can be fine-tuned, see API docs.
A set of commands can be assembled and dispatched at once with a shortcut dispatch_commands() which isn’t as flexible as the full version described above but helps reduce the code in many cases. Please refer to the API documentation for details.
As you can see, with argh the CLI application consists of three parts:
This clear separation makes a simple script just a bit more readable, but for a large application this is extremely important.
Also note that the parser is standard. It’s OK to call dispatch() on a custom subclass of argparse.ArgumentParser.
By the way, argh ships with ArghParser which integrates the assembling and dispatching functions for DRYness.
There are cases when the application performs a single task and it perfectly maps to a single command. The method above would require the user to type a command like check_mail.py check --now while check_mail.py --now would suffice. In such cases add_commands() should be replaced with set_default_command():
def main():
return 1
argh.set_default_command(parser, main)
There’s also a nice shortcut dispatch_command(). Please refer to the API documentation for details.
Argparse takes care of generating nicely formatted help for commands and arguments. The usage information is displayed when user provides the switch --help. However argparse does not provide a help command.
Argh always adds the command help automatically:
- help shell → shell --help
- help web serve → web serve --help
See also #documenting-your-commands.
Most commands print something. The traditional straightforward way is this:
def foo():
print('hello')
print('world')
However, this approach has a couple of flaws:
- it is difficult to test functions that print results: you are bound to doctests or need to mess with replacing stdout;
- terminals and pipes frequently have different requirements for encoding, so Unicode output may break the pipe (e.g. $ foo.py test | wc -l). Of course you don’t want to do the checks on every print statement.
Good news: if you return a string, Argh will take care of the encoding:
def foo():
return 'привет'
But what about multiple print statements? Collecting the output in a list and bulk-processing it at the end would suffice. Actually you can simply return a list and Argh will take care of it:
def foo():
return ['hello', 'world']
Note
If you return a string, it is printed as is. A list or tuple is iterated and printed line by line. This is how dispatcher works.
This is fine, but what about non-linear code with if/else, exceptions and interactive prompts? Well, you don’t need to manage the stack of results within the function. Just convert it to a generator and Argh will do the rest:
def foo():
yield 'hello'
yield 'world'
Syntactically this is exactly the same as the first example, only with yield instead of print. But the function becomes much more flexible.
Hint
If your command is likely to output Unicode and be used in pipes, you should definitely use the last approach.
Usually you only want to display the traceback on unexpected exceptions. If you know that something can be wrong, you’ll probably handle it this way:
def show_item(key):
try:
item = items[key]
except KeyError as error:
print(e) # hide the traceback
sys.exit() # bail out (unsafe!)
else:
... do something ...
print(item)
This works, but the print-and-exit tasks are repetitive; moreover, there are cases when you don’t want to raise SystemExit and just need to collect the output in a uniform way. Use CommandError:
def show_item(key):
try:
item = items[key]
except KeyError as error:
raise CommandError(error) # bail out, hide traceback
else:
... do something ...
return item
Argh will wrap this exception and choose the right way to display its message (depending on how dispatch() was called).
Decorator wrap_errors() reduces the code even further:
@wrap_errors([KeyError]) # catch KeyError, show the message, hide traceback
def show_item(key):
return items[key] # raise KeyError
Of course it should be used with care in more complex commands.
The decorator accepts a list as its first argument, so multiple commands can be specified. It also allows plugging in a preprocessor for the catched errors:
@wrap_errors(processor=lambda excinfo: 'ERR: {0}'.format(excinfo))
def func():
raise CommandError('some error')
The command above will print ERR: some error.