Recipes
Loading file-based defaults
It's common to load parameters defaults from external config files and then allow CLI to override them.
In a simple cases, this pattern is supported with a help of const and Context.
First, custom_action loads the config into the context, then dynamic const options do the loads.
from paramspecli import Missing, MISSING
@dataclass
class Ctx:
toml: dict[str, dict[str, str]] | None = None
def load_foo(context: Ctx) -> str | Missing:
if not context.toml:
return MISSING
try:
return context.toml["app"]["foo"]
except KeyError:
return MISSING
def load_toml(context: Ctx, value: Path, **kwargs: Any) -> None:
with value.open("rb") as f:
context.toml = tomllib.load(f)
def f(*, foo: str):
pass
cli = Command(f)
cli.bind(foo=-(const(load=load_foo) | option("--foo", default="a")))
cli.append_action(custom_action("--toml", handler=load_toml, type=PathConv.file(exists=True)))
cli.parse(context=Ctx())
This code have some not very evident bug. Argparse parse options in order of appearance on a command line.
The line myprog --foo b --toml pyproject.toml would load toml too late, then the foo is already processed.
There are two crude solutions to this problem. For one, --toml may be defined as a group option, with loadable options moved to the subcommands: myprog --toml pyproject.toml my_command --foo b. For another, parse could be restarted if toml found:
ctx = Ctx()
cli.parse(context=ctx)
if ctx.toml: # toml found, parse again
cli.parse(context=ctx)
Or, simpler, handler may throw a special ParseAgain exception.
from paramspecli import ParseAgain
def load_toml(context: Ctx, value: Path, **kwargs: Any) -> None:
# if second pass, do nothing
if context.toml:
return
# on a first pass, load and restart
with value.open("rb") as f:
context.toml = tomllib.load(f)
raise ParseAgain
Emulating the FileType
Don't. Use the pathlib.Path methods instead:
def func(*, file: pathlib.Path):
data=path.read_bytes()
cmd.bind(file=option("--file", type=PathConv.file(exist=True)))
Using Enum as a type converter
StrEnum is nice and actually works.
class MyEnum(StrEnum):
A = "a"
B = "b"
def func(*, foo: MyEnum):
pass
Command(func).bind(
foo=-option("--foo", type=MyEnum, choices=MyEnum, default=MyEnum.A)
)
Clean multiarg options
To make the list options less ugly, mix a few mutually exclusive options and flags.
Here, tags list may be set manually, set to some predefined value, cleared or left default:
cmd.bind(
tags=-cmd.add_oneof().include(
option(
"--tags",
nargs="+",
default=["head", "title", "body"],
)
| flag("--tags-recommended", value=["pre", "code"])
| flag("--tags-not-recommended", value=["blink"])
| flag("--no-tags", value=[])
)
)
Generating shell completions
Use the shtab. While there may be some minor incompatibles, generally result is ok.
For external build, use the Group.build_parser/Command.build_parser method (specially included to expose the generated ArgumentParser):
import shtab
cli = Group(prog="prog")
...
parser = cli.build_parser(config=Config())
script = shtab.complete(parser)
print(script)
For embedding the completions generation in your app, use the custom action:
def gen_completion(*, parser: ArgumentParser, value: str, **kwargs: Any) -> None:
script = shtab.complete(parser, value)
sys.stdout.write(script)
parser.exit()
cli = Group(prog="prog")
cli.append_action(custom_action("--completion", handler=gen_completion, choices=shtab.SUPPORTED_SHELLS))