Advanced Usage

Dynamic Parameter Defaults

In most cases, a simple default value for a given Parameter is sufficient, but sometimes it can be helpful to dynamically generate a default based on the runtime environment or the value of other parsed arguments.

Most Parameters that support a using a default_cb also support registering a method in a Command as their default callback. A very simple example that references another Parameter:

class MyCommand(Command):
    foo = Flag('-f')
    bar = Option('-b')

    @bar.register_default_cb
    def _bar_default_cb(self):
        return str(self.foo)

In the above example, if --bar baz was provided, then within MyCommand, self.bar would be 'baz'. If no value was provided, then it would be the string 'True' or 'False', depending on whether --foo was specified.

Post-Run & Context

While Commands are intended to be self-contained, it is possible to interact with them after calling Command.parse_and_run(), which returns the instance of the executed Command. Example:

>>> class Foo(Command):
...     bar = Flag('--no-bar', '-B', default=True)
...     baz = Positional(nargs='+')
...
...     def main(self):
...         print(f'{self.bar=}, {self.baz=}')
...

>>> foo = Foo.parse_and_run(['test', 'one', '-B'])
self.bar=False, self.baz=['test', 'one']

>>> foo
<__main__.Foo at 0x26dfcad6e00>

Parameter values are still accessible, as they were from inside the command:

>>> foo.bar
False

While it is also accessible while running, it’s easier to inspect the parsing Context in an interactive terminal after parsing and running:

>>> foo.ctx
<lib.cli_command_parser.context.Context at 0x26dfa94fbb0>

>>> foo.ctx.params
<CommandParameters[command=Foo, positionals=1, options=2]>

>>> foo.ctx.get_parsed()
{
    'baz': ['test', 'one'],
    'bar': False,
    'help': False
}

>>> foo.ctx.argv
['test', 'one', '-B']

The Context object stores information about the Command’s configuration, the parameters defined in that Command (organized in a CommandParameters object), the input, and a dictionary containing the parsed values. The help entry in the above example is the automatically added ActionFlag that represents the --help action:

>>> foo.ctx.params.action_flags
[ActionFlag('help', action='store_const', const=True, default=False, required=False, help='Show this help message and exit', order=-inf, before_main=True)]

>>> foo.ctx.params.action_flags[0].func
<function lib.cli_command_parser.actions.help_action(self)>

The name is automatically generated to avoid potential name conflicts with other parameters / methods in Commands. It will not always have the same number in the name.

Since its ActionFlag.order is negative infinity, the help_action() will always take precedence over any other ActionFlag. There is special handling in the parser for specifically allowing that action to be processed when parsing would otherwise fail.

Accessing Raw Argument Values

Parsed Args as a Dictionary

A get_parsed() helper function exists for retrieving a dictionary of parsed arguments without needing to deal with the ctx attribute like in the above example. The get_parsed helper function will continue to work, even if a given command overrides the ctx attribute with a different value.

Example using the same Command as above:

>>> get_parsed(foo)
{
    'baz': ['test', 'one'],
    'bar': False,
    'help': False
}

As an added convenience, this helper function accepts a collections.abc.Callable object to filter the parsed dict to only the keys that match that callable’s signature. Only VAR_KEYWORD parameters (i.e., **kwargs) are excluded - if any parameters of the given callable cannot be passed as a keyword argument, that must be handled after calling get_parsed.

Example:

>>> def test(bar, **kwargs):
...     pass
...

>>> get_parsed(foo, test)
{'bar': False}

Parameters with Overridden Names

In some cases, subcommands may have Parameters with names that override those defined in parent Commands. A common example of this occurs when multiple levels of subcommands exist, where each level has a sub_cmd = SubCommand().

In such cases, it is sometimes necessary for a parent Command to know the raw parsed value for that Parameter. The get_raw_arg() function simplifies the process of accessing that value.

Given the following simplified example Commands:

class Foo(Command):
    sub_cmd = SubCommand()

class Bar(Foo):
    sub_cmd = Positional()

We can see that accessing the sub_cmd attribute directly returns the parsed subcommand’s result:

>>> cmd = Foo.parse(['bar', 'baz'])

