diff --git a/copier.yml b/copier.yml index 6902e20..9669ae9 100644 --- a/copier.yml +++ b/copier.yml @@ -87,7 +87,7 @@ year: python_min: type: int help: Minimal minor version of Python, i.e '14' for 3.14 - default: 10 + default: 11 validator: >- {% if python_min < 8 %}Must be >= 8{% endif %} @@ -109,6 +109,8 @@ cli_framework: No CLI: none Typer: typer Cyclopts: cyclopts + validator: >- + {% if cli_framework == 'cyclopts' and python_min < 11 %}Cyclopts requires python_min >= 11{% endif %} has_cli: type: bool diff --git a/project_name/pyproject.toml.jinja b/project_name/pyproject.toml.jinja index b436496..f6d5057 100644 --- a/project_name/pyproject.toml.jinja +++ b/project_name/pyproject.toml.jinja @@ -51,7 +51,7 @@ dependencies = [ {%- if cli_framework == "typer" %} "typer >=0.20", {%- elif cli_framework == "cyclopts" %} - "cyclopts >=4", + "cyclopts >=5", {%- endif %} ] diff --git a/project_name/src/{{package_name}}/{% if cli_framework == 'cyclopts' %}cli.py{% endif %}.jinja b/project_name/src/{{package_name}}/{% if cli_framework == 'cyclopts' %}cli.py{% endif %}.jinja index ec319d2..73190b9 100644 --- a/project_name/src/{{package_name}}/{% if cli_framework == 'cyclopts' %}cli.py{% endif %}.jinja +++ b/project_name/src/{{package_name}}/{% if cli_framework == 'cyclopts' %}cli.py{% endif %}.jinja @@ -9,7 +9,7 @@ from collections.abc import Sequence from typing import Annotated, NoReturn import cyclopts.types -from cyclopts import App, CycloptsError, Parameter +from cyclopts import App, Parameter app = App(name="{{ project_name }}") app.register_install_completion_command() @@ -49,12 +49,9 @@ def do_something( # Maps the commands above onto exit codes, and is what `[project.scripts]` and # `__main__` both call. -# sysexits(3) would put usage errors at 64, but 2 is the far wider convention: -# argparse, click, clap, grep, diff, curl and bash builtins all use it. -# https://stackoverflow.com/questions/1101957/are-there-any-standard-exit-status-codes-in-linux -EX_USAGE = 2 -# The rest are sysexits(3) codes. `os.EX_*` holds the same values but only -# exists on Unix, so they are inlined to keep the CLI importable on Windows. +# Cyclopts itself exits 2 on invalid usage. These are sysexits(3) codes. +# `os.EX_*` holds the same values but only exists on Unix, so they are inlined +# to keep the CLI importable on Windows. EX_NOINPUT = 66 EX_UNAVAILABLE = 69 EX_SOFTWARE = 70 @@ -77,12 +74,7 @@ def main(tokens: Sequence[str] | None = None) -> None: # `tokens` is a parameter so that tests can pass a command line here. # Under pytest, a bare `app()` warns, since it would parse pytest's own # argv, and a test that does so passes while testing nothing. - app(tokens, exit_on_error=False) - except CycloptsError: - # Cyclopts has already printed its own error panel. Cyclopts >=5 exits 2 - # on parse errors itself, so once the dependency requires it, this clause - # and `exit_on_error=False` above can both go. - sys.exit(EX_USAGE) + app(tokens) # Nothing reports the errors below, so without `_fail` the CLI would exit on # a bare code and no output. Match on the exception rather than on # `type(exc)`, so that subclasses such as ConnectionRefusedError still land diff --git a/project_name/tests/{% if cli_framework == 'cyclopts' %}test_cli.py{% endif %}.jinja b/project_name/tests/{% if cli_framework == 'cyclopts' %}test_cli.py{% endif %}.jinja index f96d81c..1876e85 100644 --- a/project_name/tests/{% if cli_framework == 'cyclopts' %}test_cli.py{% endif %}.jinja +++ b/project_name/tests/{% if cli_framework == 'cyclopts' %}test_cli.py{% endif %}.jinja @@ -1,7 +1,7 @@ import pytest from {{ package_name }} import __version__, cli -from {{ package_name }}.cli import EX_SOFTWARE, EX_USAGE, app, main +from {{ package_name }}.cli import EX_SOFTWARE, app, main def test_version(capsys: pytest.CaptureFixture[str]) -> None: @@ -21,7 +21,9 @@ def test_app(capsys: pytest.CaptureFixture[str]) -> None: def test_main_usage_error() -> None: with pytest.raises(SystemExit) as exc_info: main(["--not-an-option"]) - assert exc_info.value.code == EX_USAGE + # Cyclopts >=5 exits 2 on invalid usage, as argparse, click and clap do. + # sysexits(3) would say 64, but 2 is the far wider convention. + assert exc_info.value.code == 2 def test_main_unhandled_error(monkeypatch: pytest.MonkeyPatch) -> None: