ProvenPin Install

Cyclopts CLI with Annotated types and rich help: working versions (tested 2026-10-15)

A typed Cyclopts command using Annotated Parameter help text and enum/int/list coercion, invoked offline in tests with captured stdout, stderr and return values.

passed6 passed · 0 failed2026-10-15 15:58 UTC
Status
passed
Last passed
2026-10-15 15:58 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions2
cyclopts@5.2.0pytest@9.1.1
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38764699834

The code

Exactly the files that passed. Your agent can pull them with get_recipe("cyclopts-annotated-cli") (install), or see them on GitHub.

requirements.txt
cyclopts==5.2.0
pytest==9.1.1
cli.py
from __future__ import annotations

from enum import Enum
from typing import Annotated

from cyclopts import App, Parameter

app = App(name="tasks", help="Manage a small todo list.")


class Priority(str, Enum):
    low = "low"
    medium = "medium"
    high = "high"


@app.command
def add(
    title: Annotated[str, Parameter(help="Short task description.")],
    *,
    priority: Annotated[Priority, Parameter(help="Task priority.")] = Priority.medium,
    copies: Annotated[int, Parameter(help="How many copies to create.")] = 1,
    tags: Annotated[list[str], Parameter(name="--tag", help="Attach a tag; repeat for many.")] = [],
    verbose: Annotated[bool, Parameter(help="Print extra detail.")] = False,
) -> dict:
    """Add a task to the list."""
    return {
        "title": title,
        "priority": priority.value,
        "copies": copies,
        "tags": tags,
        "verbose": verbose,
    }
tests/test_cli.py
import contextlib
import io
from dataclasses import dataclass

import pytest
from cyclopts import App
from cyclopts.exceptions import CoercionError, MissingArgumentError

from cli import app


@dataclass
class Result:
    return_value: object = None
    stdout: str = ""
    stderr: str = ""
    error: Exception | None = None

    @property
    def exit_code(self) -> int:
        return 2 if self.error is not None else 0


def invoke(cli: App, tokens: list[str]) -> Result:
    """Run a Cyclopts app offline and capture its streams and return value."""
    out, err = io.StringIO(), io.StringIO()
    result = Result()
    try:
        with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
            result.return_value = cli(tokens, exit_on_error=False, result_action="return_value")
    except Exception as exc:  # CycloptsError subclasses
        result.error = exc
    result.stdout, result.stderr = out.getvalue(), err.getvalue()
    return result


def test_annotated_types_are_coerced():
    result = invoke(
        app,
        ["add", "buy milk", "--priority", "high", "--copies", "3", "--tag", "home", "--verbose"],
    )
    assert result.error is None
    assert result.return_value == {
        "title": "buy milk",
        "priority": "high",
        "copies": 3,
        "tags": ["home"],
        "verbose": True,
    }
    assert isinstance(result.return_value["copies"], int)


def test_repeated_option_accumulates_but_commas_do_not_split():
    result = invoke(app, ["add", "x", "--tag", "a,b", "--tag", "c"])
    assert result.return_value["tags"] == ["a,b", "c"]


def test_help_renders_annotated_help_and_choices():
    result = invoke(app, ["add", "--help"])
    assert result.error is None and result.exit_code == 0
    assert "Short task description." in result.stdout
    assert "[choices: low, medium, high]" in result.stdout
    assert "--no-verbose" in result.stdout


def test_missing_required_argument_is_reported_on_stderr():
    result = invoke(app, ["add"])
    assert isinstance(result.error, MissingArgumentError)
    assert result.stdout == ""
    assert "--title" in result.stderr


def test_coercion_errors_are_cyclopts_errors():
    result = invoke(app, ["add", "x", "--priority", "urgent"])
    assert isinstance(result.error, CoercionError)
    assert "low" in result.stderr and "high" in result.stderr


def test_default_app_call_exits_for_production_scripts():
    with pytest.raises(SystemExit) as exc_info:
        app(["add"])  # exit_on_error defaults to True
    assert exc_info.value.code == 2
pytest.ini
[pytest]
pythonpath = .

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.