Meta App

What if you want more control over the application launch process? Cyclopts provides the option of launching an app from an app; a meta app!

Meta Sub App

Typically, a Cyclopts application is launched by calling the App object:

from cyclopts import App

app = App()
# Register some commands here (not shown)
app()  # Run the app

To change how the primary app is run, you can use the meta-app feature of Cyclopts.

from cyclopts import App, Group, Parameter
from typing_extensions import Annotated

app = App()
# Rename the meta's "Parameter" -> "Session Parameters".
# Set sort_key so it will be drawn higher up the help-page.
app.meta.group_parameters = Group("Session Parameters", sort_key=0)


@app.command
def foo(loops: int):
    for i in range(loops):
        print(f"Looping! {i}")


@app.meta.default
def my_app_launcher(*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], user: str):
    print(f"Hello {user}")
    app(tokens)


app.meta()
$ my-script --user=Bob foo 3
Hello Bob
Looping! 0
Looping! 1
Looping! 2

The variable positional *tokens will aggregate all remaining tokens, including those starting with a hyphen (typically options). We can then pass them along to the primary app.

The meta app is mostly a normal Cyclopts app; the only thing special about it is that it will be additionally scanned when generate help screens *tokens is annotated with show=False since we do not want this variable to show up in the help screen.

$ my-script --help
Usage: my-script COMMAND

╭─ Session Parameters ────────────────────────────────────────────────────╮
│ *  --user  [required]                                                   │
╰─────────────────────────────────────────────────────────────────────────╯
╭─ Commands ──────────────────────────────────────────────────────────────╮
│ foo                                                                     │
│ --help,-h  Display this message and exit.                               │
│ --version  Display application version.                                 │
╰─────────────────────────────────────────────────────────────────────────╯

Meta Commands

If you want a command to circumvent my_app_launcher, add it as you would any other command to the meta app.

@app.meta.command
def info():
    print("CLI didn't have to provide --user to call this.")
$ my-script info
CLI didn't have to provide --user to call this.

$ my-script --help
Usage: my-script COMMAND

╭─ Session Parameters ────────────────────────────────────────────────────╮
│ *  --user  [required]                                                   │
╰─────────────────────────────────────────────────────────────────────────╯
╭─ Commands ──────────────────────────────────────────────────────────────╮
│ foo                                                                     │
│ info                                                                    │
│ --help,-h  Display this message and exit.                               │
│ --version  Display application version.                                 │
╰─────────────────────────────────────────────────────────────────────────╯

Just like a standard application, the parsed command executes instead of default.

Custom Command Invocation

The core logic of App.__call__() method is the following:

def __call__(self, tokens=None, **kwargs):
    tokens = normalize_tokens(tokens)
    command, bound = self.parse_args(tokens, **kwargs)
    return command(*bound.args, **bound.kwargs)

Knowing this, we can easily customize how we actually invoke actions with Cyclopts. Let's imagine that we want to instantiate an object, User in our meta app, and pass it to all subsequent commands. This might be useful to share an expensive-to-create object amongst commands in a single session; see Command Chaining.

from cyclopts import App, Parameter
from typing_extensions import Annotated

app = App()


class User:
    def __init__(self, name):
        self.name = name


@app.command
def create(
    age: int,
    *,
    user_obj: Annotated[User, Parameter(parse=False)],
):
    print(f"Creating user {user_obj.name} with age {age}.")


@app.meta.default
def launcher(*tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)], user: str):
    user_obj = User(user)
    command, bound = app.parse_args(tokens)
    return command(*bound.args, **bound.kwargs, user_obj=user_obj)


if __name__ == "__main__":
    app.meta()
$ my-script create --user Alice 30
Creating user Alice with age 30.

The parse=False configuration tells Cyclopts to not try and bind arguments to this parameter. The annotated parameter must be a keyword-only parameter.