# Cyclopts

Documentation: https://cyclopts.readthedocs.io

Source Code: https://github.com/BrianPugh/cyclopts

Cyclopts is a modern, easy-to-use command-line interface (CLI) framework that aims to provide an intuitive & efficient developer experience.

## Why Cyclopts?

- Intuitive API: Quickly write CLI applications using a terse, intuitive syntax.

- Advanced Type Hinting: Full support of all builtin types and even user-specified (yes, including Pydantic, Dataclasses, and Attrs).

- Rich Help Generation: Automatically generates beautiful help pages from docstrings and other contextual data.

- Extendable: Easily customize converters, validators, token parsing, and application launching.

## Installation

Cyclopts requires Python >=3.10; to install Cyclopts, run:

    pip install cyclopts

## Quick Start

- Import `cyclopts.run()` and give it a function to run.

    from cyclopts import run

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

    run(foo)

Execute the script from the command line:

    $ python start.py 3
    Looping! 0
    Looping! 1
    Looping! 2

When you need more control:

- Create an application using `cyclopts.App`.

- Register commands with the `command` decorator.

- Register a default function with the `default` decorator.

    from cyclopts import App

    app = App()

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

    @app.default
    def default_action():
        print("Hello world! This runs when no command is specified.")

    app()

Execute the script from the command line:

    $ python demo.py
    Hello world! This runs when no command is specified.

    $ python demo.py foo 3
    Looping! 0
    Looping! 1
    Looping! 2

With just a few additional lines of code, we have a full-featured CLI app. See the docs for more advanced usage.

## Compared to Typer

Cyclopts is what you thought Typer was. Cyclopts's includes information from docstrings, support more complex types (even Unions and Literals!), and include proper validation support. See the documentation for a complete Typer comparison.

Consider the following short 29-line Cyclopts application:

    import cyclopts
    from typing import Literal

    app = cyclopts.App()

    @app.command
    def deploy(
        env: Literal["dev", "staging", "prod"],
        replicas: int | Literal["default", "performance"] = "default",
    ):
        """Deploy code to an environment.

        Parameters
        ----------
        env
            Environment to deploy to.
        replicas
            Number of workers to spin up.
        """
        if replicas == "default":
            replicas = 10
        elif replicas == "performance":
            replicas = 20

        print(f"Deploying to {env} with {replicas} replicas.")

    if __name__ == "__main__":
        app()

    $ my-script deploy --help
    Usage: my-script.py deploy [ARGS] [OPTIONS]

    Deploy code to an environment.

    ╭─ Parameters ────────────────────────────────────────────────────────────────────────────────────╮
    │ *  ENV --env            Environment to deploy to. [choices: dev, staging, prod] [required]      │
    │    REPLICAS --replicas  Number of workers to spin up. [choices: default, performance] [default: │
    │                         default]                                                                │
    ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯

    $ my-script deploy staging
    Deploying to staging with 10 replicas.

    $ my-script deploy staging 7
    Deploying to staging with 7 replicas.

    $ my-script deploy staging performance
    Deploying to staging with 20 replicas.

    $ my-script deploy nonexistent-env
    ╭─ Error ────────────────────────────────────────────────────────────────────────────────────────────╮
    │ Error converting value "nonexistent-env" to typing.Literal['dev', 'staging', 'prod'] for "--env".  │
    ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯

    $ my-script --version
    0.0.0

