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:

  1. Explicitly creating a Group object.

  2. Implicitly with a string. This will implicitly create a group, Group(my_str_group_name), if it doesn't exist. If there exists a Group 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:

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.