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 [subcommand names] [options] [positional arguments]

Note that argparse does not allow multiple subparsers for a command, so commands that take the following form are not easily constructed with decree-tree.

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 DecreeTree.add_arguments, DecreeTree.process_options, and DecreeTree.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 DecreeTree.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 [-h] [-e] {basic_command,double} ...
basic_tree_inherit_false.py: error: unrecognized arguments: -e

Note that in this case, the error message is slightly confusing because the [-e] option appears in the usage statement. However, since that option was not inherited for the basic_command subcommand, it was not available there, and the usage statement is for the invoked “root” command (the one that executed run()). 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."""

    name = ''  # FIXME why?

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

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] [-e] {echo,other} ...

An example command tree.

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

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

Deconfliction#

Since this example above used inherit=False, there was no conflict and no attempt to add the --extra option being to a 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 DecreeTree.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 DecreeTree.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.

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 DecreeTree.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#

DecreeTree uses argparse._SubParsersAction objects to configure subcommand parsing. These are normally created via the add_subparsers method from an argparse parser. Each DecreeTree sets some default arguments for any necessary add_subparsers call in DecreeTree.configure_parser_tree.

One of the defaults is to set required=True. This means that invocation of a subcommand, if any are available at at particular level of a tree, is required. The expectation is that this behavior is typical. However, if this or other arguments to add_subparsers need to be modified, this can be accomplished by overriding DecreeTree.subparsers_options and passing the appropriate argument as a key-value pair in its return dict.

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.