Skip to content

Commit 8ea6cc1

Browse files
[3.12] gh-121018: Fix more cases of exiting in argparse when exit_on_error=False (GH-121056) (GH-121129)
* parse_intermixed_args() now raises ArgumentError instead of calling error() if exit_on_error is false. * Internal code now always raises ArgumentError instead of calling error(). It is then caught at the higher level and error() is called if exit_on_error is true. (cherry picked from commit 81a654a)
1 parent 21a63b9 commit 8ea6cc1

File tree

3 files changed

+59
-16
lines changed

3 files changed

+59
-16
lines changed

Lib/argparse.py

+13-9
Original file line numberDiff line numberDiff line change
@@ -1843,7 +1843,7 @@ def _get_kwargs(self):
18431843
# ==================================
18441844
def add_subparsers(self, **kwargs):
18451845
if self._subparsers is not None:
1846-
self.error(_('cannot have multiple subparser arguments'))
1846+
raise ArgumentError(None, _('cannot have multiple subparser arguments'))
18471847

18481848
# add the parser class to the arguments if it's not present
18491849
kwargs.setdefault('parser_class', type(self))
@@ -1898,7 +1898,8 @@ def parse_args(self, args=None, namespace=None):
18981898
msg = _('unrecognized arguments: %s') % ' '.join(argv)
18991899
if self.exit_on_error:
19001900
self.error(msg)
1901-
raise ArgumentError(None, msg)
1901+
else:
1902+
raise ArgumentError(None, msg)
19021903
return args
19031904

19041905
def parse_known_args(self, args=None, namespace=None):
@@ -2177,7 +2178,7 @@ def consume_positionals(start_index):
21772178
self._get_value(action, action.default))
21782179

21792180
if required_actions:
2180-
self.error(_('the following arguments are required: %s') %
2181+
raise ArgumentError(None, _('the following arguments are required: %s') %
21812182
', '.join(required_actions))
21822183

21832184
# make sure all required groups had one option present
@@ -2193,7 +2194,7 @@ def consume_positionals(start_index):
21932194
for action in group._group_actions
21942195
if action.help is not SUPPRESS]
21952196
msg = _('one of the arguments %s is required')
2196-
self.error(msg % ' '.join(names))
2197+
raise ArgumentError(None, msg % ' '.join(names))
21972198

21982199
# return the updated namespace and the extra arguments
21992200
return namespace, extras
@@ -2220,7 +2221,7 @@ def _read_args_from_files(self, arg_strings):
22202221
arg_strings = self._read_args_from_files(arg_strings)
22212222
new_arg_strings.extend(arg_strings)
22222223
except OSError as err:
2223-
self.error(str(err))
2224+
raise ArgumentError(None, str(err))
22242225

22252226
# return the modified argument list
22262227
return new_arg_strings
@@ -2300,7 +2301,7 @@ def _parse_optional(self, arg_string):
23002301
for action, option_string, sep, explicit_arg in option_tuples])
23012302
args = {'option': arg_string, 'matches': options}
23022303
msg = _('ambiguous option: %(option)s could match %(matches)s')
2303-
self.error(msg % args)
2304+
raise ArgumentError(None, msg % args)
23042305

23052306
# if exactly one action matched, this segmentation is good,
23062307
# so return the parsed action
@@ -2360,7 +2361,7 @@ def _get_option_tuples(self, option_string):
23602361

23612362
# shouldn't ever get here
23622363
else:
2363-
self.error(_('unexpected option string: %s') % option_string)
2364+
raise ArgumentError(None, _('unexpected option string: %s') % option_string)
23642365

23652366
# return the collected option tuples
23662367
return result
@@ -2417,8 +2418,11 @@ def _get_nargs_pattern(self, action):
24172418
def parse_intermixed_args(self, args=None, namespace=None):
24182419
args, argv = self.parse_known_intermixed_args(args, namespace)
24192420
if argv:
2420-
msg = _('unrecognized arguments: %s')
2421-
self.error(msg % ' '.join(argv))
2421+
msg = _('unrecognized arguments: %s') % ' '.join(argv)
2422+
if self.exit_on_error:
2423+
self.error(msg)
2424+
else:
2425+
raise ArgumentError(None, msg)
24222426
return args
24232427

