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/extra/get_issues.py b/extra/get_issues.py index 9407aeded..ae99c9aa6 100644 --- a/extra/get_issues.py +++ b/extra/get_issues.py @@ -74,7 +74,7 @@ def report(issues): if __name__ == "__main__": import argparse - parser = argparse.ArgumentParser("process bitbucket issues") + parser = argparse.ArgumentParser("process bitbucket issues", allow_abbrev=False) parser.add_argument( "--refresh", action="store_true", help="invalidate cache, refresh issues" ) diff --git a/scripts/release.py b/scripts/release.py index 5009df359..d2a51e25a 100644 --- a/scripts/release.py +++ b/scripts/release.py @@ -105,7 +105,7 @@ def changelog(version, write_out=False): def main(): init(autoreset=True) - parser = argparse.ArgumentParser() + parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument("version", help="Release version") options = parser.parse_args() pre_release(options.version) 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 9d903f802..bac7d4f3a 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -984,7 +984,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/example_scripts/perf_examples/collect_stats/generate_folders.py b/testing/example_scripts/perf_examples/collect_stats/generate_folders.py index ff1eaf7d6..d2c1a30b2 100644 --- a/testing/example_scripts/perf_examples/collect_stats/generate_folders.py +++ b/testing/example_scripts/perf_examples/collect_stats/generate_folders.py @@ -4,7 +4,7 @@ import pathlib HERE = pathlib.Path(__file__).parent TEST_CONTENT = (HERE / "template_test.py").read_bytes() -parser = argparse.ArgumentParser() +parser = argparse.ArgumentParser(allow_abbrev=False) parser.add_argument("numbers", nargs="*", type=int) diff --git a/testing/test_capture.py b/testing/test_capture.py index 0825745ad..3f75089f4 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