>>> cmd.sub_cmd
'baz'

The raw parsed value for both levels can be retrieved using get_raw_arg():

>>> get_raw_arg(cmd, Foo.sub_cmd)
['bar']

>>> get_raw_arg(cmd, Bar.sub_cmd)
'baz'

Note that the raw value for some Parameters like SubCommand may be a list instead of a string. This is due to the way that values containing spaces are supported.

From within a Command instance method, self would be used instead of the cmd variable from the above examples. E.g.:

def main(self):
    value = get_raw_arg(self, Foo.sub_cmd)
    print(value)

Alternatively, it is possible to define Parameters with double-underscore names to take advantage of native name mangling. Doing do results in direct access within a given Command returning the raw value that was parsed at that level. Example:

>>> class Foo(Command):
...     __sub_cmd = SubCommand()
...     def _init_command_(self):
...         print(f'Foo: {self.__sub_cmd}')
...
... class Bar(Foo):
...     __sub_cmd = Positional()
...     def main(self):
...         print(f'Bar: {self.__sub_cmd}')
...

>>> Foo.parse_and_run(['bar', 'baz'])
Foo: bar
Bar: baz

In the above example, if __sub_cmd had been named sub_cmd instead, then the output would have been:

Foo: baz
Bar: baz

Mixing Actions & ActionFlags

The build_docs.py script that is used to build the documentation for this project is an example of a Command that includes both Action methods and ActionFlags. Additionally, some of the methods even have the two decorators stacked so that they can be called either way.

Example snippet:

class BuildDocs(Command, description='Build documentation using Sphinx'):
    action = Action()
    verbose = Counter('-v', help='Increase logging verbosity (can specify multiple times)')
    dry_run = Flag('-D', help='Print the actions that would be taken instead of taking them')

    def __init__(self):
        # Initialize logging, etc
        ...

    @action(default=True, help='Run sphinx-build')
    def sphinx_build(self):
        # Call sphinx-build in a subprocess
        ...

    @before_main('-c', help='Clean the docs directory before building docs', order=1)
    @action(help='Clean the docs directory')
    def clean(self):
        # Clean up the build dir to remove old generated RST files / HTML
        ...

    @before_main('-u', help='Update RST files', order=2)
    def update(self):
        # Re-generate RST files for API docs
        ...

    @after_main('-o', help='Open the docs in the default web browser after running sphinx-build')
    def open(self):
        ...

    @action('backup', help='Test the RST backup')
    def backup_rsts(self):
        # Backup the existing auto-generated RST files
        ...

The help text (note that clean appears in both the Actions section and the optional args section):

$ build_docs.py -h
usage: build_docs.py {clean,backup} [--verbose [VERBOSE]] [--dry-run] [--clean] [--update] [--open] [--help]

Build documentation using Sphinx

Actions:
  {clean,backup}
    (default)                 Run sphinx-build
    clean                     Clean the docs directory
    backup                    Test the RST backup

Optional arguments:
  --verbose [VERBOSE], -v [VERBOSE]
                              Increase logging verbosity (can specify multiple times) (default: 0)
  --dry-run, -D               Print the actions that would be taken instead of taking them
  --clean, -c                 Clean the docs directory before building docs
  --update, -u                Update RST files
  --open, -o                  Open the docs in the default web browser after running sphinx-build
  --help, -h                  Show this help message and exit

If the script is called with build_docs.py clean or build_docs.py backup, then only the clean or backup method would be called, respectively. If neither action was specified, then the sphinx_build method would be called because it is marked as the default action (@action(default=True, ...).

When called without a positional action, but with action flags specified, then each of the methods enabled via specified flags and sphinx_build will be called. For example, running build_docs.py -uco would result in the following methods being called in the following order:

  • clean (before main, order=1)

  • update (before main, order=2)

  • sphinx_build (main, default action)

  • open (after main)

Higher order values result in being called later, when specified.

It is technically possible to call the same method both via action and flag, such as build_docs.py clean -c. Nothing in this library will prevent that. If this is problematic, but you want to stack decorators like this, then you should include a check in your application to prevent it from being run twice.