Nesting Commands ################ Objects of the :class:`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 :ref:`here `. Goals ***** :class:`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 :ref:`nesting:Subparser Customization` below.) .. code:: 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``. .. code:: command [subcommand1] [s1 options] [s1 positional arguments] [subcommand2] [s2 options] [s2 positional arguments] [...] These goals may differ from that of alternative CLI tools. See :doc:`discussion` for a summary of some alternatives, as well as some limitations of ``argparse``. Creating a Tree *************** The :class:`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. .. testsetup:: trees # Originate classes in a distinct file for __class__.__module__ import os import sys current_dir = os.getcwd() new_dir = os.path.join(current_dir, 'source') new_dir = os.path.join(new_dir, 'code') sys.path.append(new_dir) from nesting import Root, Foo, Bar, Baz, Qux The easiest way to assemble ``DecreeTree`` commands into a tree is to use the :meth:`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: .. testcode:: trees 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 :meth:`structure ` of the tree for the root command: .. testoutput:: trees 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 :meth:`add ` is actually just a renamed convenience wrapper of the :meth:`append_child ` method. Both function identically. This is similar to another important method, :meth:`get `. This is a wrapper for :meth:`get_child ` and can be used to retrieve nested children from any command node in the tree: .. testcode:: trees 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 :ref:`configuration: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 :meth:`insert_child `, :meth:`remove_child `, :meth:`clear_children `, and more. See the :doc:`decreetree` 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. :class:`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 :class:`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 :meth:`DecreeTree.add_arguments `, :meth:`DecreeTree.process_options `, and :meth:`DecreeTree.execute `. These calls occur just prior to the call to their ``super()`` methods. See the :ref:`basic tree example ` for a manually inherited command tree. This behavior is governed by the :attr:`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. .. shtest:: :cwd: code :returncode: 2 :stderr: $ 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: .. FIXME include info about empty name attr str .. literalinclude:: code/abstract_tree.py :language: python :caption: ``abstract_tree.py`` :lines: 4- Invoking one of the subcommands also invokes the abstract class code: .. shtest:: :cwd: 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... .. shtest:: :cwd: code $ 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. .. shtest:: :cwd: code $ 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 :attr:`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 :meth:`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 :class:`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 :meth:`DecreeTree.run ` on the top level ("root") parent command: .. literalinclude:: code/run_parent.py :language: python :caption: ``run_parent.py`` :lines: 4- If this is invoked, we see that the ``parent`` object is used: .. shtest:: :cwd: code $ 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: .. shtest:: :cwd: code $ 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 :meth:`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 :meth:`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.