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
This commit is contained in:
parent
ffda25f6b8
commit
8edfc47bae
|
@ -2199,24 +2199,23 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
action = self._option_string_actions[option_string]
|
action = self._option_string_actions[option_string]
|
||||||
return action, option_string, explicit_arg
|
return action, option_string, explicit_arg
|
||||||
|
|
||||||
if self.allow_abbrev or not arg_string.startswith('--'):
|
# search through all possible prefixes of the option string
|
||||||
# search through all possible prefixes of the option string
|
# and all actions in the parser for possible interpretations
|
||||||
# and all actions in the parser for possible interpretations
|
option_tuples = self._get_option_tuples(arg_string)
|
||||||
option_tuples = self._get_option_tuples(arg_string)
|
|
||||||
|
|
||||||
# if multiple actions match, the option string was ambiguous
|
# if multiple actions match, the option string was ambiguous
|
||||||
if len(option_tuples) > 1:
|
if len(option_tuples) > 1:
|
||||||
options = ', '.join([option_string
|
options = ', '.join([option_string
|
||||||
for action, option_string, explicit_arg in option_tuples])
|
for action, option_string, explicit_arg in option_tuples])
|
||||||
args = {'option': arg_string, 'matches': options}
|
args = {'option': arg_string, 'matches': options}
|
||||||
msg = _('ambiguous option: %(option)s could match %(matches)s')
|
msg = _('ambiguous option: %(option)s could match %(matches)s')
|
||||||
self.error(msg % args)
|
self.error(msg % args)
|
||||||
|
|
||||||
# if exactly one action matched, this segmentation is good,
|
# if exactly one action matched, this segmentation is good,
|
||||||
# so return the parsed action
|
# so return the parsed action
|
||||||
elif len(option_tuples) == 1:
|
elif len(option_tuples) == 1:
|
||||||
option_tuple, = option_tuples
|
option_tuple, = option_tuples
|
||||||
return option_tuple
|
return option_tuple
|
||||||
|
|
||||||
# if it was not found as an option, but it looks like a negative
|
# if it was not found as an option, but it looks like a negative
|
||||||
# number, it was meant to be positional
|
# number, it was meant to be positional
|
||||||
|
@ -2240,16 +2239,17 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
# split at the '='
|
# split at the '='
|
||||||
chars = self.prefix_chars
|
chars = self.prefix_chars
|
||||||
if option_string[0] in chars and option_string[1] in chars:
|
if option_string[0] in chars and option_string[1] in chars:
|
||||||
if '=' in option_string:
|
if self.allow_abbrev:
|
||||||
option_prefix, explicit_arg = option_string.split('=', 1)
|
if '=' in option_string:
|
||||||
else:
|
option_prefix, explicit_arg = option_string.split('=', 1)
|
||||||
option_prefix = option_string
|
else:
|
||||||
explicit_arg = None
|
option_prefix = option_string
|
||||||
for option_string in self._option_string_actions:
|
explicit_arg = None
|
||||||
if option_string.startswith(option_prefix):
|
for option_string in self._option_string_actions:
|
||||||
action = self._option_string_actions[option_string]
|
if option_string.startswith(option_prefix):
|
||||||
tup = action, option_string, explicit_arg
|
action = self._option_string_actions[option_string]
|
||||||
result.append(tup)
|
tup = action, option_string, explicit_arg
|
||||||
|
result.append(tup)
|
||||||
|
|
||||||
# single character options can be concatenated with their arguments
|
# single character options can be concatenated with their arguments
|
||||||
# but multiple character options always have to have their argument
|
# but multiple character options always have to have their argument
|
||||||
|
|
|
@ -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):
|
class TestDisallowLongAbbreviationAllowsShortGrouping(ParserTestCase):
|
||||||
"""Do not allow abbreviations of long options at all"""
|
"""Do not allow abbreviations of long options at all"""
|
||||||
|
|
||||||
|
@ -828,6 +845,26 @@ class TestDisallowLongAbbreviationAllowsShortGrouping(ParserTestCase):
|
||||||
('-ccrcc', NS(r='cc', c=2)),
|
('-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
|
# Positional tests
|
||||||
# ================
|
# ================
|
||||||
|
|
|
@ -1112,6 +1112,7 @@ Bruce Merry
|
||||||
Alexis Métaireau
|
Alexis Métaireau
|
||||||
Luke Mewburn
|
Luke Mewburn
|
||||||
Carl Meyer
|
Carl Meyer
|
||||||
|
Kyle Meyer
|
||||||
Mike Meyer
|
Mike Meyer
|
||||||
Piotr Meyer
|
Piotr Meyer
|
||||||
Steven Miale
|
Steven Miale
|
||||||
|
|
|
@ -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 "-".
|
Loading…
Reference in New Issue