The command line interface tools module provides several decorators
which can be applied to a regular function to turn it into a console
script. It is designed to adapt a function so that it can be used as
a console_scripts
entrypoint. The decorators allow various
command line arguments to be declared, and for the command line to be
parsed using the argparse
module; the results of the parsing are
then passed to the function as regular keyword arguments. This does
not interfere with the normal calling conventions of the function; it
can be called from Python code directly.
The simplest example of using the cli_tools
decorators is as
follows:
from cli_tools import * @console def function(): """ Performs an action. """ ...
In this declaration, the function is defined as taking no arguments
(except argparse
's default of "--help"). The description of the
resulting script will be "Performs an action."
To declare this as an actual console script, the following clause will
need to be added to the setup()
call in your setup.py:
entry_points={ 'console_scripts': [ 'function = your_module:function.console', ], }
Notice in particular the ".console" appended to the function name.
The decorators add several attributes to the function, including the
callable console()
, which performs the actual command line
argument parsing.
The above example is the simplest example, but it would be more interesting with some defined arguments:
@argument('--debug', '-d', dest='debug', action='store_true', default=False, help="Run the tool in debug mode.") @argument('--dryrun', '--dry_run', '--dry-run', '-n', dest='dry_run', action='store_true', default=False, help="Perform a dry run.") def function(dry_run=False): """ Performs an action. """ ...
The first thing to notice is the elimination of the @console
decorator. It doesn't hurt anything to use @console
, but all the
decorators perform the same core actions; as long as one of the other
decorators is used, @console
is unnecessary.
The second thing to notice is that the dest
specified for the
"--dryrun" option matches the only function argument. When run as a
console script, the value computed from the command line arguments
will be passed as this keyword parameter.
The third thing to notice is that the dest
specified for the
"--debug" option matches no function arguments. That flag will simply
not be passed to the function.
(As it happens, the debug
argument is treated specially. Under
normal circumstances, if the function raises an exception, the
exception is coerced to a string, printed to standard error, and then
the console script exits. If the debug
argument is True
,
however, the exception will not be caught, resulting in a print out of
the stack trace.)
By default, the first paragraph of the function docstring becomes the
description for the console script, which is printed out when the
"--help" option is given. This and several other argparse
options
may be overridden using the following decorators:
- @prog()
- Overrides the
prog
parameter passed toargparse.ArgumentParser
. By default, it will beNone
, causing the program name to be derived fromsys.argv[0]
. - @usage()
- Overrides the
usage
parameter passed toargparse.ArgumentParser
. By default, it will beNone
, causing the usage message to be automatically computed byargparse
. - @description()
- Overrides the
description
parameter passed toargparse.ArgumentParser
. By default, it will be the first paragraph of the function docstring. - @epilog()
- Overrides the
epilog
parameter passed toargparse.ArgumentParser
. By default, it will beNone
; when given, the text will be output at the end of the help text. - @formatter_class()
- Overrides the
formatter_class
parameter passed toargparse.ArgumentParser
. By default, it will beargparse.HelpFormatter
. See theargparse
documentation for more details.
The argparse
package provides the ability to group arguments.
There are two ways of grouping arguments; in the first, related
arguments are simply grouped together so their documentation is more
easily found, while in the second, a group of arguments are identified
as mutually exclusive. The cli_tools
decorators accommodate this
by adding a special group
parameter to the @argument()
decorator; this group name identifies a group added using the
@argument_group()
or @mutually_exclusive_group()
decorators,
and must be unique. These latter two decorators take the group name
as the first argument, and remaining keyword arguments are passed to
the underlying argparse.ArgumentParser.add_argument_group()
and
argparse.ArgumentParser.add_mutually_exclusive_group()
methods.
Arguments and groups are constructed in the order in which they appear in the file; that is, in the earlier example, the "--debug" option will be added to the argument parser before the "--dryrun" option. This is opposite the normal decorator rules, but simplifies setting up the arguments, particularly positional arguments.
Some functions can't act as stand-alone console scripts without some sort of setup. For instance, it may be necessary to configure logging. This can be handled by declaring a processor:
@console def function(): """ Performs an action. """ ... @function.processor def _processor(args): logging.basicConfig()
Here we declare the function _processor()
as a processor for the
console script function()
. After the command line arguments are
parsed, _processor()
will be called with those arguments; after it
returns, function()
will be called.
It is also possible to perform actions after the function returns. Consider the following example:
@console def function(): """ Performs an action. """ ... return result @function.processor def _processor(args): result = yield print result yield None
Here we turn _processor()
into a generator; the result of the
first yield
statement is the return value of function()
, which
we can see will be whatever result it computed. Thus,
_processor()
will print out that result, then yield None
--this
is needed so that the script exits without any errors; a non-None
value is interpreted as an error condition by the machinery
surrounding the console_scripts
endpoint.
Generator-based processors also receive any exceptions thrown by the function, like so:
class BailoutException(Exception): pass @console def function(): """ Performs an action. """ ... raise BailoutException("I'm done") @function.processor def _processor(args): try: result = yield except BailoutException: print "All done!" else: print "Results so far: %s" % result yield None
Note the try
block around the first yield
, which allows the
processor function to catch this special exception and do something
appropriate.
It may be necessary to arbitrarily manipulate the argument parser before parsing the command line arguments. For instance, a system which used pluggable authentication modules may need to allow those modules to add specific command line arguments. This can be handled by declaring an argument hook:
@console def function(): """ Performs an action. """ ... @function.args_hook def _hook(parser): parser.add_argument(...)
Here we declare the function _hook()
as an argument hook for the
console script function()
. After the declared arguments have been
added to the parser, the hook will be called with the parser (an
argparse.ArgumentParser
instance), which it can manipulate in any
way.
It is also possible to manipulate the parser prior to adding the declared arguments. Consider the following example:
@console def function(): """ Performs an action. """ ... @function.args_hook def _hook(parser): parser.add_argument(...) yield
Here we turn _hook()
into a generator. The statements preceding
the first yield
statement will be run immediately before adding
the declared arguments, and can manipulate the parser in any way
necessary. If manipulation needs to be done after the declared
arguments are added, that can be done in statements following the
yield
statement.
The console()
function added to the decorated function uses
several other functions for setting up the argument parser
(setup_args()
), building the keyword arguments to pass to the
underlying function (get_kwargs()
), and safely calling the
function and handling exceptions (safe_call()
). These functions
are provided to allow other consumers to make use of the argument
information. This could be used to build a "Swiss army knife" command
interpreter, for instance.
In fact, such "Swiss army knife" command interpreters are supported
directly by cli_tools
, through the use of such decorators as
@subparsers()
, @load_subcommands()
, and the @subcommand()
argument parser decorator.
We begin by showing how to directly declare one function as a subcommand of another:
@console def function(): """ Performs an action. """ pass @function.subcommand def subcmd1(): """ Performs subcmd1. """ ... @function.subcommand('sub2') def subcmd2(): """ Performs sub2. """ ...
In this example, we have defined two subcommands. The subcommand
defined by subcmd1()
has a name derived from the function name,
while the subcommand defined by subcmd2()
has its name explicitly
set to "sub2".
To introspect the declared subcommands, use the get_subcommands()
function which is also added to the decorated function. The
get_subcommands()
function returns a dictionary mapping the
subcommand name to the function which implements that subcommand. For
instance, in the example above, function.get_subcommands()
would
return the dictionary {"subcmd1": subcmd1, "sub2": subcmd2}
.
Note that, when using subcommands, the original function will never be
called. If no subcommand is passed on the command line, the
underlying argparse
module reports an error.
It is also possible to load subcommands using a pkg_resources
entrypoint group, using the @load_subcommands()
decorator like
so:
@load_subcommands('example.subcommands') def function(): """ Performs an action. """ ...
In this example, all functions listed under the "example.subcommands"
entrypoint group will be added as subcommands of function()
, with
the subcommand name being set to the name of the entrypoint. For
instance, if the following entrypoint entries existed:
entry_points={ 'example.subcommands': [ 'subcmd1 = your_module:subcommand1', 'subcmd2 = your_module:subcommand2', 'subcmd3 = other_module:subcommand3', ], }
Then in the example above, the three subcommands "subcmd1", "subcmd2", and "subcmd3" would be defined. (Carefully note that these entrypoints are not followed by the ".console" that was required in the "console_scripts" entrypoint.)
As a final point, subcommands are handled by calling the
argparse.ArgumentParser.add_subparsers()
method. This method can
take certain keyword arguments for nicer rendering of the help text;
to set these arguments, use the @subparsers()
decorator:
@subparsers(title="My subcommands") def function(): """ Perform an action. """ ...
The command line interface tools module does not provide any
integration with shell argument completion directly. However,
cli_tools
uses the argparse
module, and any argument
completion framework that works with argparse
can be used with it.
As an example, consider the argcomplete
module; here's an example
of how it might be integrated into a cli_tools
-compatible CLI:
from cli_tools import * import argcomplete # PYTHON_ARGCOMPLETE_OK @argument('--debug', '-d', dest='debug', action='store_true', default=False, help="Run the tool in debug mode.") @argument('--dryrun', '--dry_run', '--dry-run', '-n', dest='dry_run', action='store_true', default=False, help="Perform a dry run.") def function(dry_run=False): """ Performs an action. """ ... @function.args_hook def _hook(parser): argcomplete.autocomplete(parser)
Note the use of an argument hook to invoke the
argcomplete.autocomplete()
function; for argcomplete
, this
performs the actual argument completion. Also note the comment
containing PYTHON_ARGCOMPLETE_OK
, which enables argcomplete
's
global completion mode.
For more information about argcomplete
, see:
https://pypi.python.org/pypi/argcomplete