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 theadd
method. These are instantiated with no arguments as they are added.Instantiated
DecreeTree
objects can be passed to theadd
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.