jsonargparse
jsonargparse copied to clipboard
Add multilevel subcommands with class objects support to CLI
🚀 Feature request
I would like to request the ability to define multilevel subcommands using only class objects.
Motivation
Related: #334
We cannot define multilevel subcommands using class objects. While it's possible to define such subcommands using dictionaries, in this case, generating help from docstrings is not feasible (see below).
class Sub3:
"""Docstring for Sub3."""
def command3(self):
...
class Sub1:
"""Docstring for Sub1."""
def command1(self):
...
if __name__ == "__main__":
import jsonargparse
jsonargparse.CLI(
{
"sub1": Sub1,
"sub2": { # sub2 has no help string.
"sub3": Sub3,
}
}
)
$ python -m app -h
usage: app.py [-h] [--config CONFIG] [--print_config[=flags]] {sub1,sub2} ...
options:
-h, --help Show this help message and exit.
--config CONFIG Path to a configuration file.
--print_config[=flags]
Print the configuration after applying all other arguments and exit. The optional
flags customizes the output and are one or more keywords separated by comma. The
supported flags are: comments, skip_default, skip_null.
subcommands:
For more details of each subcommand, add it as an argument followed by --help.
Available subcommands:
sub1 Docstring for Sub1.
sub2
$ python -m app sub2 -h
usage: app.py [options] sub2 [-h] [--config CONFIG] [--print_config[=flags]] {sub3} ...
dict() -> new empty dictionary
options:
-h, --help Show this help message and exit.
--config CONFIG Path to a configuration file.
--print_config[=flags]
Print the configuration after applying all other arguments and exit. The optional
flags customizes the output and are one or more keywords separated by comma. The
supported flags are: comments, skip_default, skip_null.
subcommands:
For more details of each subcommand, add it as an argument followed by --help.
Available subcommands:
sub3 Docstring for Sub3.
So, we have to choose between multilevel subcommands or complete help.
Also, I personally don't like using a dictionary to define CLIs. It is not intuitive and can easily get messy.
Pitch
One idea I came up with is to use properties as references to the sub-subcommands.
- The name of the property defines the name of the sub-subcommand.
- The type hint of the return value of the property defines the component of the sub-subcommand.
This allows us to define arbitrary levels of subcommands using only class objects and generate complete help using only docstrings.
Example:
class Sub3:
"""Docstring for Sub3."""
def command3(self):
"""Docstring for command3."""
...
class Sub2:
"""Docstring for Sub2."""
def command2(self):
"""Docstring for command2."""
...
@property
def sub3(self) -> Sub3:
return Sub3()
class Main:
def command1(self):
"""Docstring for command1."""
...
@property
def sub2(self) -> Sub2:
return Sub2()
if __name__ == "__main__":
import jsonargparse
jsonargparse.CLI(Main)
In a shell, it can be called like this.
python -m app sub2 sub3 command3
This is equivalent to the following Python code.
Main().sub2.sub3.command3()
As a proof of concept, I implemented this idea. https://github.com/Kenji-Hata/jsonargparse/pull/1/files
The changes required seem small, but I'm new to jsonargparse, so I may be missing something.
Alternatives
As an alternative to multilevel subcommands, another solution is chaining function calls. That is, if a command returns a class object or function, use it as the next component and run the CLI with the remaining arguments.
With this approach, we could give additional arguments to each level of the subcommand.
python -m app sub2 --x=1 sub3 --y=2 command3
Main().sub2(x=1).sub3(y=2).command3()
This is not what I requested, as I do not design CLIs like this, but if there are plans to add such a feature, then the feature I suggested would be redundant.