Config Files

For more complicated CLI applications, it is common to have an external user configuration file. For example, the popular python tools poetry, ruff, and pytest are all configurable from a pyproject.toml file. The App.config attribute accepts a callable (or list of callables) that add (or remove) values to the parsed CLI tokens. The provided callable must have signature:

def config(app: "App", commands: Tuple[str, ...], arguments: ArgumentCollection):
    """Modifies the argument collection inplace with some injected values.

    Parameters
    ----------
    app: App
       The current command app being executed.
    commands: Tuple[str, ...]
       The CLI strings that led to the current command function.
    arguments: ArgumentCollection
       Complete ArgumentCollection for the app.
       Modify this collection inplace to influence values provided to the function.
    """
    ...

The provided config does not have to be a function; all the Cyclopts builtin configs are classes that implement the __call__ method. The Cyclopts builtins offer good standard functionality for common configuration files like yaml or toml.

TOML Example

In this example, we create a small CLI tool that counts the number of times a given character occurs in a file.

# character-counter.py
import cyclopts
from cyclopts import App
from pathlib import Path

app = App(
    name="character-counter",
    config=cyclopts.config.Toml(
        "pyproject.toml",  # Name of the TOML File
        root_keys=["tool", "character-counter"],  # The project's namespace in the TOML.
        # If "pyproject.toml" is not found in the current directory,
        # then iteratively search parenting directories until found.
        search_parents=True,
    ),
)

@app.command
def count(filename: Path, *, character="-"):
    print(filename.read_text().count(character))

if __name__ == "__main__":
    app()

Running this code without a pyproject.toml present:

$ python character-counter.py count README.md
70
$ python character-counter.py count README.md --character=t
380

We can have the new default character be t by adding the following to pyproject.toml:

[tool.character-counter.count]
character = "t"

Rerunning the app without a specified --character will result in using the toml-provided value:

$ python character-counter.py count README.md
380

User-Specified Config File

Extending the above TOML Example, what if we want to allow the user to specify the toml configuration file? This can be accomplished via a Meta App.

# character-counter.py
from pathlib import Path
from typing import Annotated

import cyclopts
from cyclopts import App, Parameter

app = App(name="character-counter")

@app.command
def count(filename: Path, *, character="-"):
    print(filename.read_text().count(character))

@app.meta.default
def meta(
    *tokens: Annotated[str, Parameter(show=False, allow_leading_hyphen=True)],
    config: Path = Path("pyproject.toml"),
):
    app.config = cyclopts.config.Toml(
        config,
        root_keys=["tool", "character-counter"],
        search_parents=True,
    )

    app(tokens)

if __name__ == "__main__":
    app.meta()

Environment Variable Example

To automatically derive and read appropriate environment variables, use the cyclopts.config.Env class. Continuing the above TOML example:

# character-counter.py
import cyclopts
from pathlib import Path

app = cyclopts.App(
    name="character-counter",
    config=cyclopts.config.Env(
        "CHAR_COUNTER_",  # Every environment variable will begin with this.
    ),
)

@app.command
def count(filename: Path, *, character="-"):
    print(filename.read_text().count(character))

app()

Env assembles the environment variable name by joining the following components (in-order):

  1. The provided prefix. In this case, it is "CHAR_COUNTER_".

  2. The command and subcommand(s) that lead up to the function being executed.

  3. The parameter's CLI name, with the leading -- stripped, and hyphens - replaced with underscores _.

Running this code without a specified --character results in counting the default - character.

$ python character-counter.py count README.md
70

By exporting a value to CHAR_COUNTER_COUNT_CHARACTER, that value will now be used as the default:

$ export CHAR_COUNTER_COUNT_CHARACTER=t
$ python character-counter.py count README.md
380
$ python character-counter.py count README.md --character=q
3

In-Memory Dict

For configurations that come from sources other than files, use cyclopts.config.Dict.

# character-counter.py
import json
import cyclopts
from pathlib import Path

def fetch_config():
    """Simulate fetching configuration from an API."""
    return {"count": {"character": "e"}}

config_data = fetch_config_from_api()

app = cyclopts.App(
    name="character-counter",
    config=cyclopts.config.Dict(
        fetch_config,
        # Optional: provide custom source identifier for better error messages
        source="api",
    ),
)

@app.command
def count(filename: Path, *, character="-"):
    print(filename.read_text().count(character))

if __name__ == "__main__":
    app()

Combining Multiple Config Sources

You can combine multiple config sources in a single application by passing a sequence to App.config. Each configuration is applied sequentially.

In the following example, we combine a TOML file and environment variables, allowing environment variables to override TOML settings:

# character-counter.py
import cyclopts
from pathlib import Path

app = cyclopts.App(
    name="character-counter",
    config=[
        # Since Env comes before Toml, it has priority.
        cyclopts.config.Env("CHAR_COUNTER_"),
        cyclopts.config.Toml(
            "pyproject.toml",
            root_keys=["tool", "character-counter"],
            search_parents=True,
        ),
    ],
)

@app.command
def count(filename: Path, *, character="-"):
    print(filename.read_text().count(character))

if __name__ == "__main__":
    app()

With this setup, the configuration is resolved in the following order:

  1. CLI arguments (if provided) override everything else

  2. Environment variables (prefixed with CHAR_COUNTER_) can override TOML values

  3. TOML file (pyproject.toml) provides the base configuration

  4. Python default the default value - in the python code.

For example, with pyproject.toml containing:

[tool.character-counter.count]
character = "t"

You can override it via environment variable:

$ CHAR_COUNTER_COUNT_CHARACTER=a python character-counter.py count README.md

Or via CLI argument:

$ python character-counter.py count README.md --character=x