Skip to content

Commit 81a654a

Browse files
gh-121018: Fix more cases of exiting in argparse when exit_on_error=False (GH-121056)
* 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.
1 parent 43709d5 commit 81a654a

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
@@ -1791,7 +1791,7 @@ def _get_kwargs(self):
17911791
# ==================================
17921792
def add_subparsers(self, **kwargs):
17931793
if self._subparsers is not None:
1794-
self.error(_('cannot have multiple subparser arguments'))
1794+
raise ArgumentError(None, _('cannot have multiple subparser arguments'))
17951795

17961796
# add the parser class to the arguments if it's not present
17971797
kwargs.setdefault('parser_class', type(self))
@@ -1846,7 +1846,8 @@ def parse_args(self, args=None, namespace=None):
18461846
msg = _('unrecognized arguments: %s') % ' '.join(argv)
18471847
if self.exit_on_error:
18481848
self.error(msg)
1849-
raise ArgumentError(None, msg)
1849+
else:
1850+
raise ArgumentError(None, msg)
18501851
return args
18511852

18521853
def parse_known_args(self, args=None, namespace=None):
@@ -2135,7 +2136,7 @@ def consume_positionals(start_index):
21352136
self._get_value(action, action.default))
21362137

21372138
if required_actions:
2138-
self.error(_('the following arguments are required: %s') %
2139+
raise ArgumentError(None, _('the following arguments are required: %s') %
21392140
', '.join(required_actions))
21402141

21412142
# make sure all required groups had one option present
@@ -2151,7 +2152,7 @@ def consume_positionals(start_index):
21512152
for action in group._group_actions
21522153
if action.help is not SUPPRESS]
21532154
msg = _('one of the arguments %s is required')
2154-
self.error(msg % ' '.join(names))
2155+
raise ArgumentError(None, msg % ' '.join(names))
21552156

21562157
# return the updated namespace and the extra arguments
21572158
return namespace, extras
@@ -2178,7 +2179,7 @@ def _read_args_from_files(self, arg_strings):
21782179
arg_strings = self._read_args_from_files(arg_strings)
21792180
new_arg_strings.extend(arg_strings)
21802181
except OSError as err:
2181-
self.error(str(err))
2182+
raise ArgumentError(None, str(err))
21822183

21832184
# return the modified argument list
21842185
return new_arg_strings
@@ -2258,7 +2259,7 @@ def _parse_optional(self, arg_string):
22582259
for action, option_string, sep, explicit_arg in option_tuples])
22592260
args = {'option': arg_string, 'matches': options}
22602261
msg = _('ambiguous option: %(option)s could match %(matches)s')
2261-
self.error(msg % args)
2262+
raise ArgumentError(None, msg % args)
22622263

22632264
# if exactly one action matched, this segmentation is good,
22642265
# so return the parsed action
@@ -2318,7 +2319,7 @@ def _get_option_tuples(self, option_string):
23182319

23192320
# shouldn't ever get here
23202321
else:
2321-
self.error(_('unexpected option string: %s') % option_string)
2322+
raise ArgumentError(None, _('unexpected option string: %s') % option_string)
23222323

23232324
# return the collected option tuples
23242325
return result
@@ -2375,8 +2376,11 @@ def _get_nargs_pattern(self, action):
23752376
def parse_intermixed_args(self, args=None, namespace=None):
23762377
args, argv = self.parse_known_intermixed_args(args, namespace)
23772378
if argv:
2378-
msg = _('unrecognized arguments: %s')
2379-
self.error(msg % ' '.join(argv))
2379+
msg = _('unrecognized arguments: %s') % ' '.join(argv)
2380+
if self.exit_on_error:
2381+
self.error(msg)
2382+
else:
2383+
raise ArgumentError(None, msg)
23802384
return args
23812385

23822386
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
@@ -2147,7 +2147,9 @@ def _get_parser(self, subparser_help=False, prefix_chars=None,
21472147
else:
21482148
subparsers_kwargs['help'] = 'command help'
21492149
subparsers = parser.add_subparsers(**subparsers_kwargs)
2150-
self.assertArgumentParserError(parser.add_subparsers)
2150+
self.assertRaisesRegex(argparse.ArgumentError,
2151+
'cannot have multiple subparser arguments',
2152+
parser.add_subparsers)
21512153

21522154
# add first sub-parser
21532155
parser1_kwargs = dict(description='1 description')
@@ -6042,7 +6044,8 @@ def test_help_with_metavar(self):
60426044
class TestExitOnError(TestCase):
60436045

60446046
def setUp(self):
6045-
self.parser = argparse.ArgumentParser(exit_on_error=False)
6047+
self.parser = argparse.ArgumentParser(exit_on_error=False,
6048+
fromfile_prefix_chars='@')
60466049
self.parser.add_argument('--integers', metavar='N', type=int)
60476050

60486051
def test_exit_on_error_with_good_args(self):
@@ -6053,9 +6056,44 @@ def test_exit_on_error_with_bad_args(self):
60536056
with self.assertRaises(argparse.ArgumentError):
60546057
self.parser.parse_args('--integers a'.split())
60556058

6056-
def test_exit_on_error_with_unrecognized_args(self):
6057-
with self.assertRaises(argparse.ArgumentError):
6058-
self.parser.parse_args('--foo bar'.split())
6059+
def test_unrecognized_args(self):
6060+
self.assertRaisesRegex(argparse.ArgumentError,
6061+
'unrecognized arguments: --foo bar',
6062+
self.parser.parse_args, '--foo bar'.split())
6063+
6064+
def test_unrecognized_intermixed_args(self):
6065+
self.assertRaisesRegex(argparse.ArgumentError,
6066+
'unrecognized arguments: --foo bar',
6067+
self.parser.parse_intermixed_args, '--foo bar'.split())
6068+
6069+
def test_required_args(self):
6070+
self.parser.add_argument('bar')
6071+
self.parser.add_argument('baz')
6072+
self.assertRaisesRegex(argparse.ArgumentError,
6073+
'the following arguments are required: bar, baz',
6074+
self.parser.parse_args, [])
6075+
6076+
def test_required_mutually_exclusive_args(self):
6077+
group = self.parser.add_mutually_exclusive_group(required=True)
6078+
group.add_argument('--bar')
6079+
group.add_argument('--baz')
6080+
self.assertRaisesRegex(argparse.ArgumentError,
6081+
'one of the arguments --bar --baz is required',
6082+
self.parser.parse_args, [])
6083+
6084+
def test_ambiguous_option(self):
6085+
self.parser.add_argument('--foobaz')
6086+
self.parser.add_argument('--fooble', action='store_true')
6087+
self.assertRaisesRegex(argparse.ArgumentError,
6088+
"ambiguous option: --foob could match --foobaz, --fooble",
6089+
self.parser.parse_args, ['--foob'])
6090+
6091+
def test_os_error(self):
6092+
self.parser.add_argument('file')
6093+
self.assertRaisesRegex(argparse.ArgumentError,
6094+
"No such file or directory: 'no-such-file'",
6095+
self.parser.parse_args, ['@no-such-file'])
6096+
60596097

60606098
def tearDownModule():
60616099
# 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 :func:`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)