Skip to main content

Command Line Interface Tools

Project description

https://travis-ci.org/klmitch/cli_tools.svg?branch=master

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.

Simple usage of cli_tools

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.)

Getting a Little More Advanced: Set the Description

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 to argparse.ArgumentParser. By default, it will be None, causing the program name to be derived from sys.argv[0].

@usage()

Overrides the usage parameter passed to argparse.ArgumentParser. By default, it will be None, causing the usage message to be automatically computed by argparse.

@description()

Overrides the description parameter passed to argparse.ArgumentParser. By default, it will be the first paragraph of the function docstring.

@epilog()

Overrides the epilog parameter passed to argparse.ArgumentParser. By default, it will be None; when given, the text will be output at the end of the help text.

@formatter_class()

Overrides the formatter_class parameter passed to argparse.ArgumentParser. By default, it will be argparse.HelpFormatter. See the argparse documentation for more details.

Getting More Advanced: Argument Groups

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.

Argument Declaration Order

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.

Processors

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.

Argument Hooks

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.

Advanced cli_tools Usage

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.
    """
    ...

Argument Completion

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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cli_tools-1.0.1.tar.gz (27.6 kB view details)

Uploaded Source

File details

Details for the file cli_tools-1.0.1.tar.gz.

File metadata

  • Download URL: cli_tools-1.0.1.tar.gz
  • Upload date:
  • Size: 27.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: Python-urllib/2.7

File hashes

Hashes for cli_tools-1.0.1.tar.gz
Algorithm Hash digest
SHA256 769e09cf976918e17f76033511e8a47fb3c2732129141e41ba0448a1e78e08ea
MD5 b2784462a7ffd84a85f6d87b8f8ca8d4
BLAKE2b-256 496855e3220ed46962654bd2db804cd19c30739b284b823a683d95946a114ec5

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page