diff --git a/AUTHORS b/AUTHORS index ca2872f32..a02326523 100644 --- a/AUTHORS +++ b/AUTHORS @@ -365,6 +365,7 @@ Wil Cooley William Lee Wim Glenn Wouter van Ackooy +Wu Zhenyu Xixi Zhao Xuan Luong Xuecong Liao diff --git a/changelog/10304.feature.rst b/changelog/10304.feature.rst new file mode 100644 index 000000000..d3896cf98 --- /dev/null +++ b/changelog/10304.feature.rst @@ -0,0 +1 @@ +Added shell completions by shtab diff --git a/setup.cfg b/setup.cfg index 38f50556c..d35028bb3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -64,6 +64,8 @@ console_scripts = py.test=pytest:console_main [options.extras_require] +completion = + shtab testing = argcomplete hypothesis>=3.56 @@ -71,6 +73,7 @@ testing = nose pygments>=2.7.2 requests + shtab xmlschema [options.package_data] diff --git a/src/_pytest/__init__.py b/src/_pytest/__init__.py index 8a406c5c7..5301acd53 100644 --- a/src/_pytest/__init__.py +++ b/src/_pytest/__init__.py @@ -1,4 +1,4 @@ -__all__ = ["__version__", "version_tuple"] +__all__ = ["__version__", "version_tuple", "shtab", "XML_FILE", "PREAMBLE"] try: from ._version import version as __version__, version_tuple @@ -7,3 +7,24 @@ except ImportError: # pragma: no cover # unknown only works because we do poor mans version compare __version__ = "unknown" version_tuple = (0, 0, "unknown") # type:ignore[assignment] + +try: + import shtab +except ImportError: + from . import _shtab as shtab + +# https://github.com/iterative/shtab/blob/5358dda86e8ea98bf801a43a24ad73cd9f820c63/examples/customcomplete.py#L11-L22 +XML_FILE = { + "bash": "_shtab_greeter_compgen_xml_files", + "zsh": "_files -g '*.xml'", + "tcsh": "f:*.xml", +} +PREAMBLE = { + "bash": """ +# $1=COMP_WORDS[1] +_shtab_greeter_compgen_xml_files() { + compgen -d -- $1 # recurse into subdirs + compgen -f -X '!*?.xml' -- $1 +} +""" +} diff --git a/src/_pytest/_shtab.py b/src/_pytest/_shtab.py new file mode 100644 index 000000000..609e45e24 --- /dev/null +++ b/src/_pytest/_shtab.py @@ -0,0 +1,14 @@ +"""A shim of shtab.""" +from argparse import Action +from argparse import ArgumentParser +from typing import Any +from typing import Dict +from typing import List + +FILE = None +DIRECTORY = DIR = None + + +def add_argument_to(parser: ArgumentParser, *args: List[Any], **kwargs: Dict[str, Any]): + Action.complete = None # type: ignore + return parser diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index d3f01916b..31726bdde 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -17,6 +17,8 @@ from typing import TYPE_CHECKING from typing import Union import _pytest._io +from _pytest import PREAMBLE +from _pytest import shtab from _pytest.compat import final from _pytest.config.exceptions import UsageError from _pytest.deprecated import ARGUMENT_PERCENT_DEFAULT @@ -27,6 +29,7 @@ from _pytest.deprecated import check_ispytest if TYPE_CHECKING: from typing_extensions import Literal + FILE_OR_DIR = "file_or_dir" @@ -124,11 +127,19 @@ class Parser: if group.options: desc = group.description or group.name arggroup = optparser.add_argument_group(desc) + if group.name == "debugconfig": + shtab.add_argument_to(arggroup, preamble=PREAMBLE) for option in group.options: n = option.names() a = option.attrs() - arggroup.add_argument(*n, **a) + complete = a.get("complete") + if complete: + del a["complete"] # type: ignore + action = arggroup.add_argument(*n, **a) + if complete: + action.complete = complete # type: ignore file_or_dir_arg = optparser.add_argument(FILE_OR_DIR, nargs="*") + file_or_dir_arg.complete = shtab.FILE # type: ignore # bash like autocompletion for dirs (appending '/') # Type ignored because typeshed doesn't know about argcomplete. file_or_dir_arg.completer = filescompleter # type: ignore diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 151bc6dff..9e137d224 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -7,6 +7,7 @@ from typing import Optional from typing import Union import pytest +from _pytest import shtab from _pytest.config import Config from _pytest.config import ExitCode from _pytest.config import PrintHelp @@ -86,6 +87,7 @@ def pytest_addoption(parser: Parser) -> None: help="Store internal tracing debug information in this log file. " "This file is opened with 'w' and truncated as a result, care advised. " "Default: pytestdebug.log.", + complete=shtab.FILE, ) group._addoption( "-o", diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 7a5170f32..5d61fc974 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -21,6 +21,7 @@ from typing import Tuple from typing import Union import pytest +from . import XML_FILE from _pytest import nodes from _pytest import timing from _pytest._code.code import ExceptionRepr @@ -33,7 +34,6 @@ from _pytest.reports import TestReport from _pytest.stash import StashKey from _pytest.terminal import TerminalReporter - xml_key = StashKey["LogXML"]() @@ -390,6 +390,7 @@ def pytest_addoption(parser: Parser) -> None: type=functools.partial(filename_arg, optname="--junitxml"), default=None, help="Create junit-xml style report file at given path", + complete=XML_FILE, ) group.addoption( "--junitprefix", diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index f9091399f..3939e197d 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -19,6 +19,7 @@ from typing import TypeVar from typing import Union from _pytest import nodes +from _pytest import shtab from _pytest._io import TerminalWriter from _pytest.capture import CaptureManager from _pytest.compat import final @@ -35,6 +36,7 @@ from _pytest.main import Session from _pytest.stash import StashKey from _pytest.terminal import TerminalReporter + if TYPE_CHECKING: logging_StreamHandler = logging.StreamHandler[StringIO] @@ -272,6 +274,7 @@ def pytest_addoption(parser: Parser) -> None: dest="log_file", default=None, help="Path to a file when logging will be written to", + complete=shtab.FILE, ) add_option_ini( "--log-file-level", diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 61fb7eaa4..d67293140 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -23,6 +23,7 @@ import attr import _pytest._code from _pytest import nodes +from _pytest import shtab from _pytest.compat import final from _pytest.compat import overload from _pytest.config import Config @@ -128,6 +129,7 @@ def pytest_addoption(parser: Parser) -> None: dest="inifilename", help="Load configuration from `file` instead of trying to locate one of the " "implicit configuration files", + complete=shtab.FILE, ) group._addoption( "--continue-on-collection-errors", @@ -143,6 +145,7 @@ def pytest_addoption(parser: Parser) -> None: help="Define root directory for tests. Can be relative path: 'root_dir', './root_dir', " "'root_dir/another_dir/'; absolute path: '/home/user/root_dir'; path with variables: " "'$HOME/root_dir'.", + complete=shtab.DIR, ) group = parser.getgroup("collect", "collection") @@ -183,6 +186,7 @@ def pytest_addoption(parser: Parser) -> None: metavar="dir", type=functools.partial(directory_arg, optname="--confcutdir"), help="Only load conftest.py's relative to specified dir", + complete=shtab.DIR, ) group.addoption( "--noconftest", @@ -226,6 +230,7 @@ def pytest_addoption(parser: Parser) -> None: "Base temporary directory for this test run. " "(Warning: this directory is removed if it exists.)" ), + complete=shtab.DIR, )