Subcommand¶
A subcommand class is just regular class capable of being a cappa Command.
The primary relevant detail for actually attaching a subcommand to your command,
is that the field which captures the subcommand options must be annotated with
Subcommand.
As you’ll have already seen throughout the documentation, before now.
Subcommands are expressed by annotating a union of subcommand options, and
annotating the union with cappa.Subcommand. I.e.
Note
If you want to explicitly control the name of a subcommand beyond the default,
you must annotate the command’s class with @cappa.command(name="the-name").
from __future__ import annotations
from dataclasses import dataclass
from typing import Annotated
from cappa import Subcommand, Subcommands
@dataclass
class Command:
# This
subcommand: Subcommands[SubcmdOne | SubcmdTwo]
# is shorthand for this
subcommand2: Annotated[SubcmdOne | SubcmdTwo, Subcommand]
# is shorthand for this
subcommand3: Annotated[SubcmdOne | SubcmdTwo, Subcommand()]
@dataclass
class SubcmdOne:
example: str
@dataclass
class SubcmdTwo:
option: int
You can use the
Subcommands[...]shorthand to avoid the use ofAnnotated, in cases where you dont need to customizeSubcommand’s constructor arguments.Annotation with
Subcommandwill instantiate it no arguments, getting the default behaviorThe above denotes a required subcommand with two options. To make it optional, you can additionally union the options with
Noneand default the field toNone.By default, name of the class (converted to dash-case) will be the CLI name for the subcommand option. E.x.
subcmd-oneandsubcmd-two, from above. You can decorate each subcommand class with@cappa.command(name='<x>')to choose your own name.
Aliases¶
Subcommands can declare alternate names that invoke the same command. This is
useful for short forms (ls for list), legacy spellings, and migration paths
when renaming a command.
Pass aliases= to @cappa.command(...):
import cappa
from dataclasses import dataclass
@cappa.command(aliases=["ls"])
@dataclass
class List:
pass
@cappa.command(name="remove", aliases=["rm"])
@dataclass
class Remove:
pass
@dataclass
class Tool:
cmd: cappa.Subcommands[List | Remove]
With the above, tool list, tool ls, tool remove, and tool rm all work,
and --help displays both names per subcommand:
Subcommands
list, ls
remove, rm
Deprecated aliases¶
cappa.Alias(name, deprecated=...) emits a runtime warning to stderr when the
user invokes the command via that alias, while still dispatching to the
canonical command. Useful when you’ve renamed a subcommand but want to give
users a transition period:
@cappa.command(
name="remove",
aliases=[
cappa.Alias("rm", deprecated="use 'remove' instead"),
cappa.Alias("delete", deprecated=True), # default message
],
)
@dataclass
class Remove:
pass
Invoking tool rm runs Remove, then prints:
Error: Command alias `rm` is deprecated: use 'remove' instead
hidden and deprecated compose — a hidden + deprecated alias is the typical
shape for an old name you want to keep alive but never show again.
Imperative construction¶
When constructing commands manually (without the decorator), aliases= is a
keyword argument on Command itself:
import cappa
cmd = cappa.Command(
Tool,
arguments=[
cappa.Subcommand(
field_name="cmd",
options={
"list": cappa.Command(List, name="list", aliases=["ls"]),
"remove": cappa.Command(
Remove, name="remove", aliases=["rm"]
),
},
),
],
)
The dict key in Subcommand.options remains the canonical name; aliases are
declared on each Command and resolved automatically.
Collisions¶
Aliases must not collide with another subcommand’s canonical name, with another
alias under the same parent, or with the command’s own canonical name. Any of
these raises ValueError at command construction time so the conflict is
caught before the CLI ever runs.