24242428
def parse_known_intermixed_args(self, args=None, namespace=None):

Lib/test/test_argparse.py

+43-5
Original file line numberDiff line numberDiff line change
@@ -2126,7 +2126,9 @@ def _get_parser(self, subparser_help=False, prefix_chars=None,
21262126
else:
21272127
subparsers_kwargs['help'] = 'command help'
21282128
subparsers = parser.add_subparsers(**subparsers_kwargs)
2129-
self.assertArgumentParserError(parser.add_subparsers)
2129+
self.assertRaisesRegex(argparse.ArgumentError,
2130+
'cannot have multiple subparser arguments',
2131+
parser.add_subparsers)
21302132

21312133
# add first sub-parser
21322134
parser1_kwargs = dict(description='1 description')
@@ -5732,7 +5734,8 @@ def test_help_with_metavar(self):
57325734
class TestExitOnError(TestCase):
57335735

57345736
def setUp(self):
5735-
self.parser = argparse.ArgumentParser(exit_on_error=False)
5737+
self.parser = argparse.ArgumentParser(exit_on_error=False,
5738+
fromfile_prefix_chars='@')
57365739
self.parser.add_argument('--integers', metavar='N', type=int)
57375740

57385741
def test_exit_on_error_with_good_args(self):
@@ -5743,9 +5746,44 @@ def test_exit_on_error_with_bad_args(self):
57435746
with self.assertRaises(argparse.ArgumentError):
57445747
self.parser.parse_args('--integers a'.split())
57455748

5746-
def test_exit_on_error_with_unrecognized_args(self):
5747-
with self.assertRaises(argparse.ArgumentError):
5748-
self.parser.parse_args('--foo bar'.split())
5749+
def test_unrecognized_args(self):
5750+
self.assertRaisesRegex(argparse.ArgumentError,
5751+
'unrecognized arguments: --foo bar',
5752+
self.parser.parse_args, '--foo bar'.split())
5753+
5754+
def test_unrecognized_intermixed_args(self):
5755+
self.assertRaisesRegex(argparse.ArgumentError,
5756+
'unrecognized arguments: --foo bar',
5757+
self.parser.parse_intermixed_args, '--foo bar'.split())
5758+
5759+
def test_required_args(self):
5760+
self.parser.add_argument('bar')
5761+
self.parser.add_argument('baz')
5762+
self.assertRaisesRegex(argparse.ArgumentError,
5763+
'the following arguments are required: bar, baz',
5764+
self.parser.parse_args, [])
5765+
5766+
def test_required_mutually_exclusive_args(self):
5767+
group = self.parser.add_mutually_exclusive_group(required=True)
5768+
group.add_argument('--bar')
5769+
group.add_argument('--baz')
5770+
self.assertRaisesRegex(argparse.ArgumentError,
5771+
'one of the arguments --bar --baz is required',
5772+
self.parser.parse_args, [])
5773+
5774+
def test_ambiguous_option(self):
5775+
self.parser.add_argument('--foobaz')
5776+
self.parser.add_argument('--fooble', action='store_true')
5777+
self.assertRaisesRegex(argparse.ArgumentError,
5778+
"ambiguous option: --foob could match --foobaz, --fooble",
5779+
self.parser.parse_args, ['--foob'])
5780+
5781+
def test_os_error(self):
5782+
self.parser.add_argument('file')
5783+
self.assertRaisesRegex(argparse.ArgumentError,
5784+
"No such file or directory: 'no-such-file'",
5785+
self.parser.parse_args, ['@no-such-file'])
5786+
57495787

57505788
def tearDownModule():
57515789
# Remove global references to avoid looking like we have refleaks.
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1-
Fixed an issue where :meth:`!argparse.ArgumentParser.parses_args` did not honor ``exit_on_error=False`` when given unrecognized arguments.
2-
Patch by Ben Hsing.
1+
Fixed issues where :meth:`!argparse.ArgumentParser.parses_args` did not honor
2+
``exit_on_error=False``.
3+
Based on patch by Ben Hsing.

0 commit comments

Comments
 (0)