In its current state, this application would be impossible to implement in Typer. However, lets see how close we can get with Typer (47-lines):

    import typer
    from typing import Annotated, Literal
    from enum import Enum

    app = typer.Typer()

    class Environment(str, Enum):
        dev = "dev"
        staging = "staging"
        prod = "prod"

    def replica_parser(value: str):
        if value == "default":
            return 10
        elif value == "performance":
            return 20
        else:
            return int(value)

    def _version_callback(value: bool):
        if value:
            print("0.0.0")
            raise typer.Exit()

    @app.callback()
    def callback(
        version: Annotated[
            bool | None, typer.Option("--version", callback=_version_callback)
        ] = None,
    ):
        pass

    @app.command(help="Deploy code to an environment.")
    def deploy(
        env: Annotated[Environment, typer.Argument(help="Environment to deploy to.")],
        replicas: Annotated[
            int,
            typer.Argument(
                parser=replica_parser,
                help="Number of workers to spin up.",
            ),
        ] = replica_parser("default"),
    ):
        print(f"Deploying to {env.name} with {replicas} replicas.")

    if __name__ == "__main__":
        app()

    $ my-script deploy --help

    Usage: my-script deploy [OPTIONS] ENV:{dev|staging|prod} [REPLICAS]

     Deploy code to an environment.

    ╭─ Arguments ─────────────────────────────────────────────────────────────────────────────────────╮
    │ *    env           ENV:{dev|staging|prod}  Environment to deploy to. [default: None] [required] │
    │      replicas      [REPLICAS]              Number of workers to spin up. [default: 10]          │
    ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯
    ╭─ Options ───────────────────────────────────────────────────────────────────────────────────────╮
    │ --help          Show this message and exit.                                                     │
    ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯

    $ my-script deploy staging
    Deploying to staging with 10 replicas.

    $ my-script deploy staging 7
    Deploying to staging with 7 replicas.

    $ my-script deploy staging performance
    Deploying to staging with 20 replicas.

    $ my-script deploy nonexistent-env
    Usage: my-script.py deploy [OPTIONS] ENV:{dev|staging|prod} [REPLICAS]
    Try 'my-script.py deploy --help' for help.
    ╭─ Error ─────────────────────────────────────────────────────────────────────────────────────────╮
    │ Invalid value for '[REPLICAS]': nonexistent-env                                                 │
    ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯

    $ my-script --version
    0.0.0

The Typer implementation is 47 lines long, while the Cyclopts implementation is just 29 (38% shorter!). Not only is the Cyclopts implementation significantly shorter, but the code is easier to read. Since Typer does not support Unions, the choices for `replica` could not be displayed on the help page. Cyclopts is much more terse, much more readable, and much more intuitive to use.

## Contributing

Contributions are welcome! See CONTRIBUTING.md for development setup, coding standards, and how to submit a pull request.

For extensive documentation on all the features Cyclopts has to offer, checkout the API page.

Usage

- Installation
- Getting Started
- Commands
- Parameters
- Default Parameter
- Groups
- Parameter Validators
- Group Validators
- Help
- Version
- Shell Completion
- Coercion Rules
- Text Editor
- API
- CLI Reference
- Known Issues

Advanced Usage

- Lazy Loading
  - Basic Usage
  - Import Path Format
  - Name vs Function Name
  - Error Handling
  - Groups and Lazy Loading
- Help Customization
  - Setting Help Formatters
  - Built-in Formatters
  - Basic Customization
  - Group-Level Formatting
  - Custom Column Layout
  - Creating Custom Formatters
  - Reference
- User Classes
  - Basic Example
  - JSON Dict Parsing
  - JSON List Parsing
  - Namespace Flattening
  - Sharing Parameters
  - Config File
- Args & Kwargs
  - Args (Variable Positional)
  - Kwargs (Variable Keywords)
- Config Files
  - TOML Example
  - User-Specified Config File
  - Environment Variable Example
  - In-Memory Dict
  - Combining Multiple Config Sources
- Sphinx Integration
  - Quick Start
  - Directive Usage
  - Directive Options
  - Complete Example
  - Advanced Usage
  - Output Formats
  - See Also
- MkDocs Integration
  - Quick Start
  - Directive Usage
  - Directive Options
  - Complete Example
  - Advanced Usage
  - See Also
- Packaging
  - __main__.py
  - Entrypoints
  - Result Action
- App Calling & Return Values
  - Input Command
  - Return Value
  - Exception Handling and Exiting
  - Custom Error Formatting
- Meta App
  - Meta Sub App
  - Meta Commands
  - Custom Command Invocation
- Command Chaining
- AutoRegistry

Cookbook

- App Upgrade
- Dataclass Commands
  - Basic Example
  - How It Works
- Interactive Shell & Help
- Rich Formatted Exceptions
  - Standard Python Traceback
  - Rich Formatted Traceback
- Sharing Parameters
- Unit Testing
  - Mocking
  - Exit Codes
  - Checking stdout
  - Environment Variables
  - File Config
  - Help Page
- Reading/Writing From File or Stdin/Stdout
  - StdioPath
  - Alternative Approach (Python < 3.12)
- Random Tips
  - Basic Usage

Migration

- Migrating From Typer
  - General Steps

Alternative Libraries

- Typer Comparison
  - Argument vs Option
  - Positional or Keyword Arguments
  - Choices
  - Default Command
  - Docstring Parsing
  - Decorator Parentheses
  - Optional Lists
  - Keyword Multiple Values
  - Flag Negation
  - Help Defaults
  - Validation
  - Union/Optional Support
  - Adding a Version Flag
  - Documentation
- Fire Comparison
- Arguably Comparison
  - Global State
  - Subcommands