diff --git a/changelog/1149.removal.rst b/changelog/1149.removal.rst new file mode 100644 index 000000000..f507014d9 --- /dev/null +++ b/changelog/1149.removal.rst @@ -0,0 +1,7 @@ +Pytest no longer accepts prefixes of command-line arguments, for example +typing ``pytest --doctest-mod`` inplace of ``--doctest-modules``. +This was previously allowed where the ``ArgumentParser`` thought it was unambiguous, +but this could be incorrect due to delayed parsing of options for plugins. +See for example issues `#1149 `__, +`#3413 `__, and +`#4009 `__. diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index fb36c7985..d62ed0d03 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -1,5 +1,7 @@ import argparse +import sys import warnings +from gettext import gettext import py @@ -328,6 +330,7 @@ class MyOptionParser(argparse.ArgumentParser): usage=parser._usage, add_help=False, formatter_class=DropShorterLongHelpFormatter, + allow_abbrev=False, ) # extra_info is a dict of (param -> value) to display if there's # an usage error to provide more contextual information to the user @@ -355,6 +358,42 @@ class MyOptionParser(argparse.ArgumentParser): getattr(args, FILE_OR_DIR).extend(argv) return args + if sys.version_info[:2] < (3, 8): # pragma: no cover + # Backport of https://github.com/python/cpython/pull/14316 so we can + # disable long --argument abbreviations without breaking short flags. + def _parse_optional(self, arg_string): + if not arg_string: + return None + if not arg_string[0] in self.prefix_chars: + return None + if arg_string in self._option_string_actions: + action = self._option_string_actions[arg_string] + return action, arg_string, None + if len(arg_string) == 1: + return None + if "=" in arg_string: + option_string, explicit_arg = arg_string.split("=", 1) + if option_string in self._option_string_actions: + action = self._option_string_actions[option_string] + return action, option_string, explicit_arg + if self.allow_abbrev or not arg_string.startswith("--"): + option_tuples = self._get_option_tuples(arg_string) + if len(option_tuples) > 1: + msg = gettext( + "ambiguous option: %(option)s could match %(matches)s" + ) + options = ", ".join(option for _, option, _ in option_tuples) + self.error(msg % {"option": arg_string, "matches": options}) + elif len(option_tuples) == 1: + option_tuple, = option_tuples + return option_tuple + if self._negative_number_matcher.match(arg_string): + if not self._has_negative_number_optionals: + return None + if " " in arg_string: + return None + return None, arg_string, None + class DropShorterLongHelpFormatter(argparse.HelpFormatter): """shorten help for long options that differ only in extra hyphens diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 3eb7752a4..5567d994d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -996,7 +996,7 @@ def test_zipimport_hook(testdir, tmpdir): "app/foo.py": """ import pytest def main(): - pytest.main(['--pyarg', 'foo']) + pytest.main(['--pyargs', 'foo']) """ } ) diff --git a/testing/test_capture.py b/testing/test_capture.py index 8d1d33bc7..f5b193597 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -735,7 +735,7 @@ def test_capture_badoutput_issue412(testdir): assert 0 """ ) - result = testdir.runpytest("--cap=fd") + result = testdir.runpytest("--capture=fd") result.stdout.fnmatch_lines( """ *def test_func* diff --git a/testing/test_parseopt.py b/testing/test_parseopt.py index 7c581cce1..dd7bc8753 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -200,7 +200,7 @@ class TestParser: def test_drop_short_helper(self): parser = argparse.ArgumentParser( - formatter_class=parseopt.DropShorterLongHelpFormatter + formatter_class=parseopt.DropShorterLongHelpFormatter, allow_abbrev=False ) parser.add_argument( "-t", "--twoword", "--duo", "--two-word", "--two", help="foo" @@ -239,10 +239,8 @@ class TestParser: parser.addoption("--funcarg", "--func-arg", action="store_true") parser.addoption("--abc-def", "--abc-def", action="store_true") parser.addoption("--klm-hij", action="store_true") - args = parser.parse(["--funcarg", "--k"]) - assert args.funcarg is True - assert args.abc_def is False - assert args.klm_hij is True + with pytest.raises(UsageError): + parser.parse(["--funcarg", "--k"]) def test_drop_short_2(self, parser): parser.addoption("--func-arg", "--doit", action="store_true") diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 48dea14bd..fd443ed40 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -21,7 +21,7 @@ class TestPasteCapture: pytest.skip("") """ ) - reprec = testdir.inline_run(testpath, "--paste=failed") + reprec = testdir.inline_run(testpath, "--pastebin=failed") assert len(pastebinlist) == 1 s = pastebinlist[0] assert s.find("def test_fail") != -1