Nesting Commands#

Objects of the DecreeTree class can be nested to create a tree of commands. This page describes the means to do so, as well as the ways in which nesting can be customized. For a basic example, see here.

Goals#

DecreeTree is a wrapper around argparse , and as such, it can create CLIs that align with the capabilities of that standard library. By default, DecreeTree (sub)commands are expected to either: accept options and arguments; or a child subcommand name. This default can be customized – see Subparser Customization below.

command [all subcommand names] [options] [positional arguments]

This contrasts with the form below, where options and positional arguments are interspersed with subcommands. That form is not explicitly supported by decree-tree at present.

command [subcommand1] [s1 options] [s1 positional arguments] [subcommand2] [s2 options] [s2 positional arguments] [...]

These goals may differ from that of alternative CLI tools. See Discussion for a summary of some alternatives, as well as some limitations of argparse.

Creating a Tree#

The DecreeTree class can be nested to form a tree of commands. Such a tree is constructed from instantiated DecreeTree objects, using any of the tree manipulation methods it makes available. Essentially, each DecreeTree can be used as a node in a tree structure. When this occurs, each will have one parent (except for the root with none), and an ordered list of subcommand children.

The easiest way to assemble DecreeTree commands into a tree is to use the add() method, which appends a subcommand to the list of children for the parent command. For example, if Root, Foo, Bar, Baz, and Qux are all DecreeTree subclasses, imported from a module named nesting, then a tree structure can be created like this:

root = Root()
root.add(Foo)
bar = root.add(Bar)
bar.add(Baz)
qux = Qux(version='1.0')
bar.add(qux)
print(root.structure)

This results in printing the structure of the tree for the root command:

root: nesting.Root
root -> foo: nesting.Foo
root -> bar: nesting.Bar
root -> bar -> baz: nesting.Baz
root -> bar -> qux: nesting.Qux

Noteworthy aspects of this example include:

  • A top-level (“root”) DecreeTree must be instantiated.

  • DecreeTree classes can be passed directly to the add method. These are instantiated with no arguments as they are added.

  • Instantiated DecreeTree objects can be passed to the add method, allowing arguments to be passed to the class constructor methods.

  • For convenience, the add method returns the child command, regardless of where it was instantiated. As usual, this return value can be ignored.

Be aware that add() is actually just a renamed convenience wrapper of the append_child() method. Both function identically. This is similar to another important method, get(). This is a wrapper for get_child() and can be used to retrieve nested children from any command node in the tree:

root = Root()
root.add(Foo)
bar = root.add(Bar)
baz = bar.add(Baz)
qux_1 = baz.add(qux)
qux_2 = bar.get('baz', 'qux')
assert qux_1 is qux_2

The assertion in the above code snippet succeeds. Rather than the class names, the arguments to get are the “names” of the commands, as documented in Command Names. This behavior allows the same command class to fall in multiple places on the tree.

In addition to add and get, DecreeTree includes a full set of methods for manipulating its children, based on the built-in Python methods for list. These include insert_child(), remove_child(), clear_children(), and more. See the DecreeTree Class API for a full list of the available tree-related methods and properties.

Inheritance#

It is frequently useful to reuse the definition of CLI options or arguments, as well as processing that is shared among subcommands. For example, this could include settings, or setup/cleanup code for several subcommands. Using a class to define each command makes this easy. DecreeTree can be customized in several ways, including via manual inheritance and abstract parent classes.

Manual Inheritance#

One way of inheriting functionality is achieved simply by nesting DecreeTree objects together in a tree. By default, each DecreeTree will invoke several methods from its parent in the tree. The “manually inherited” so invoked are add_arguments(), process_options(), and execute(). These calls occur just prior to the call to their super() methods. See the basic tree example for a manually inherited command tree.

This behavior is governed by the inherit attribute. If desired, the easiest way to disable manual inheritance of parent class functionality for a child is to pass inherit=False into the constructor for the child command. In the example referenced above, inherit=False could have been passed when instantiating BasicCommand via root.add(BasicCommand(inherit=False)). If so, the invoked command would have resulted in an error, since the -e option would not be available to the basic_command subcommand.

$ python basic_tree_inherit_false.py basic_command -e --upper "foo bar"
usage: basic_tree_inherit_false.py basic_command [-h] [--upper] value
basic_tree_inherit_false.py basic_command: error: unrecognized arguments: -e

Here the [-e] option is correctly identified as not being available to the basic_command subcommand, as it was not inherited. In general, the use of manual inheritance is recommended when possible.

Abstract Parent Classes#

Another way to approach inheritance is to use standard Python class inheritance. An “Abstract” class can be defined containing shared code, and child classes can use the abstract class as a parent. See the following example:

abstract_tree.py#
from decree_tree import DecreeTree

