Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion copier.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion project_name/pyproject.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ dependencies = [
{%- if cli_framework == "typer" %}
"typer >=0.20",
{%- elif cli_framework == "cyclopts" %}
"cyclopts >=4",
"cyclopts >=5",
{%- endif %}
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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:
Expand Down
Loading