Subcommands¶
Instead of using the function clig.run() described in the
userguide, you can create an object instance of the type
Command, passing your function to its constructor, and call
the Command.run() method.
# prog01.py
from clig import Command
def main(name:str, age: int, height: float):
print(locals())
cmd = Command(main)
cmd.run()
> python prog01.py "Carmem Miranda" 42 1.85
{'name': 'Carmem Miranda', 'age': 42, 'height': 1.85}
This makes it possible to use some methods to add
subcommands. All
subcommands will also be instances of the same class Command.
There are 4 main methods available:
Command.new_subcommand: Creates a subcommand and returns the new createdCommandinstance.Command.add_subcommand: Creates the subcommand and returns the caller object. This is useful to add multiple subcommands in one single line.Command.end_subcommand: Creates the subcommand and returns the parent of the caller object. If the caller doesn’t have a parent, an error will be raised. This is useful when finishing to add subcommands in the object on a single line.Command.subcommand: Creates the subcommand and returns the input function unchanged. This is a proper method to be used as a function decorator.
There are also
2 module level functions:
clig.command() and clig.subcommand().
They also returns the functions unchanged, and so may also be used as
decorators.
All the functions declared as commands execute sequentially, from a
Command to its subcommands.
The Command constructor also accepts other arguments to
customize the interface, and also has other methods, like
print_help(), analog to the
original method.
Subcommands using methods¶
The methods new_subcommand and
add_subcommand can be used to add subcommands
in an usual object oriented code.
Consider the case below, with 2 levels of subcommands:
prog
├─── subfunction1
└─── subfunction2
└─── subsubfunction
You can create the main command object and add subcommands to it after:
>>> from clig import Command
>>> def prog(name: str, age: int):
... print(locals())
...
>>> def subfunction1(height: float):
... print(locals())
...
>>> def subfunction2(father: str, mother: str):
... print(locals())
...
>>> def subsubfunction(city: str, state: str):
... print(locals())
...
>>> # defines the main object
>>> cmd = Command(prog)
...
>>> # adds a subcommand to the main object
>>> cmd.add_subcommand(subfunction1)
...
>>> # adds and returns a new created subcommand object
>>> sub = cmd.new_subcommand(subfunction2)
...
>>> # adds a subcommand to the subcommand object
>>> sub.add_subcommand(subsubfunction)
...
>>> # main command help
>>> cmd.print_help()
usage: prog [-h] name age {subfunction1,subfunction2} ...
positional arguments:
name
age
{subfunction1,subfunction2}
options:
-h, --help show this help message and exit
Note
To put the subcommands in a separate section with another title (other than
positional arguments: as above), see the
parameters that can be passed to the Command() constructor.
Subcommands are correctly handled with their subparsers.
>>> # subcommand help
>>> sub.print_help()
usage: prog name age subfunction2 [-h] father mother {subsubfunction} ...
positional arguments:
father
mother
{subsubfunction}
options:
-h, --help show this help message and exit
Remember that the command functions execute sequentially, from a
Command to its subcommands.
>>> # run the main comand with all subcommands
>>> cmd.run("jack 23 subfunction2 michael suzan subsubfunction santos SP".split())
{'name': 'jack', 'age': 23}
{'father': 'michael', 'mother': 'suzan'}
{'city': 'santos', 'state': 'SP'}
...
>>> # run only the subcommand with its subcommand
>>> sub.run(["jean", "karen", "subsubfunction", "campos", "RJ"])
{'father': 'jean', 'mother': 'karen'}
{'city': 'campos', 'state': 'RJ'}
To access the attributes of a command inside the functions of its subcommands,
check out the feature of the Context object.
All CLI in one statement¶
By using the 3 methods new_subcommand,
add_subcommand and
end_subcommand you can define the whole
interface in one single statement (one line of code).
To give a clear example, consider the Git cli interface. Some of its command’s hierarchy is the following:
git
├─── status
├─── commit
├─── remote
│ ├─── add
│ ├─── rename
│ └─── remove
└─── submodule
├─── init
└─── update
Then, the functions could be declared in the following structure, with the CLI definition at the end:
# prog02.py
from inspect import getframeinfo, currentframe
from pathlib import Path
from clig import Command
def git(exec_path: Path = Path("git"), work_tree: Path = Path("C:/Users")):
"""The git command line interface"""
print(f"Command: {getframeinfo(currentframe()).function} | Arguments: {locals()}")
def status(branch: str):
"""Show the repository status"""
print(f"Command: {getframeinfo(currentframe()).function} | Arguments: {locals()}")
def commit(message: str):
"""Record changes to the repository"""
print(f"Command: {getframeinfo(currentframe()).function} | Arguments: {locals()}")
def remote(verbose: bool = False):
"""Manage remote repositories"""
print(f"Command: {getframeinfo(currentframe()).function} | Arguments: {locals()}")
def add(name: str, url: str):
"""Add a new remote"""
print(f"Command: {getframeinfo(currentframe()).function} | Arguments: {locals()}")
def rename(old: str, new: str):
"""Rename an existing remote"""
print(f"Command: {getframeinfo(currentframe()).function} | Arguments: {locals()}")
def remove(name: str):
"""Remove the remote reference"""
print(f"Command: {getframeinfo(currentframe()).function} | Arguments: {locals()}")
def submodule(quiet: bool):
"""Manages git submodules"""
print(f"Command: {getframeinfo(currentframe()).function} | Arguments: {locals()}")
def init(path: Path = Path(".").resolve()):
"""Initialize the submodules recorded in the index"""
print(f"Command: {getframeinfo(currentframe()).function} | Arguments: {locals()}")
def update(init: bool, path: Path = Path(".").resolve()):
"""Update the registered submodules"""
print(f"Command: {getframeinfo(currentframe()).function} | Arguments: {locals()}")
######################################################################################
# The whole interface is built in the code below
# It could also be placed in a separated file importing the functions
(
Command(git)
.add_subcommand(status)
.add_subcommand(commit)
.new_subcommand(remote)
.add_subcommand(add)
.add_subcommand(rename)
.end_subcommand(remove)
.new_subcommand(submodule)
.add_subcommand(init)
.end_subcommand(update)
.run()
)
Help for the main command:
> python prog02.py -h
usage: git [-h] [--exec-path EXEC_PATH] [--work-tree WORK_TREE]
{status,commit,remote,submodule} ...
The git command line interface
positional arguments:
{status,commit,remote,submodule}
status Show the repository status
commit Record changes to the repository
remote Manage remote repositories
submodule Manages git submodules
options:
-h, --help show this help message and exit
--exec-path EXEC_PATH
--work-tree WORK_TREE
Help for the remote subcomand:
> python prog02.py remote -h
usage: git remote [-h] [--verbose] {add,rename,remove} ...
Manage remote repositories
positional arguments:
{add,rename,remove}
add Add a new remote
rename Rename an existing remote
remove Remove the remote reference
options:
-h, --help show this help message and exit
--verbose
Help for the remote rename subcommand:
> python prog02.py remote rename -h
usage: git remote rename [-h] old new
Rename an existing remote
positional arguments:
old
new
options:
-h, --help show this help message and exit
Remember: the command functions execute sequentially, from a
Command to its subcommands.
> python prog02.py remote rename oldName newName
Command: git | Arguments: {'exec_path': WindowsPath('git'), 'work_tree': WindowsPath('C:/Users')}
Command: remote | Arguments: {'verbose': False}
Command: rename | Arguments: {'old': 'oldName', 'new': 'newName'}
Subcommands using method decorators¶
You can define subcommands using the
Command.subcommand() method as a decorator. To do
it, first create a Command instance. The decorator only
registries the functions as commands (it doesn’t change their definitions).
# prog03.py
from clig import Command
def main(verbose: bool = False):
"""Description for the main command"""
print(f"{locals()}")
# create the command object
cmd = Command(main)
@cmd.subcommand
def foo(a, b):
"""Help for foo sub command"""
print(f"{locals()}")
@cmd.subcommand
def bar(c, d):
"""Help for bar sub command"""
print(f"{locals()}")
cmd.run()
> python prog03.py -h
usage: main [-h] [--verbose] {foo,bar} ...
Description for the main command
positional arguments:
{foo,bar}
foo Help for foo sub command
bar Help for bar sub command
options:
-h, --help show this help message and exit
--verbose
Note
The cmd object in the example above could also be created
without a function argument
(i.e., cmd = Command())
Using the Command() constructor as a decorator¶
You could also use de Command() constructor as a
decorator. However,
that would redefine the function name as a Command instance.
>>> from clig import Command
>>> def main():
... pass
...
>>> cmd = Command(main) # the `main` function is not affected with this
>>> print(type(main))
<class 'function'>
...
>>> # this will change the `main` function!
>>> @Command
>>> def main():
... pass
...
>>> print(type(main)) # now the main function is a `Command` instance
<class 'clig.clig.Command'>
Adding internal level of subcommads with decorators¶
By using the @Command.subcommand decorator without
arguments, the functions are not modified at runtime, but you won’t be able to
define more than one level of subcommands,
unless you pass an parent argument to the decorator.
However, by knowing the fact that subcommads are registered in a
OrderedDict
attribute defined as Command.subcommands, it it is
possible to use it directly, just knowing the subcommad
name and call the respective
subcommand decorator.
# prog04.py
from clig import Command
def prog(name: str = "mario", age: int = 40):
print(locals())
# defines the main object and adds subcommands
cmd = Command(prog)
@cmd.subcommand
def subfunction1(height: float):
print(locals())
@cmd.subcommand
def subfunction2(father: str, mother: str):
print(locals())
# now use the attribute `subcommands` to add another level of subcommands
@cmd.subcommands["subfunction2"].subcommand
def subsubfunction(city: str, state: str):
print(locals())
cmd.run()
> python prog04.py -h
usage: prog [-h] [--name NAME] [--age AGE] {subfunction1,subfunction2} ...
positional arguments:
{subfunction1,subfunction2}
options:
-h, --help show this help message and exit
--name NAME
--age AGE
> python prog04.py subfunction2 -h
usage: prog subfunction2 [-h] father mother {subsubfunction} ...
positional arguments:
father
mother
{subsubfunction}
options:
-h, --help show this help message and exit
Method decorator with arguments¶
The @Command.subcommand() decorator accepts all
arguments that creates a new Command instance, check them
on its docs.
A particular argument used to create more levels of subcommands is the
parent argument, which accepts a subcommad name, or
its function object.
# prog05.py
from clig import Command
def prog(name: str = "mario", age: int = 40):
print(locals())
# First, defines the main object and adds on subcommand
cmd = Command(prog)
@cmd.subcommand
def subfunction(height: float):
print(locals())
# now use the parent argument, with the function object, or the command name
@cmd.subcommand(parent=subfunction)
def internalsubfunction(city: str, state: str):
print(locals())
cmd.run()
> python prog05.py --help
usage: prog [-h] [--name NAME] [--age AGE] {subfunction} ...
positional arguments:
{subfunction}
options:
-h, --help show this help message and exit
--name NAME
--age AGE
> python prog05.py subfunction --help
usage: prog subfunction [-h] height {internalsubfunction} ...
positional arguments:
height
{internalsubfunction}
options:
-h, --help show this help message and exit
Subcommands using function decorators¶
As it was noticed in the previous section, using decorators without the
parent argument does not allow you to declare more
than one level of subcommands, when you don’t use the
subcommands dict attribute,
calling methods from its elements.
For these simple cases (i.e., one command with just one level of subcommands),
it is more convenient to use the module level functions
clig.command() and clig.subcommand() as
decorators, because they don’t require to define a Command
object:
# prog06.py
from clig import command, subcommand, run
# Here, you don't need to define the main `Command` object
@command
def main(verbose: bool = False):
"""Description for the main command"""
print(locals())
@subcommand
def foo(a, b):
"""Help for foo sub command"""
print(locals())
@subcommand
def bar(c, d):
"""Help for bar sub command"""
print(locals())
run()
> python prog06.py -h
usage: main [-h] [--verbose] {foo,bar} ...
Description for the main command
positional arguments:
{foo,bar}
foo Help for foo sub command
bar Help for bar sub command
options:
-h, --help show this help message and exit
--verbose
However, to define more than one level of subcommands using these function decorators, you can also pass arguments to the functions, in a similar way as passing an argument to the methods decorators, as shown in the following.
Function decorator with arguments¶
The @clig.subcommand function decorator accepts all
arguments used to create a new Command instance, check them
on the their detailed docs.
A particular argument used to create more levels of subcommands is the
parent argument, which accepts a subcommad name, or
its function object.
# prog07.py
from clig import command, subcommand, run
# Here, you don't need to define the main `Command` object
@command
def prog(name: str = "mario", age: int = 40):
print(locals())
@subcommand
def subfunction(height: float):
print(locals())
# but you still can use the parent argument, with the function object, or the subcommand name
@subcommand(parent=subfunction)
def internalsubfunction(city: str, state: str):
print(locals())
run()
> python prog07.py -h
usage: prog [-h] [--name NAME] [--age AGE] {subfunction} ...
positional arguments:
{subfunction}
options:
-h, --help show this help message and exit
--name NAME
--age AGE
> python prog07.py subfunction -h
usage: prog subfunction [-h] height {internalsubfunction} ...
positional arguments:
height
{internalsubfunction}
options:
-h, --help show this help message and exit
Context¶
As noted above, one command and its subcommads run sequentially. However, they normally don’t have access from each other’s arguments.
To get this access and use function’s argument in its subcommad’s functions, you
can declare an argument having the type clig.Context. This
argument will not be added to the interface, but it will contain the whole
parser
Namespace
in its attribute namespace.
# prog08.py
import clig
@clig.command
def first(foo: str, bar: int):
print(f"Arguments in the top level command: {locals()}")
@clig.subcommand
def second(ctx: clig.Context, ham: float):
print()
print("Running now the second command . . .")
print(f"The 'foo' argument from the previous command was: foo = {ctx.namespace.foo}")
clig.run()
> python prog08.py bazinga 32 second 22.5
Arguments in the top level command: {'foo': 'bazinga', 'bar': 32}
Running now the second command . . .
The 'foo' argument from the previous command was: foo = bazinga
Protocols¶
You can pass a
Protocol as
type argument to the annotated Context parameter, so it can be
recognized by intellisense or type checkers.
# prog09.py
from typing import Protocol
from clig import Command, Context
class MyProtocol(Protocol):
foo: str
bar: int
def first(foo: str, bar: int):
print(locals())
def second(ctx: Context[MyProtocol], ham: float):
foo_value: str = ctx.namespace.foo # --> recognized by type checkers / intellisense
print("foo value = " + foo_value)
Command(first).add_subcommand(second).run()
> python prog09.py shazan 23 second 74.9
{'foo': 'shazan', 'bar': 23}
foo value = shazan
The Attribute Context.command¶
In addition to the namespace attribute of type
Namespace,
the Context object will also contain the whole main
Command object in the attribute
Context.command. So, you can use this object and all
its attributes.
>>> from clig import Command, Context
...
...
>>> def main(foo: str, bar: int):
... print(f"Running main with: {locals()}")
...
...
>>> def sub1(ctx: Context, ham: float):
... print(f"Top level command name = {ctx.command.name}")
...
...
>>> def sub2(ctx: Context, baz: bool):
... print("Subcommand functions:")
... for cmd in ctx.command.subcommands:
... print(f"{cmd}: {ctx.command.subcommands[cmd].func}")
...
...
>>> command = Command(main).add_subcommand(sub1).add_subcommand(sub2)
...
>>> command.run(["hello", "23", "sub1", "32.5"])
Running main with: {'foo': 'hello', 'bar': 23}
Top level command name = main
...
>>> command.run(["hello", "23", "sub2", "--baz"])
Running main with: {'foo': 'hello', 'bar': 23}
Subcommand functions:
sub1: <function sub1 at 0x00000169DC1AC4A0>
sub2: <function sub2 at 0x00000169DC1EA200>
An solved issue with argparse subparsers¶
There is a known argparse
behavior that happens when you have subparsers with same argument names, which
may be seen as an issue, as described below.
Normally, all arguments are gathered into one
Namespace:
>>> from argparse import ArgumentParser
...
>>> parser = ArgumentParser()
>>> parser.add_argument("--foo")
>>> subcommand = parser.add_subparsers()
>>> subcommand = subcommand.add_parser("subcommand")
>>> subcommand.add_argument("--bar")
>>> parser.parse_args(["--foo", "span", "subcommand", "--bar", "cheese"])
Namespace(foo='span', bar='cheese')
The issue is generated when you have subparsers with same argument names.
Imagine you have the following subcommand structure:
parser
├─── argument "--name"
└─── subparser
└─── argument "--name"
That would be built in argparse with:
>>> from argparse import ArgumentParser
...
>>> parser = ArgumentParser()
>>> parser.add_argument("--name")
>>> subcommand = parser.add_subparsers()
>>> subcommand = subcommand.add_parser("subcommand")
>>> subcommand.add_argument("--name")
Parsing the argument individually to each parser works ok:
>>> parser.parse_args(["--name", "jean"])
Namespace(name='jean')
>>> parser.parse_args(["subcommand", "--name", "rose"])
Namespace(name='rose')
But using the whole command line gets an unexpected behavior → Only the last argument value passed is stored:
>>> parser.parse_args(["--name", "monica", "subcommand", "--name", "joe"])
Namespace(name='joe')
clig solves that issue before passing argument to the functions:
>>> import clig
...
...
>>> def main(name: str = ""):
... print(locals())
...
...
>>> def subcommand(name: str = ""):
... print(locals())
...
...
>>> cmd = clig.Command(main).add_subcommand(subcommand)
...
>>> cmd.run(["--name", "monica", "subcommand", "--name", "joe"])
{'name': 'monica'}
{'name': 'joe'}
The solution applied by clig is changing the argument names at runtime: Blank
spaces (" ") are appended to them, which are stripped when passing argument to
the functions. That can be inspected with the Context object
approach:
>>> import clig
...
...
>>> def main(name: str = ""):
... print(locals())
...
...
>>> def subcommand(ctx: clig.Context, name: str = ""):
... args = locals().copy()
... args.pop("ctx")
... print(args)
... print(ctx.namespace)
...
...
>>> cmd = clig.Command(main).add_subcommand(subcommand)
...
>>> cmd.run(["--name", "monica", "subcommand", "--name", "joe"])
{'name': 'monica'}
{'name': 'joe'}
Namespace(name='monica', **{'{subcommand}': 'subcommand', 'name ': 'joe'})
You don’t need to know that internal solution in most cases. But, in cases when
you are using subparsers that access Context object with the
whole namespace attribute, then you may want to know
how the
Namespace
object will look.