Groups
Groups offer a way of organizing parameters and commands on the help-page; for example:
Usage: my-script.py create [OPTIONS]
╭─ Vehicle (choose one) ───────────────────────────────────────────────────────╮
│ --car [default: False] │
│ --truck [default: False] │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Engine ─────────────────────────────────────────────────────────────────────╮
│ --hp [default: 200] │
│ --cylinders [default: 6] │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Wheels ─────────────────────────────────────────────────────────────────────╮
│ --wheel-diameter [default: 18] │
│ --rims,--no-rims [default: False] │
╰──────────────────────────────────────────────────────────────────────────────╯
They also provide an additional abstraction layer that validators can operate on.
Groups can be created in two ways:
Explicitly creating a
Group
object.Implicitly with a string. This will implicitly create a group,
Group(my_str_group_name)
, if it doesn't exist. If there exists aGroup
object with the same name within the command/parameter context, it will join that group.Warning
While convenient and terse, mistyping a group name as a string will unintentionally create a new group!
Every command and parameter belongs to at least one group.
Group(s) can be provided to the group
keyword argument of app.command
and Parameter
.
Like Parameter
, the Group
class itself only marks objects with metadata; the group does not contain direct references to it's members.
This means that groups can be re-used across commands.
Command Groups
An example of using groups to organize commands:
from cyclopts import App
app = App()
# Change the group of "--help" and "--version" to the implicitly created "Admin" group.
app["--help"].group = "Admin"
app["--version"].group = "Admin"
@app.command(group="Admin")
def info():
"""Print debugging system information."""
print("Displaying system info.")
@app.command
def download(path, url):
"""Download a file."""
print(f"Downloading {url} to {path}.")
@app.command
def upload(path, url):
"""Upload a file."""
print(f"Downloading {url} to {path}.")
app()
$ python my-script.py --help
Usage: my-script.py COMMAND
╭─ Admin ──────────────────────────────────────────────────────────────────────╮
│ info Print debugging system information. │
│ --help,-h Display this message and exit. │
│ --version Display application version. │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Commands ───────────────────────────────────────────────────────────────────╮
│ download Download a file. │
│ upload Upload a file. │
╰──────────────────────────────────────────────────────────────────────────────╯
The default group is defined by the registering app's App.group_commands
, which defaults to a group named "Commands"
.
Parameter Groups
Like commands above, parameter groups allow us to organize parameters on the help page. They also allow us to add additional inter-parameter validators (e.g. mutually-exclusive parameters). An example of using groups with parameters:
from cyclopts import App, Group, Parameter, validators
from typing import Annotated
app = App()
vehicle_type_group = Group(
"Vehicle (choose one)",
default_parameter=Parameter(negative=""), # Disable "--no-" flags
validator=validators.MutuallyExclusive(), # Only one option is allowed to be selected.
)
@app.command
def create(
*, # force all subsequent variables to be keyword-only
# Using an explicitly created group object.
car: Annotated[bool, Parameter(group=vehicle_type_group)] = False,
truck: Annotated[bool, Parameter(group=vehicle_type_group)] = False,
# Implicitly creating an "Engine" group.
hp: Annotated[float, Parameter(group="Engine")] = 200,
cylinders: Annotated[int, Parameter(group="Engine")] = 6,
# You can explicitly create groups in-line.
wheel_diameter: Annotated[float, Parameter(group=Group("Wheels"))] = 18,
# Groups within the function signature can always be referenced with a string.
rims: Annotated[bool, Parameter(group="Wheels")] = False,
):
pass
app()
$ python my-script.py create --help
Usage: my-script.py create [OPTIONS]
╭─ Engine ──────────────────────────────────────────────────────╮
│ --hp [default: 200] │
│ --cylinders [default: 6] │
╰───────────────────────────────────────────────────────────────╯
╭─ Vehicle (choose one) ────────────────────────────────────────╮
│ --car [default: False] │
│ --truck [default: False] │
╰───────────────────────────────────────────────────────────────╯
╭─ Wheels ──────────────────────────────────────────────────────╮
│ --wheel-diameter [default: 18] │
│ --rims --no-rims [default: False] │
╰───────────────────────────────────────────────────────────────╯
$ python my-script.py create --car --truck
╭─ Error ───────────────────────────────────────────────────────╮
│ Invalid values for group "Vehicle (choose one)". Mutually │
│ exclusive arguments: {--car, --truck} │
╰───────────────────────────────────────────────────────────────╯
In this example, we use the MutuallyExclusive
validator to make it so the user can only specify --car
or --truck
.
The default groups are defined by the registering app:
App.group_arguments
for positional-only arguments, which defaults to a group named"Arguments"
.App.group_parameters
for all other parameters, which defaults to a group named"Parameters"
.
Validators
Group validators offer a way of jointly validating group parameter members of CLI-provided values.
Groups with an empty name, or with show=False
, are a way of using group validators without impacting the help-page.
from cyclopts import App, Group, Parameter, validators
from typing import Annotated
app = App()
mutually_exclusive = Group(
# This Group has no name, so it won't impact the help page.
validator=validators.MutuallyExclusive(),
# show_default=False - Showing "[default: False]" isn't too meaningful for mutually-exclusive options.
# negative="" - Don't create a "--no-" flag
default_parameter=Parameter(show_default=False, negative=""),
)
@app.command
def foo(
car: Annotated[bool, Parameter(group=(app.group_parameters, mutually_exclusive))] = False,
truck: Annotated[bool, Parameter(group=(app.group_parameters, mutually_exclusive))] = False,
):
print(f"{car=} {truck=}")
app()
$ python demo.py foo --help
Usage: demo.py foo [ARGS] [OPTIONS]
╭─ Parameters ──────────────────────────────────────────────────────╮
│ CAR,--car │
│ TRUCK,--truck │
╰───────────────────────────────────────────────────────────────────╯
$ python demo.py foo --car
car=True truck=False
$ python demo.py foo --truck
car=False truck=True
$ python demo.py foo --car --truck
╭─ Error ───────────────────────────────────────────────────────────╮
│ Mutually exclusive arguments: {--car, --truck} │
╰───────────────────────────────────────────────────────────────────╯
See Group.validator
for details.
Cyclopts has some builtin group-validators for common use-cases.
Help Page
Groups form titled panels on the help-page.
Groups with an empty name, or with show=False
, are not shown on the help-page.
This is useful for applying additional grouping logic (such as applying a LimitedChoice
validator) without impacting the help-page.
By default, the ordering of panels is alphabetical.
However, the sorting can be manipulated by Group.sort_key
. See it's documentation for usage.
The Group.create_ordered()
convenience classmethod creates a Group
with a sort_key
value drawn drawn from a global monotonically increasing counter.
This means that the order in the help-page will match the order that the groups were instantiated.
from cyclopts import App, Group
app = App()
plants = Group.create_ordered("Plants")
animals = Group.create_ordered("Animals")
fungi = Group.create_ordered("Fungi")
@app.command(group=animals)
def zebra():
pass
@app.command(group=plants)
def daisy():
pass
@app.command(group=fungi)
def portobello():
pass
app()
$ my-script --help
Usage: scratch.py COMMAND
╭─ Plants ───────────────────────────────────────────────────────────╮
│ daisy │
╰────────────────────────────────────────────────────────────────────╯
╭─ Animals ──────────────────────────────────────────────────────────╮
│ zebra │
╰────────────────────────────────────────────────────────────────────╯
╭─ Fungi ────────────────────────────────────────────────────────────╮
│ portobello │
╰────────────────────────────────────────────────────────────────────╯
╭─ Commands ─────────────────────────────────────────────────────────╮
│ --help -h Display this message and exit. │
│ --version Display application version. │
╰────────────────────────────────────────────────────────────────────╯
Even when using Group.create_ordered()
, a sort_key
can still be supplied; the global counter will only be used to break sorting ties.