diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3185d7f..5a53725 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -20,7 +20,7 @@ that DevRepro should already have redacted usernames/home paths/secrets. **Environment** - OS + version: - Shell: -- DevRepro version: +- DevRepro version (`devrepro --version`): **Additional context** Anything else relevant. \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 708a09b..5e33a37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,9 +31,9 @@ jobs: python -m pip install --upgrade pip pip install -e ".[dev]" - name: Ruff lint - run: ruff check devrepro tests + run: ruff check devrepro tests scripts - name: Format check - run: ruff format --check devrepro tests + run: ruff format --check devrepro tests scripts - name: Type check (mypy) run: mypy devrepro - name: Tests + coverage diff --git a/devrepro/cli/app.py b/devrepro/cli/app.py index f8e77e5..2f8a8fd 100644 --- a/devrepro/cli/app.py +++ b/devrepro/cli/app.py @@ -15,6 +15,7 @@ import typer +from devrepro import __version__ from devrepro.cli.commands import register_all app = typer.Typer( @@ -27,6 +28,34 @@ register_all(app) +def _version_callback(value: bool) -> None: + """Print the installed version and exit. + + Eager, so it answers before Typer demands a subcommand — the bug-report + template asks reporters for this and it has to work on its own. + """ + if value: + typer.echo(f"devrepro {__version__}") + raise typer.Exit + + +@app.callback() +def _root( + version: bool = typer.Option( + False, + "--version", + callback=_version_callback, + is_eager=True, + help="Show the installed devrepro version and exit.", + ), +) -> None: + """Project-aware developer-environment diagnostics and safe remediation. + + This callback exists so ``--version`` has somewhere to live; Typer needs a + root callback to hang a group-level option on. + """ + + def main() -> None: try: app() diff --git a/tests/test_version_is_reportable.py b/tests/test_version_is_reportable.py new file mode 100644 index 0000000..ced3e61 --- /dev/null +++ b/tests/test_version_is_reportable.py @@ -0,0 +1,47 @@ +"""`devrepro --version` has to work, and report the real version. + +`__version__` was already derived from installed metadata (see +`devrepro/__init__.py`), but nothing exposed it on the command line: the flag +exited 2 with "No such option". A version you cannot ask the tool for is not +much use to a bug reporter. +""" + +from __future__ import annotations + +import pathlib +import tomllib + +from devrepro import __version__ +from devrepro.cli.app import app +from typer.testing import CliRunner + +runner = CliRunner() +PYPROJECT = pathlib.Path(__file__).resolve().parents[1] / "pyproject.toml" + + +def _declared_version() -> str: + data = tomllib.loads(PYPROJECT.read_text(encoding="utf-8")) + return str(data["project"]["version"]) + + +def test_package_version_matches_pyproject() -> None: + assert __version__ == _declared_version() + + +def test_version_flag_exits_zero_and_prints_the_version() -> None: + result = runner.invoke(app, ["--version"]) + assert result.exit_code == 0, result.output + assert __version__ in result.output + + +def test_version_flag_does_not_require_a_subcommand() -> None: + """The app is `no_args_is_help=True`; an eager callback must still answer.""" + result = runner.invoke(app, ["--version"]) + assert "Usage:" not in result.output + + +def test_adding_the_callback_did_not_hide_the_commands() -> None: + result = runner.invoke(app, ["--help"]) + assert result.exit_code == 0 + for expected in ("doctor", "check", "preflight", "snapshot"): + assert expected in result.output