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