Skip to content

Commit 8edfc47

Browse files
authored
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, b1e4d1b (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
1 parent ffda25f commit 8edfc47

File tree

4 files changed

+69
-28
lines changed

4 files changed

+69
-28
lines changed

Lib/argparse.py

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2199,24 +2199,23 @@ def _parse_optional(self, arg_string):
21992199
action = self._option_string_actions[option_string]
22002200
return action, option_string, explicit_arg
22012201

2202-
if self.allow_abbrev or not arg_string.startswith('--'):
2203-
# search through all possible prefixes of the option string
2204-
# and all actions in the parser for possible interpretations
2205-
option_tuples = self._get_option_tuples(arg_string)
2206-
2207-
# if multiple actions match, the option string was ambiguous
2208-
if len(option_tuples) > 1:
2209-
options = ', '.join([option_string
2210-
for action, option_string, explicit_arg in option_tuples])
2211-
args = {'option': arg_string, 'matches': options}
2212-
msg = _('ambiguous option: %(option)s could match %(matches)s')
2213-
self.error(msg % args)
2214-
2215-
# if exactly one action matched, this segmentation is good,
2216-
# so return the parsed action
2217-
elif len(option_tuples) == 1:
2218-
option_tuple, = option_tuples
2219-
return option_tuple
2202+
# search through all possible prefixes of the option string
2203+
# and all actions in the parser for possible interpretations
2204+
option_tuples = self._get_option_tuples(arg_string)
2205+
2206+
# if multiple actions match, the option string was ambiguous
2207+
if len(option_tuples) > 1:
2208+
options = ', '.join([option_string
2209+
for action, option_string, explicit_arg in option_tuples])
2210+
args = {'option': arg_string, 'matches': options}
2211+
msg = _('ambiguous option: %(option)s could match %(matches)s')
2212+
self.error(msg % args)
2213+
2214+
# if exactly one action matched, this segmentation is good,
2215+
# so return the parsed action
2216+
elif len(option_tuples) == 1:
2217+
option_tuple, = option_tuples
2218+
return option_tuple
22202219

22212220
# if it was not found as an option, but it looks like a negative
22222221
# number, it was meant to be positional
@@ -2240,16 +2239,17 @@ def _get_option_tuples(self, option_string):
22402239
# split at the '='
22412240
chars = self.prefix_chars
22422241
if option_string[0] in chars and option_string[1] in chars:
2243-
if '=' in option_string:
2244-
option_prefix, explicit_arg = option_string.split('=', 1)
2245-
else:
2246-
option_prefix = option_string
2247-
explicit_arg = None
2248-
for option_string in self._option_string_actions:
2249-
if option_string.startswith(option_prefix):
2250-
action = self._option_string_actions[option_string]
2251-
tup = action, option_string, explicit_arg
2252-
result.append(tup)
2242+
if self.allow_abbrev:
2243+
if '=' in option_string:
2244+
option_prefix, explicit_arg = option_string.split('=', 1)
2245+
else:
2246+
option_prefix = option_string
2247+
explicit_arg = None
2248+
for option_string in self._option_string_actions:
2249+
if option_string.startswith(option_prefix):
2250+
action = self._option_string_actions[option_string]
2251+
tup = action, option_string, explicit_arg
2252+
result.append(tup)
22532253

22542254
# single character options can be concatenated with their arguments
22552255
# but multiple character options always have to have their argument

Lib/test/test_argparse.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,23 @@ class TestOptionalsDisallowLongAbbreviation(ParserTestCase):
810810
]
811811

812812

813+
class TestOptionalsDisallowLongAbbreviationPrefixChars(ParserTestCase):
814+
"""Disallowing abbreviations works with alternative prefix characters"""
815+
816+
parser_signature = Sig(prefix_chars='+', allow_abbrev=False)
817+
argument_signatures = [
818+
Sig('++foo'),
819+
Sig('++foodle', action='store_true'),
820+
Sig('++foonly'),
821+
]
822+
failures = ['+foon 3', '++foon 3', '++food', '++food ++foo 2']
823+
successes = [
824+
('', NS(foo=None, foodle=False, foonly=None)),
825+
('++foo 3', NS(foo='3', foodle=False, foonly=None)),
826+
('++foonly 7 ++foodle ++foo 2', NS(foo='2', foodle=True, foonly='7')),
827+
]
828+
829+
813830
class TestDisallowLongAbbreviationAllowsShortGrouping(ParserTestCase):
814831
"""Do not allow abbreviations of long options at all"""
815832

@@ -828,6 +845,26 @@ class TestDisallowLongAbbreviationAllowsShortGrouping(ParserTestCase):
828845
('-ccrcc', NS(r='cc', c=2)),
829846
]
830847

848+
849+
class TestDisallowLongAbbreviationAllowsShortGroupingPrefix(ParserTestCase):
850+
"""Short option grouping works with custom prefix and allow_abbrev=False"""
851+
852+
parser_signature = Sig(prefix_chars='+', allow_abbrev=False)
853+
argument_signatures = [
854+
Sig('+r'),
855+
Sig('+c', action='count'),
856+
]
857+
failures = ['+r', '+c +r']
858+
successes = [
859+
('', NS(r=None, c=None)),
860+
('+ra', NS(r='a', c=None)),
861+
('+rcc', NS(r='cc', c=None)),
862+
('+cc', NS(r=None, c=2)),
863+
('+cc +ra', NS(r='a', c=2)),
864+
('+ccrcc', NS(r='cc', c=2)),
865+
]
866+
867+
831868
# ================
832869
# Positional tests
833870
# ================

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,7 @@ Bruce Merry
11121112
Alexis Métaireau
11131113
Luke Mewburn
11141114
Carl Meyer
1115+
Kyle Meyer
11151116
Mike Meyer
11161117
Piotr Meyer
11171118
Steven Miale
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a regression in :class:`~argparse.ArgumentParser` where
2+
``allow_abbrev=False`` was ignored for long options that used a prefix
3+
character other than "-".

0 commit comments

Comments
 (0)