summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorKyle Meyer <kyle@kyleam.com>2020-02-18 09:48:57 (GMT)
committerGitHub <noreply@github.com>2020-02-18 09:48:57 (GMT)
commit8edfc47baec7ff4cb1b9db83dd35c8ffc1d498a4 (patch)
treef4127d288dadd66c7546da9f1c1b4f5e0b7550a3 /Lib
parentffda25f6b825f3dee493b6f0746266a4dd6989f0 (diff)
downloadcpython-8edfc47baec7ff4cb1b9db83dd35c8ffc1d498a4.zip
cpython-8edfc47baec7ff4cb1b9db83dd35c8ffc1d498a4.tar.gz
cpython-8edfc47baec7ff4cb1b9db83dd35c8ffc1d498a4.tar.bz2
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
Diffstat (limited to 'Lib')
-rw-r--r--Lib/argparse.py56
-rw-r--r--Lib/test/test_argparse.py37
2 files changed, 65 insertions, 28 deletions
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
# ================