Basic usage
Let's take a command from the quick start example and put it into the group.
from paramspecli import Group, argument, option, flag
def ping(addr: str, *, until_stopped: bool, count: int | None):
print(f"Pinging {addr} ...")
def server(name: str, *, port: int, debug: bool):
print(f"Serving {name} on port {port} ...")
cli = Group(prog="myprog")
with cli.add_group("net") as group:
with group.add_command("ping", ping) as cmd:
cmd.bind(
-argument("IP"),
until_stopped=-flag("-t"),
count=-option("-n", type=int),
)
with group.add_command("server", server) as cmd:
cmd.bind(
-argument("TEXT"),
port=-option("--port", type=int, default=80),
debug=-flag("--debug", "-g"),
)
route = cli.parse()
route()
Running it:
$ python myprog.py net ping -t -n 10 127.0.0.1
Pinging 127.0.0.1 ...
$ python myprog.py net server --port 8080 -g breakfast
Serving breakfast on port 8080 ...
$ python myprog.py net --help
usage: myprog net [--help] {ping,server} ...
options:
--help, -h Show help and exit
commands:
{ping,server}
ping
server
The example is self-explaining enough, except the crucial bit. Did you notice the unnoticeable minus sign prefixing the option? You must add it for the type check to pass:
cli.bind(
count=-option("-n", type=int),
)
Why? In short, paramspecli lies to the type checker. In the name of good, of course.
If you don't like minus, use the t property. Or t object, which overloads [], @, () operations. Choose one style
and stick to it.
from paramspecli import t
option("-n").t
t(option("-n"))
t[option("-n")]
t @ option("-n")
option("-n") @ t
Why with and context managers?
Just for an aesthetic - they paint a very nice visual hierarchy. They are not required and in fact do nothing.
Now let's take a closer look at the building blocks.
Arguments
Arguments are instances of the Argument class. They are created by the argument factory function.
Arguments are positional-only, and it's an error to bind it by keyword.
argument(metavar, type=None, *, help=None, nargs=None, default=None, choices=None)
- metavar - metavar name. Note that it's required
- type - callable to convert string into the parameter type
- help - help string
-
nargs
- int: Consume fixed number of words
*: Consume zero or more words+: Consume one or more words?: Consume zero or one word (optional mode)
-
default - Allowed if nargs=
"?". May be anything. If default is a string, it will be converted by type(), otherwise left as is. - choices - Iterable of allowed values
| Code | Command line | Result |
|---|---|---|
argument("N", type=int) |
1 |
1 |
argument("N", type=int, nargs="*") |
1 |
[1] |
1 2 |
[1, 2] |
|
|
[] |
|
argument("N", type=int, nargs="?", default=-1) |
1 |
1 |
|
-1 |
Options
Options are instances of the Option class. They are created by one of factory functions.
Options are keyword-only, and it's an error to bind it positionally.
option
Tries to consume a word from the command line. Result is a (type converted) word. If it is not present, result is default.
option(*names, type=None, help=None, default=None, nargs=None, choices=None, metavar=None, show_default=None)
- names - one or more names on a command line. Should start from the
-, for example,--foo,-g. - type - callable to convert string into the parameter type
- help - help string. Also may be set to
Falsehide option from--help. - default - result if option is missing. May be anything. If default is a string, it will be converted by type(), otherwise left as is.
- nargs:
- int: Consume fixed number of words
+: Consume one or more words*: Consume zero or more words?: Consume zero or one word. If zero,...(ellipsis) is returned instead.
- choices - Iterable of allowed values.
- metavar - meta-variable string. If nargs is a fixed number, may be a tuple of strings.
- show_default - show default value in help:
True: show default valueFalse: show nothingNone: show default if it makes sense- str: show this very string
| Code | Command line | Result |
|---|---|---|
option("--addr") |
--addr www.example.com |
"www.example.com" |
|
None |
|
option("--width", type=int, default=80) |
--width 120 |
120 |
|
80 |
|
option("--user", nargs="+") |
||
--user bob |
["bob"] |
|
--user bob alice |
["bob", "alice"] |
|
|
None |
|
option("--xyz", nargs=3, type=int) |
--xyz 0 5 -1 |
[0, 5, -1] |
option("--threads", nargs="?", type=int) |
--threads 2 |
2 |
--threads |
... |
|
|
None |
|
option("--day", choices=("sunday", "monday")) |
--day sunday |
"sunday" |
--day monday |
"monday" |
|
|
None |
repeated_option
Like option, but allowed to present multiple times on a command line. Result is a list of (type converted) words. If missing, result is an empty list.
repeated_option(names, *, type=None, help=None, nargs=None, choices=None, flatten=False, metavar=None)
- names, type, help, choices, metavar, show_default - see option
- nargs - int,
+or*. Consume fixed or unlimited number of words for each option occurence. - flatten - Meaningful only if nargs set. Flattens the individual lists into the
single list. Under the hood, it switches the argparse action from
appendto theextend.
| Code | Command line | Result |
|---|---|---|
repeated_option("--port", type=int) |
--port 80 --port 8080 |
[80, 8080] |
|
[] |
|
repeated_option("--port", type=int, nargs="+") |
--port 80 |
[[80]] |
--port 80 --port 8080 8081 |
[[80], [8080, 8081]] |
|
|
[] |
|
repeated_option("--port", type=int, nargs="+", flatten=True) |
--port 80 |
[[80]] |
--port 80 --port 8080 8081 |
[80, 8080, 8081] |
|
|
[] |
flag
Simple flag. Result is value. If missing, result is default.
flag(names, *, help=None, value=True, default=..., show_default=None)
- names, help, show_default - see option
- value - result if flag is present. May be anything.
- default - result if flag is missing. If not set, choosen automatically between
True,FalseandNonedepending on a value
| Code | Command line | Result |
|---|---|---|
flag("--debug") |
--debug |
True |
|
False |
|
flag("--noerr", value=False) |
--noerr |
False |
|
True |
|
flag("--cheat", value="iddqd") |
--cheat |
"iddqd" |
|
None |
|
flag("--cheat", value="iddqd", default="nocheat") |
--cheat |
"iddqd" |
|
"nocheat" |
switch
A --foo/--no-foo style complimentary flags. Result is True or False depending on a flag. If missing, result is default.
switch(names, *, help=None, default=False, show_default=None)
- names, help, show_default - see option
- default - result if flag is missing. May be anything.
| Code | Command line | Result |
|---|---|---|
switch("--magic") |
--magic |
True |
--no-magic |
False |
|
|
False |
|
switch("--magic", default=True) |
--magic |
True |
--no-magic |
False |
|
|
True |
|
switch("--magic", default=None) |
--magic |
True |
--no-magic |
False |
|
|
None |
count
A -vvv style flag. Result is a number of the flag occurences.
If missing, result is default
count(names, *, help=None, default=0, show_default=None)
- names, help, show_default - see option
- default - int or
None. If int, it's also the initial count value.
| Code | Command line | Result |
|---|---|---|
count("-v") |
-vv |
2 |
|
0 |
|
count("-v", default=40) |
-vv |
42 |
|
40 |
|
count("-v", default=None) |
-vv |
2 |
|
None |
repeated_flag
Like a flag, but may present multiple times on a command line. Result is a list of value. If missing, result is an empty list. Repeated flags are most useful if mixed.
repeated_flag(names, *, help=None, value=True)
- names, help - see option
- value - value to append to the result list for each flag occurence. May be anything.
| Code | Command line | Result |
|---|---|---|
repeated_flag("--hard") |
--hard |
[True] |
--hard --hard |
[True, True] |
|
|
[] |
|
repeated_flag("--hard", value="core") |
--hard |
["core"] |
--hard --hard |
["core", "core"] |
|
|
[] |
Tip
Option and Argument objects are immutable and may be reused in different commands. It's a great way to reduce copypaste.
Commands
Commands are instances of the Command class. They could be created directly and attached to the groups later. For hierarchical CLIs it's simpler to use the Group.add_command() method, which combines the creation and attaching.
class Command(func, *, help=None, info=None, usage=None, epilog=None, prog=None, add_help=True)
- func - command handler
- help - short help string. Shown in the commands list of a parent group help.
- info - long description
- usage - usage string. By default, it's generated automatically.
- epilog - epilog message
- prog - program name. Part of the default usage string.
- add_help - add
--helpaction
Groups
Similar the to commands, groups are created by instantiating the Group class or by calling the Group.add_group() method.
Groups may have own options and handlers.
class Group(*, help=None, info=None, usage=None, epilog=None, prog=None, title=None, headline=None, metavar=None, default_func=..., add_help=True)
- help - short help string. Shown in the commands list of a parent group help.
- info - long description
- usage - usage string. By default, it's generated automatically.
- epilog - epilog message
- prog - program name. Part of the default usage string.
- title - title for the list of own commands. Default is set in Configuration.
- headline - description for the list of own commands
- metavar - meta-variable name for own commands
- default_func - default handler if no command was choosen. By default, prints own help. If set to
None, choosing command is required. - add_help - add
--helpaction
Most of the Group and Command settings are presentational.
See what they mean
Group
usage: PROG GROUP_METAVAR ...
GROUP_INFO
GROUP_TITLE:
GROUP_HEADLINE
GROUP_METAVAR
command COMMAND_HELP
EPILOG
Command
usage: COMMAND_USAGE
COMMAND_INFO
SECTION_TITLE:
SECTION_HEADLINE
--option OPTION_METAVAR OPTION_HELP
EPILOG
Group.add_group(name, *args, **kwargs)
Group.add_command(name, *args, **kwargs)
- name - group or command name as seen on the command line. May be a tuple to provide aliases
- args, kwargs: Forwarded to the
Group/Commandclass
Assembling CLI from separate groups and commands is possible too:
from paramspecli import Command
def ping():
pass
cmd = Command(ping)
from paramspecli import Group
from . import ping
cli = Group()
netgroup = Group()
netgroup["ping"] = ping.cmd
# aliases are supported too
cli["net", "network"] = netgroup
It might be more readable to define all group's nodes at once:
cli.nodes = {
"ping": ping.cmd,
("net", "network"): netgroup,
}
Tip
Same Command objects may be reused in different groups. It's a simple way to make a top-level
shortcut for some five-groups-deep command.
Parse result
Parsing the command line and executing handlers are separate steps. Inside the parse() method, paramspecli 'compiles' itself to the argparse.ArgumentParser,
runs it to process the command line, then composes the final Route object.
Route contains a sequence of handlers.
It's a sequence because groups may have own handlers too.
For convenience, Route is callable and calling it invokes handlers one by one.
Tip
Parse and execute separation allows to garbage collect CLI objects and free resources in long running programs. For this to happen, CLI code should reside in a separate function:
def process_commandline():
cli = Group()
cli.add_group(...)
# etc
return cli.parse()
def main():
route = process_commandline()
# CLI object are no longer exist at this point
route()
Group.parse(input=None, *, config=None, context=None)
Command.parse(input=None, *, config=None, context=None)
- input - Sequence of strings to parse. Defaults to sys.argv if None.
- config - argparse 'compilation' settings
- context - Optional context object to communicate between groups and commands
Handlers may also be examined and called manually:
route = cli.parse("net ping -t -n 10 127.0.0.1".split())
# Invoke the whole handlers sequence
route() # # Pinging 127.0.0.1 ...
# Invoke the final handler only
print(route[-1]) # Handler(func=<function ping at 0xDEADBEEF>, arguments=['127.0.0.1'], options={'until_stopped': True, 'count': 10})
route[-1]() # Pinging 127.0.0.1 ...
# Invoke handlers manually
for handler in route:
print(handler)
handler()
# Or even super manually
for handler in route:
if handler.func:
handler.func(*handler.arguments, **handler.options)