diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index fc3e8e9..ac8e2da 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,6 +34,7 @@ jobs: path: ~/.cache/pip - name: Install pre-requisites (e.g. hatch) run: python -m pip install --require-hashes --requirement=.github/requirements/ci.txt + - run: hatch run docs:cli - run: hatch run docs:build if: github.event_name == 'pull_request' - run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a09a2aa..8cde3de 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -27,7 +27,7 @@ jobs: - name: Install pre-requisites (e.g. hatch) run: python -m pip install --require-hashes --requirement=.github/requirements/ci.txt - name: Run test suite - run: hatch run cov + run: hatch run cov -s - name: Generate coverage report run: | export TOTAL_COV=$(hatch run cov-total) @@ -70,7 +70,8 @@ jobs: python-version: '3.12' cache: pip - run: python -Im pip install --editable .[dev] - - run: python -Ic 'import re3data.__about__; print(re3data.__about__.__version__)' + - run: python -Ic 'import re3data; print(re3data.__version__)' + - run: re3data --version - name: Set up pipdeptree if: matrix.os == 'ubuntu-latest' run: python -m pip install --require-hashes --requirement=.github/requirements/ci.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 68d2a4d..6474a01 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,7 +55,7 @@ repos: hooks: - id: mdformat args: [--number, --wrap=120, --ignore-missing-references] - exclude: CHANGELOG.md|.changelog.md|docs/src/api + exclude: CHANGELOG.md|.changelog.md|docs/src/cli.md additional_dependencies: - mdformat-mkdocs[recommended]>=v2.0.7 @@ -73,7 +73,8 @@ repos: args: [--config-file=pyproject.toml] additional_dependencies: - httpx>=0.27 - - pytest>=8.1 + - pytest>=8.2 + - typer>=0.12 - repo: https://github.com/scientific-python/cookie rev: 28d1a53da26f9daff6d9a49c50260421ad6d05e0 # frozen: 2024.04.23 diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 228b0a0..42225f5 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -15,6 +15,7 @@ watch: nav: - Home: index.md +- CLI Reference: cli.md - Meta: - Contributor Guide: contributing.md - License: license.md diff --git a/docs/src/cli.md b/docs/src/cli.md new file mode 100644 index 0000000..0ceec82 --- /dev/null +++ b/docs/src/cli.md @@ -0,0 +1,16 @@ +# CLI Reference + +python-re3data. + +**Usage**: + +```console +$ re3data [OPTIONS] COMMAND [ARGS]... +``` + +**Options**: + +* `-V, --version`: Show python-re3data version and exit. +* `--install-completion`: Install completion for the current shell. +* `--show-completion`: Show completion for the current shell, to copy it or customize the installation. +* `--help`: Show this message and exit. diff --git a/pyproject.toml b/pyproject.toml index 4118802..0cb8c9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,7 @@ optional-dependencies.cli = [ ] optional-dependencies.dev = [ "pre-commit~=3.7", + "python-re3data[cli]", ] optional-dependencies.docs = [ "mike~=2.1", @@ -112,10 +113,12 @@ cov-total = """ [tool.hatch.envs.docs] features = [ + "cli", "docs", ] template = "docs" [tool.hatch.envs.docs.scripts] +cli = "typer src/re3data/_cli.py utils docs --name=re3data --title='CLI Reference' --output docs/src/cli.md" build = "mkdocs build --config-file=docs/mkdocs.yml" serve = "mkdocs serve --verbose --config-file=docs/mkdocs.yml" deploy = "mike deploy --push --update-aliases $(hatch version) latest --config-file=docs/mkdocs.yml" @@ -220,6 +223,7 @@ source = [ ] omit = [ "__about__.py", + "__main__.py", ] [tool.coverage.report] diff --git a/tests/test_version.py b/src/re3data/__main__.py similarity index 50% rename from tests/test_version.py rename to src/re3data/__main__.py index ee64300..5958f12 100644 --- a/tests/test_version.py +++ b/src/re3data/__main__.py @@ -2,8 +2,8 @@ # # SPDX-License-Identifier: MIT -from re3data import __version__ +"""Entry point for python-re3data.""" +from re3data._cli import app -def test_version() -> None: - assert __version__ +app(prog_name="re3data") diff --git a/src/re3data/_cli.py b/src/re3data/_cli.py new file mode 100644 index 0000000..bab16f4 --- /dev/null +++ b/src/re3data/_cli.py @@ -0,0 +1,41 @@ +"""re3data command line interface.""" + +import logging +import sys +import typing + +logger = logging.getLogger(__name__) + +try: + import typer +except ImportError: + logger.error("`typer` is missing. Please run 'pip install python-re3data[cli]' to use the CLI.") + sys.exit(1) + +CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]} + +app = typer.Typer(no_args_is_help=True, context_settings=CONTEXT_SETTINGS) + + +def _version_callback(show_version: bool) -> None: + # Ref: https://typer.tiangolo.com/tutorial/options/version/ + from re3data import __version__ + + if show_version: + typer.echo(f"{__version__}") + raise typer.Exit + + +@app.callback(context_settings=CONTEXT_SETTINGS) +def callback( + version: typing.Annotated[ + bool, + typer.Option( + "--version", + "-V", + help="Show python-re3data version and exit.", + callback=_version_callback, + ), + ] = False, +) -> None: + """python-re3data.""" diff --git a/src/re3data/py.typed b/src/re3data/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/test_cli.py b/tests/integration/test_cli.py new file mode 100644 index 0000000..bd16588 --- /dev/null +++ b/tests/integration/test_cli.py @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2024 Heinz-Alexander Fütterer +# +# SPDX-License-Identifier: MIT + +import sys +from importlib import reload +from unittest.mock import patch + +import pytest +from typer.testing import CliRunner + +from re3data import __version__ +from re3data._cli import app + +runner = CliRunner() + +HELP_OPTION_NAMES = ("-h", "--help") +VERSION_OPTION_NAMES = ("-V", "--version") + + +def test_no_args_displays_help() -> None: + result = runner.invoke(app) + assert result.exit_code == 0 + assert "Usage" in result.output + assert "Options" in result.output + assert "Show this message and exit" in result.output + + +@pytest.mark.parametrize("help_option_name", HELP_OPTION_NAMES) +def test_help_option_displays_help(help_option_name: str) -> None: + result = runner.invoke(app, [help_option_name]) + assert result.exit_code == 0 + assert "Usage" in result.output + assert "Options" in result.output + assert "Show this message and exit" in result.output + + +@pytest.mark.parametrize("version_option_name", VERSION_OPTION_NAMES) +def test_version_option_displays_version(version_option_name: str) -> None: + result = runner.invoke(app, [version_option_name]) + assert result.exit_code == 0 + assert __version__ in result.output + + +def test_typer_missing_message(caplog: pytest.LogCaptureFixture) -> None: + # act as if "typer" is not installed + with patch.dict(sys.modules, {"typer": None}): + with pytest.raises(SystemExit) as exc_info: + reload(sys.modules["re3data._cli"]) + assert exc_info.type == SystemExit + assert exc_info.value.code == 1 + assert "Please run 'pip install python-re3data[cli]'" in caplog.text