Commands

There are two different ways of registering functions:

  1. app.default - Registers an action for when no registered command is provided. This was previously demonstrated in Getting Started.

    A sub-app cannot be registered with app.default. If no default command is registered, Cyclopts will display the help-page.

  2. app.command - Registers a function or App as a command.

This section will detail how to use the @app.command decorator.

Registering a Command

The @app.command decorator adds a command to a Cyclopts application.

from cyclopts import App

app = App()

@app.command
def fizz(n: int):
    print(f"FIZZ: {n}")

@app.command
def buzz(n: int):
    print(f"BUZZ: {n}")

app()

We can now control which command runs from the CLI:

$ my-script fizz 3
FIZZ: 3

$ my-script buzz 4
BUZZ: 4

$ my-script fuzz
╭─ Error ────────────────────────────────────────────────────────────────────╮
│ Unknown command "fuzz". Did you mean "fizz"?                               │
╰────────────────────────────────────────────────────────────────────────────╯

Registering a SubCommand

The app.command method can also register another Cyclopts App as a command.

from cyclopts import App

app = App()
sub_app = App(name="foo")  # "foo" would be a better variable name than "sub_app".
# "sub_app" in this example emphasizes the name comes from name="foo".
app.command(sub_app)  # Registers sub_app to command "foo"
# Or, as a one-liner:  sub_app = app.command(App(name="foo"))


@sub_app.command
def bar(n: int):
    print(f"BAR: {n}")


# Alternatively, access subapps from app like a dictionary.
@app["foo"].command
def baz(n: int):
    print(f"BAZ: {n}")


app()
$ my-script foo bar 3
BAR: 3

$ my-script foo baz 4
BAZ: 4

The subcommand may have their own registered default action. Cyclopts's command structure is fully recursive.

Flattening SubCommands

Sometimes you want to make all commands from a sub-app directly accessible from the parent app, without requiring users to type the intermediate subcommand name.

You can flatten a sub-app by registering it with the special name="*":

from cyclopts import App

app = App()
tools_app = App(name="tools")

@tools_app.command
def compress(file: str):
    print(f"Compressing {file}")

@tools_app.command
def extract(file: str):
    print(f"Extracting {file}")

# Flatten: make all tools_app commands directly accessible
app.command(tools_app, name="*")

app()
$ my-script compress data.txt
Compressing data.txt

$ my-script extract archive.zip
Extracting archive.zip

Caveats of flattening:

  • Parent app commands take precedence over flattened commands if there are name collisions.

  • Multiple sub-apps can be flattened into the same parent app.

  • You cannot supply additional configuration kwargs when using name="*".

  • Only App instances can be flattened (not functions or import paths).

Flattening is useful for organizing related commands into logical groups in your code while keeping the CLI interface simple and flat.

SubCommand Configuration

Subcommands inherit configuration from their parent apps.

from cyclopts import App

# Root app with specific error handling
root_app = App(
    exit_on_error=False,
    print_error=False,
)

# Child app inherits parent's settings
child_app = root_app.command(App(name="child"))

@child_app.default
def child_action():
    return "Child executed successfully"

# Child can override parent settings if needed
grandchild_app = child_app.command(App(name="grandchild", exit_on_error=True))

When parent_app("child ...") is called, the child command will use the parent's error handling settings unless explicitly overridden.

Changing Command Name

By default, commands are registered to the python function's name with underscores replaced with hyphens. Any leading or trailing underscores will be stripped. For example, the function _foo_bar() will become the command foo-bar. This renaming is done because CLI programs generally tend to use hyphens instead of underscores. The name transform can be configured by App.name_transform. For example, to make CLI command names be identical to their python function name counterparts, we can configure App as follows:

from cyclopts import App

app = App(name_transform=lambda s: s)

@app.command
def foo_bar():  # will now be "foo_bar" instead of "foo-bar"
    print("running function foo_bar")

app()
$ my-script foo_bar
running function foo_bar

Alternatively, the name can be manually changed in the @app.command decorator. Manually set names are not subject to App.name_transform.

from cyclopts import App

app = App()

@app.command(name="bar")
def foo():  # function name will NOT be used.
    print("Hello World!")

app()
$ my-script bar
Hello World!

Finally, if you would like to register an additional name to the Cyclopts-derived names, you can set an alias:

from cyclopts import App

app = App()

@app.command(alias="bar")
def foo():  # both "foo" and "bar" will trigger this function.
    print("Running foo.")

app()
$ my-script foo
Running bar.

$ my-script bar
Running bar.

Adding Help

There are a few ways to add a help string to a command:

  1. If the function has a docstring, the short description will be used as the help string for the command. This is generally the preferred method of providing help strings.

  2. If the registered command is a sub app, the sub app's help field will be used.

    sub_app = App(name="foo", help="Help text for foo.")
    app.command(sub_app)
    
  3. The help field of @app.command. If provided, the docstring or subapp help field will not be used.

    from cyclopts import App
    
    app = App()
    
    @app.command
    def foo():
        """Help string for foo."""
        pass
    
    @app.command(help="Help string for bar.")
    def bar():
        """This got overridden."""
    
    app()
    
    $ my-script --help
    ╭─ Commands ────────────────────────────────────────────────────────────╮
    │ bar        Help string for bar.                                       │
    │ foo        Help string for foo.                                       │
    │ --help,-h  Display this message and exit.                             │
    │ --version  Display application version.                               │
    ╰───────────────────────────────────────────────────────────────────────╯
    

Async

Cyclopts also works with async commands; when an async command is encountered, an event loop will be automatically created using the specified backend parameter (default asyncio).

import asyncio
from cyclopts import App

app = App()

@app.command
async def foo():
    await asyncio.sleep(10)

app()

When calling from within an existing async context, await the async method run_async():

async def main():
    result = await app.run_async(["foo"])
    # Instead of: app(["foo"]) which would raise RuntimeError

Decorated Function Details

Cyclopts does not modify the decorated function in any way. The returned function is the exact same function being decorated and can be used exactly as if it were not decorated by Cyclopts.

See Also

For improved CLI startup performance with large applications, see Lazy Loading.