From d72fb73fa091534a862dbe462257efeef7ecaa2f Mon Sep 17 00:00:00 2001 From: Zac Hatfield-Dodds Date: Tue, 25 Jun 2019 13:51:33 +1000 Subject: [PATCH] Do not allow abbreviated arguments --- changelog/1149.removal.rst | 7 ++++ extra/get_issues.py | 2 +- scripts/release.py | 2 +- src/_pytest/config/argparsing.py | 39 +++++++++++++++++++ testing/acceptance_test.py | 2 +- .../collect_stats/generate_folders.py | 2 +- testing/test_capture.py | 2 +- testing/test_parseopt.py | 8 ++-- testing/test_pastebin.py | 2 +- 9 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 changelog/1149.removal.rst diff --git a/changelog/1149.removal.rst b/changelog/1149.removal.rst new file mode 100644 index 00000000000..f507014d92b --- /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 9407aeded7d..ae99c9aa60e 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 5009df359e6..d2a51e25a4f 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 fb36c798526..d62ed0d03c2 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 @@ def __init__(self, parser, extra_info=None, prog=None): 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 @@ def parse_args(self, args=None, namespace=None): 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 9d903f80233..bac7d4f3a59 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 ff1eaf7d6bb..d2c1a30b2bb 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 @@ 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 0825745ad39..3f75089f4db 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -735,7 +735,7 @@ def test_func(): 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 7c581cce164..dd7bc875387 100644 --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -200,7 +200,7 @@ def defaultget(option): 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 @@ def test_drop_short_0(self, parser): 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 48dea14bdc8..fd443ed40da 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -21,7 +21,7 @@ def test_skip(): 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