diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 417633fe..92d4a8a5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -37,7 +37,7 @@ jobs: - name: Install dependencies run: | pip install poetry~=1.3.0 - poetry install --no-interaction --only main,test + poetry install --no-interaction --only main,test --all-extras - name: Run tests with pytest run: | poetry run pytest --cov=reuse @@ -53,7 +53,6 @@ jobs: - name: Install dependencies run: | pip install poetry~=1.3.0 - pip install shtab poetry install --no-interaction --only main,dev,test - name: Lint with Pylint run: | @@ -87,7 +86,6 @@ jobs: - name: Install dependencies run: | pip install poetry~=1.3.0 - pip install shtab poetry install --no-interaction --only main,dev,test - name: Test typing with mypy run: | diff --git a/README.md b/README.md index 66107ce9..eec79cac 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,16 @@ repos: - id: reuse-lint-file ``` +### Shell completion + +You can generate a shell completion script with `reuse --print-completion bash`. +Replace 'bash' as needed. You must place the printed text in a file dictated by +your shell to benefit from completions. + +This functionality depends on `shtab`, which is an optional dependency. To +benefit from this feature, install reuse with the `completion` extra, like +`pipx install reuse[completion]`. + ## Maintainers - Carmen Bianca Bakker diff --git a/changelog.d/added/shtab.md b/changelog.d/added/shtab.md new file mode 100644 index 00000000..b3543469 --- /dev/null +++ b/changelog.d/added/shtab.md @@ -0,0 +1 @@ +- Added `--print-completion` using a new `shtab` optional dependency. (#1076) diff --git a/docs/man/reuse.rst b/docs/man/reuse.rst index 37bbe644..121b6614 100644 --- a/docs/man/reuse.rst +++ b/docs/man/reuse.rst @@ -80,9 +80,14 @@ Options .. option:: -s, --print-completion SHELL - Print a static shell completion file, for the given shell. This option depends - on python-shtab and as such defines which shells are supported. Presently this - includes bash, tcsh and zsh, with fish support being proposed. + Print a static shell completion script for the given shell and exit. You must + place the printed text in a file dictated by your shell before the completions + will function. For Bash, this file is + ``${XDG_DATA_HOME}/bash-completion/reuse``. + + This option depends on ``shtab``, which is an optional dependency of + :program:`reuse`. The supported shells depend on your installed version of + ``shtab``. .. option:: -h, --help diff --git a/poetry.lock b/poetry.lock index 728d880a..39dc3d47 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1350,6 +1350,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -1406,6 +1407,21 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "shtab" +version = "1.7.1" +description = "Automagic shell tab completion for Python CLI applications" +category = "main" +optional = true +python-versions = ">=3.7" +files = [ + {file = "shtab-1.7.1-py3-none-any.whl", hash = "sha256:32d3d2ff9022d4c77a62492b6ec875527883891e33c6b479ba4d41a51e259983"}, + {file = "shtab-1.7.1.tar.gz", hash = "sha256:4e4bcb02eeb82ec45920a5d0add92eac9c9b63b2804c9196c1f1fdc2d039243c"}, +] + +[package.extras] +dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout"] + [[package]] name = "six" version = "1.16.0" @@ -1810,7 +1826,10 @@ files = [ doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +[extras] +completion = ["shtab"] + [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "3f000ef7f191dd1e5648641ddfef4e469429e33e583299bbcac630bfd59ac10b" +content-hash = "d8a6ce43f7e484927e2f0de8eeea38d3058f7802c96670e8552fb3e0434b655b" diff --git a/pyproject.toml b/pyproject.toml index 1f8e51d8..3f60bac6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,10 @@ license-expression = ">=1.0" python-debian = ">=0.1.34,!=0.1.45,!=0.1.46,!=0.1.47" tomlkit = ">=0.8" attrs = ">=21.3" +shtab = { version = ">=1.4.0", optional = true } + +[tool.poetry.extras] +completion = ["shtab"] [tool.poetry.group.test.dependencies] pytest = ">=6.0.0" diff --git a/src/reuse/_main.py b/src/reuse/_main.py index 0b6a6b0b..aab68774 100644 --- a/src/reuse/_main.py +++ b/src/reuse/_main.py @@ -10,12 +10,14 @@ """Entry functions for reuse.""" import argparse +import contextlib import logging import os import sys import warnings from gettext import gettext as _ from pathlib import Path +from types import ModuleType from typing import IO, Callable, List, Optional, Type, cast from . import ( @@ -35,10 +37,9 @@ from .project import GlobalLicensingConflict, GlobalLicensingFound, Project from .vcs import find_root -try: - import shtab -except ImportError: - shtab = None +shtab: Optional[ModuleType] = None +with contextlib.suppress(ImportError): + import shtab # type: ignore[no-redef] _LOGGER = logging.getLogger(__name__) @@ -110,6 +111,7 @@ def parser() -> argparse.ArgumentParser: help=_("define root of project"), ) if shtab: + # This is magic. Running `reuse -s bash` now prints bash completions. shtab.add_argument_to(parser, ["-s", "--print-completion"]) parser.add_argument( "--version", diff --git a/tests/test_main.py b/tests/test_main.py index 94c078f9..fb64530b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -29,7 +29,7 @@ from freezegun import freeze_time from reuse import download -from reuse._main import main +from reuse._main import main, shtab from reuse._util import GIT_EXE, HG_EXE, JUJUTSU_EXE, PIJUL_EXE, cleandoc_nl from reuse.report import LINT_VERSION @@ -88,6 +88,16 @@ def mock_put_license_in_file(monkeypatch): return result +@pytest.mark.skipif(not shtab, reason="shtab required") +def test_print_completion(capsys): + """shtab completions are printed.""" + with pytest.raises(SystemExit) as error: + main(["--print-completion", "bash"]) + + assert error.value.code == 0 + assert "AUTOMATICALLY GENERATED by `shtab`" in capsys.readouterr().out + + def test_lint(fake_repository, stringio, optional_git_exe, optional_hg_exe): """Run a successful lint. The optional VCSs are there to make sure that the test also works if these programs are not installed.