Concepts
Contrary to what the Basic Usage example might have you believing, multicommand
doesn't quite treat all modules in the subpackage passed to multicommand.create_parser(subpkg)
eqaully. In Fact, there are a few things to keep in mind in order to make using multicommand
as smooth as possible.
Throughout the remainder of this discussion assume we're working in a package called mypkg
, with an entrypoint (module + function) called cli.py
and a subpackage called commands
to house our parsers.
I.e. somewhere we're calling multicommand.create_parser(mypkg.commands)
.
parser
#
The module-level #
The variable nameEach module in commands/
- at least, the ones you want multicommand to find - must export (which, in python, really just means "must define") a module-level parser
variable. Specifically, for every module in commands/
that you want multicommand to use in the construction of the main root parser, must have a line something like:
So the following "exports" would be ignored by multicommand
#
The variable valueIn addition to requiring a module-level parser
variable, this variable must point at an instance of argparse.ArgumentParser
(or an instance of a subclass of argparse.ArgumentParser
).
That is, in addition so having parser = ...
somewhere in the module, it should point at an appropriate instance.
So the following are non-examples:
However, if the object pointed to by parser
does inherit from argparse.ArgumentParser
, it really doesn't matter how it came about, for example something like this would work:
This segues nicely to the next concept.
#
A "parser module"It's probably desirable to structure each module under commands/
consistently. In general, having consistent coding habits is less of a statement toward multicommand usage and more so just a general programming best practice.
That said, if you have no opinions or are open to suggestions, a module structure something like the following might be useful to adopt:
The key take-aways from the above snippet are (in order of appearance):
Prefer absolute imports - when importing objects from the local package absolute imports are far easier to understand and digest than explicit relative imports, especially for deeply nested commands where you might have to go far "up the tree" and end up with something like
from ....subpkg.submodule import MyClass
.They're also more portable, and with the combination of
multicommand
and absolute imports, it means you can easily move/alter your command hierarchy just by moving files, whereas explicit relative imports would (likely) break after moving a module.Create a parser factory function (or class) - This has two benefits.
First, it nicely encapsulates the parser creation behind and easy-to-call function (or easy-to-instantiate class).
Second, this encapsulation means it's easier to test your parsers. Need an instance of your parser? Call
create_parser
. Need another? Callcreate_parser
again!Embrace some kind of handler semantics - As in the above example we define a handler function and link this function to the parser in the parser factory function. This has two benefits.
First, the handlers become the spots to look for where command line arguments are converted into python objects and subsequently passed to the packages python API.
Second, by using some kind of consistent handler structure among modules we can simplify the entrypoint considerably. If we use the
handler
-style function defined above across all our parser modules, we can do something like this for the entrypoint:mypkg/cli.py
#
Terminal ParsersA terminal parser is any parser which comes from (is defined in):
- An importable module (i.e. a file ending in
.py
) - The module is located in the
commands/
(or similar) sub-package - The module is not named
_index.py
These parsers basically map directly to some specific functionality of the CLI and "end there". That is, there are no further subcommands (hence the name terminal parsers). If git
was implemented with multicommand
the parsers defining the git add
and git commit
commands could probably be terminal parsers.
In many (most?) cases terminal parsers are all you need. especially if the topology of the CLI is simple and/or only "one level deep", for example:
The above CLI might manifest itself with the following package structure:
#
Index ParsersYou may have noticed something in the Bonus section of the Basic Usage document. Specifically, take a look at the help produced by running python -m mypkg.cli topic cmd --help
(reproduced here):
Notice the descriptions of the subcommands
: ungreet
has a description, but subcmd
doesn't. Both ungreet
and subcmd
are valid substitutions for [command]
(above). So how do we add a description for subcmd
and how can we even target that command? It's represented as a directory (sub-package) after all!
As mentioned in the Motivation section of the Introduction, multicommand
aims to simplify authoring nested CLIs by exploiting the duality between the filesystem hierarchy and command hierarchy. In a filesystem there are files, but there are also directories! Well, basically everything's a file in linux filesystems, and there are more than two types of files, but for this analogy we only need to know that there are (at least) two types of objects in the tree.
I mention files and directories because in the same way that directories act as a parent node for files (and other nested directories) in a filesystem, index parsers act as the parent node for terminal parsers (and other nested commands).
So, an index parser is any parser which comes from (is defined in):
- An importable module named
_index.py
, - The module is located in the
commands/
(or similar) sub-package.
These are the only requirements. Circling back to our Basic Usage question above, we could add a description by adding the following index parser:
Adding this would yield:
#
Thoughts on index parsersTODO ...
- An index parser can "act like" a terminal parser.
- They should probably really only take options.
- Up to now we haven't needed to define any
_index.py
modules, so what gives?