From 8edfc47baec7ff4cb1b9db83dd35c8ffc1d498a4 Mon Sep 17 00:00:00 2001 From: Kyle Meyer Date: Tue, 18 Feb 2020 04:48:57 -0500 Subject: bpo-39546: argparse: Honor allow_abbrev=False for specified prefix_chars (GH-18337) When `allow_abbrev` was first added, disabling the abbreviation of long options broke the grouping of short flags ([bpo-26967](https://bugs.python.org/issue26967)). As a fix, b1e4d1b603 (contained in v3.8) ignores `allow_abbrev=False` for a given argument string if the string does _not_ start with "--" (i.e. it doesn't look like a long option). This fix, however, doesn't take into account that long options can start with alternative characters specified via `prefix_chars`, introducing a regression: `allow_abbrev=False` has no effect on long options that start with an alternative prefix character. The most minimal fix would be to replace the "starts with --" check with a "starts with two prefix_chars characters". But `_get_option_tuples` already distinguishes between long and short options, so let's instead piggyback off of that check by moving the `allow_abbrev` condition into `_get_option_tuples`. https://bugs.python.org/issue39546 --- Lib/argparse.py | 56 +++++++++++----------- Lib/test/test_argparse.py | 37 ++++++++++++++ Misc/ACKS | 1 + .../2020-02-03-15-12-51.bpo-39546._Kj0Pn.rst | 3 ++ 4 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-02-03-15-12-51.bpo-39546._Kj0Pn.rst diff --git a/Lib/argparse.py b/Lib/argparse.py index 5d3ce2a..9c710ce 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2199,24 +2199,23 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): action = self._option_string_actions[option_string] return action, option_string, explicit_arg - if self.allow_abbrev or not arg_string.startswith('--'): - # search through all possible prefixes of the option string - # and all actions in the parser for possible interpretations - option_tuples = self._get_option_tuples(arg_string) - - # if multiple actions match, the option string was ambiguous - if len(option_tuples) > 1: - options = ', '.join([option_string - for action, option_string, explicit_arg in option_tuples]) - args = {'option': arg_string, 'matches': options} - msg = _('ambiguous option: %(option)s could match %(matches)s') - self.error(msg % args) - - # if exactly one action matched, this segmentation is good, - # so return the parsed action - elif len(option_tuples) == 1: - option_tuple, = option_tuples - return option_tuple + # search through all possible prefixes of the option string + # and all actions in the parser for possible interpretations + option_tuples = self._get_option_tuples(arg_string) + + # if multiple actions match, the option string was ambiguous + if len(option_tuples) > 1: + options = ', '.join([option_string + for action, option_string, explicit_arg in option_tuples]) + args = {'option': arg_string, 'matches': options} + msg = _('ambiguous option: %(option)s could match %(matches)s') + self.error(msg % args) + + # if exactly one action matched, this segmentation is good, + # so return the parsed action + elif len(option_tuples) == 1: + option_tuple, = option_tuples + return option_tuple # if it was not found as an option, but it looks like a negative # number, it was meant to be positional @@ -2240,16 +2239,17 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer): # split at the '=' chars = self.prefix_chars if option_string[0] in chars and option_string[1] in chars: - if '=' in option_string: - option_prefix, explicit_arg = option_string.split('=', 1) - else: - option_prefix = option_string - explicit_arg = None - for option_string in self._option_string_actions: - if option_string.startswith(option_prefix): - action = self._option_string_actions[option_string] - tup = action, option_string, explicit_arg - result.append(tup) + if self.allow_abbrev: + if '=' in option_string: + option_prefix, explicit_arg = option_string.split('=', 1) + else: + option_prefix = option_string + explicit_arg = None + for option_string in self._option_string_actions: + if option_string.startswith(option_prefix): + action = self._option_string_actions[option_string] + tup = action, option_string, explicit_arg + result.append(tup) # single character options can be concatenated with their arguments # but multiple character options always have to have their argument diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 60bf199..b095783 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -810,6 +810,23 @@ class TestOptionalsDisallowLongAbbreviation(ParserTestCase): ] +class TestOptionalsDisallowLongAbbreviationPrefixChars(ParserTestCase): + """Disallowing abbreviations works with alternative prefix characters""" + + parser_signature = Sig(prefix_chars='+', allow_abbrev=False) + argument_signatures = [ + Sig('++foo'), + Sig('++foodle', action='store_true'), + Sig('++foonly'), + ] + failures = ['+foon 3', '++foon 3', '++food', '++food ++foo 2'] + successes = [ + ('', NS(foo=None, foodle=False, foonly=None)), + ('++foo 3', NS(foo='3', foodle=False, foonly=None)), + ('++foonly 7 ++foodle ++foo 2', NS(foo='2', foodle=True, foonly='7')), + ] + + class TestDisallowLongAbbreviationAllowsShortGrouping(ParserTestCase): """Do not allow abbreviations of long options at all""" @@ -828,6 +845,26 @@ class TestDisallowLongAbbreviationAllowsShortGrouping(ParserTestCase): ('-ccrcc', NS(r='cc', c=2)), ] + +class TestDisallowLongAbbreviationAllowsShortGroupingPrefix(ParserTestCase): + """Short option grouping works with custom prefix and allow_abbrev=False""" + + parser_signature = Sig(prefix_chars='+', allow_abbrev=False) + argument_signatures = [ + Sig('+r'), + Sig('+c', action='count'), + ] + failures = ['+r', '+c +r'] + successes = [ + ('', NS(r=None, c=None)), + ('+ra', NS(r='a', c=None)), + ('+rcc', NS(r='cc', c=None)), + ('+cc', NS(r=None, c=2)), + ('+cc +ra', NS(r='a', c=2)), + ('+ccrcc', NS(r='cc', c=2)), + ] + + # ================ # Positional tests # ================ diff --git a/Misc/ACKS b/Misc/ACKS index 9334020..e8ce303 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1112,6 +1112,7 @@ Bruce Merry Alexis Métaireau Luke Mewburn Carl Meyer +Kyle Meyer Mike Meyer Piotr Meyer Steven Miale diff --git a/Misc/NEWS.d/next/Library/2020-02-03-15-12-51.bpo-39546._Kj0Pn.rst b/Misc/NEWS.d/next/Library/2020-02-03-15-12-51.bpo-39546._Kj0Pn.rst new file mode 100644 index 0000000..8f035e7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-02-03-15-12-51.bpo-39546._Kj0Pn.rst @@ -0,0 +1,3 @@ +Fix a regression in :class:`~argparse.ArgumentParser` where +``allow_abbrev=False`` was ignored for long options that used a prefix +character other than "-". -- cgit v0.12