class Abstract(DecreeTree):
    """An abstract DecreeTree."""

    # Set these to ensure this class is abstract
    name = ''
    help = ''

    def add_arguments(self, parser):
        super().add_arguments(parser)
        parser.add_argument('-e', '--extra', action='store_true',
                            help='output extra text')

    def execute(self):
        super().execute()
        if self.options.extra:
            print("Extra output")

class Root(Abstract):
    """An example command tree."""

class Echo(Abstract):
    """Echo the input."""

    def add_arguments(self, parser):
        super().add_arguments(parser)
        parser.add_argument('input', help='the input to echo')

    def execute(self):
        super().execute()
        print(self.options.input)

class Other(Abstract):
    """Do something else..."""

root = Root()
root.add(Echo(inherit=False))
root.add(Other(inherit=False))

if __name__ == '__main__':
    root.run()

In Abstract, the name and help attributes were explicitly set as empty strings. This is what circumvents normal processing and makes the class abstract. Invoking one of the subcommands also invokes the abstract class code:

$ python abstract_tree.py echo --extra "foo bar"
Extra output
foo bar

We can see that the --extra option is available for both the root…

$ python abstract_tree.py -h
usage: abstract_tree.py [-h] {echo,other} ...

An example command tree.

options:
  -h, --help    show this help message and exit

subcommands:
  {echo,other}
    echo        Echo the input.
    other       Do something else...

… and the child commands.

$ python abstract_tree.py echo -h
usage: abstract_tree.py echo [-h] [-e] input

Echo the input.

positional arguments:
  input        the input to echo

options:
  -h, --help   show this help message and exit
  -e, --extra  output extra text

The decree_tree.extras.AbstractDT is an example of an abstract class defined by decree-tree. Any class inheriting from that one will be itself considered abstract, as handle conflicting options in a slightly different manner. See Deconfliction below for more info.

Deconfliction#

Since the example with abstract_tree.py above used inherit=False, the fact that the --extra option was defined in classes at multiple levels of the tree did not cause a conflict, and no attempt was made to add it to the resulting parser multiple times. However, there may be scenarios where a particular argument is added to an argparse.ArgumentParser multiple times, due to the use of inheritance. This typically occurs when manual and abstract class inheritance are combined.

To resolve this, first try to use a combination of the inherit attribute and restructuring of the commands. If that approach isn’t workable, consider setting a 'conflict_handler' key to 'resolve' in the return value from a command’s parser_options() method. This will set the corresponding flag when the subparser is created by argparse . A simple way to accomplish this is to create commands that inherit from the decree_tree.extras.AbstractDT helper class, which both declares its direct children as abstract, and sets 'conflict_handler' appropriately.

Design History#

A conceptual prototype of decree-tree was designed upon the concept of using a class hierarchy to represent the tree structure of a nested command. This allowed very lightweight package code and didn’t require class instantiation by users. Inner classes could also be included, with some mro manipulation.

However, it was found that this approach was not sufficiently flexible. It was challenging to change the tree after creation of the class hierarchy. Relatedly, it was also not easy to dynamically exclude subclasses from the tree. And there could only be one tree that included a given command class, which limited code reuse.

Rather than using this approach, decree-tree allows for flexible tree customization, while allowing inheritance at both the class and object levels.

Alternate Roots#

While a full tree is typically used, by invoking root.run() if root is the top-level DecreeTree, it is possible to only use part of the tree. This can be useful if multiple shell commands are used that tie into different parts of the same command tree. For example, consider the following script that calls run() on the top level (“root”) parent command:

run_parent.py#
from decree_tree import DecreeTree

class Parent(DecreeTree):
    """Parent command docstring"""

class Child(DecreeTree):
    """Child command docstring"""

class Grandchild(DecreeTree):
    """Grandchild command docstring"""

parent = Parent()
child = parent.add(Child)
child.add(Grandchild)
parent.run()

If this is invoked, we see that the parent object is used:

$ python run_parent.py -h
usage: run_parent.py [-h] {child} ...

Parent command docstring

options:
  -h, --help  show this help message and exit

subcommands:
  {child}
    child     Child command docstring

However, it is also possible to create. In the run_child.py file, parent.run() has been replaced with child.run(). When this altered script is executed, we see that only the part of the command tree rooted in child is called:

$ python run_child.py -h
usage: run_child.py [-h] {grandchild} ...

Child command docstring

options:
  -h, --help    show this help message and exit

subcommands:
  {grandchild}
    grandchild  Grandchild command docstring

This works for arbitrary levels of DecreeTree hierarchies.

Subparser Customization#

It is possible to alter the behavior of the parsers used for the subcommands. See Parsers and Subparsers for details.

Dynamic Tree Creation#

While it is possible to assemble a tree dynamically in a “lazy” manner, decree-tree does not presently provide specific capabilities that facilitates this process.