Skip to content

Commit 99de20d

Browse files
[3.13] gh-121018: Fix more cases of exiting in argparse when exit_on_error=False (GH-121056) (GH-121128)
* 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 394dc93 commit 99de20d

File tree

3 files changed

+58
-14
lines changed

3 files changed

+58
-14
lines changed

Lib/argparse.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1819,7 +1819,7 @@ def _get_kwargs(self):
18191819
# ==================================
18201820
def add_subparsers(self, **kwargs):
18211821
if self._subparsers is not None:
1822-
self.error(_('cannot have multiple subparser arguments'))
1822+
raise ArgumentError(None, _('cannot have multiple subparser arguments'))
18231823

18241824
# add the parser class to the arguments if it's not present
18251825
kwargs.setdefault('parser_class', type(self))
@@ -1874,7 +1874,8 @@ def parse_args(self, args=None, namespace=None):
18741874
msg = _('unrecognized arguments: %s') % ' '.join(argv)
18751875
if self.exit_on_error:
18761876
self.error(msg)
1877-
raise ArgumentError(None, msg)
1877+
else:
1878+
raise ArgumentError(None, msg)
18781879
return args
18791880

18801881
def parse_known_args(self, args=None, namespace=None):
@@ -2163,7 +2164,7 @@ def consume_positionals(start_index):
21632164
self._get_value(action, action.default))
21642165

21652166
if required_actions:
2166-
self.error(_('the following arguments are required: %s') %
2167+
raise ArgumentError(None, _('the following arguments are required: %s') %
21672168
', '.join(required_actions))
21682169

21692170
# make sure all required groups had one option present
@@ -2179,7 +2180,7 @@ def consume_positionals(start_index):
21792180
for action in group._group_actions
21802181
if action.help is not SUPPRESS]
21812182
msg = _('one of the arguments %s is required')
2182-
self.error(msg % ' '.join(names))
2183+
raise ArgumentError(None, msg % ' '.join(names))
21832184

21842185
# return the updated namespace and the extra arguments
21852186
return namespace, extras
@@ -2206,7 +2207,7 @@ def _read_args_from_files(self, arg_strings):
22062207
arg_strings = self._read_args_from_files(arg_strings)
22072208
new_arg_strings.extend(arg_strings)
22082209
except OSError as err:
2209-
self.error(str(err))
2210+
raise ArgumentError(None, str(err))
22102211

22112212
# return the modified argument list
22122213
return new_arg_strings
@@ -2286,7 +2287,7 @@ def _parse_optional(self, arg_string):
22862287
for action, option_string, sep, explicit_arg in option_tuples])
22872288
args = {'option': arg_string, 'matches': options}
22882289
msg = _('ambiguous option: %(option)s could match %(matches)s')
2289-
self.error(msg % args)
2290+
raise ArgumentError(None, msg % args)
22902291

22912292
# if exactly one action matched, this segmentation is good,
22922293
# so return the parsed action
@@ -2346,7 +2347,7 @@ def _get_option_tuples(self, option_string):
23462347

23472348
# shouldn't ever get here
23482349
else:
2349-
self.error(_('unexpected option string: %s') % option_string)
2350+
raise ArgumentError(None, _('unexpected option string: %s') % option_string)
23502351

23512352
# return the collected option tuples
23522353
return result
@@ -2403,8 +2404,11 @@ def _get_nargs_pattern(self, action):
24032404
def parse_intermixed_args(self, args=None, namespace=None):
24042405
args, argv = self.parse_known_intermixed_args(args, namespace)
24052406
if argv:
2406-
msg = _('unrecognized arguments: %s')
2407-
self.error(msg % ' '.join(argv))
2407+
msg = _('unrecognized arguments: %s') % ' '.join(argv)
2408+
if self.exit_on_error:
2409+
self.error(msg)
2410+
else:
2411+
raise ArgumentError(None, msg)
24082412
return args
24092413

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

Lib/test/test_argparse.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2190,7 +2190,9 @@ def _get_parser(self, subparser_help=False, prefix_chars=None,
21902190
else:
21912191
subparsers_kwargs['help'] = 'command help'
21922192
subparsers = parser.add_subparsers(**subparsers_kwargs)
2193-
self.assertArgumentParserError(parser.add_subparsers)
2193+
self.assertRaisesRegex(argparse.ArgumentError,
2194+
'cannot have multiple subparser arguments',
2195+
parser.add_subparsers)
21942196

21952197
# add first sub-parser
21962198
parser1_kwargs = dict(description='1 description')
@@ -6085,7 +6087,8 @@ def test_help_with_metavar(self):
60856087
class TestExitOnError(TestCase):
60866088

60876089
def setUp(self):
6088-
self.parser = argparse.ArgumentParser(exit_on_error=False)
6090+
self.parser = argparse.ArgumentParser(exit_on_error=False,
6091+
fromfile_prefix_chars='@')
60896092
self.parser.add_argument('--integers', metavar='N', type=int)
60906093

60916094
def test_exit_on_error_with_good_args(self):
@@ -6096,9 +6099,44 @@ def test_exit_on_error_with_bad_args(self):
60966099
with self.assertRaises(argparse.ArgumentError):
60976100
self.parser.parse_args('--integers a'.split())
60986101

6099-
def test_exit_on_error_with_unrecognized_args(self):
6100-
with self.assertRaises(argparse.ArgumentError):
6101-
self.parser.parse_args('--foo bar'.split())
6102+
def test_unrecognized_args(self):
6103+
self.assertRaisesRegex(argparse.ArgumentError,
6104+
'unrecognized arguments: --foo bar',
6105+
self.parser.parse_args, '--foo bar'.split())
6106+
6107+
def test_unrecognized_intermixed_args(self):
6108+
self.assertRaisesRegex(argparse.ArgumentError,
6109+
'unrecognized arguments: --foo bar',
6110+
self.parser.parse_intermixed_args, '--foo bar'.split())
6111+
6112+
def test_required_args(self):
6113+
self.parser.add_argument('bar')
6114+
self.parser.add_argument('baz')
6115+
self.assertRaisesRegex(argparse.ArgumentError,
6116+
'the following arguments are required: bar, baz',
6117+
self.parser.parse_args, [])
6118+
6119+
def test_required_mutually_exclusive_args(self):
6120+
group = self.parser.add_mutually_exclusive_group(required=True)
6121+
group.add_argument('--bar')
6122+
group.add_argument('--baz')
6123+
self.assertRaisesRegex(argparse.ArgumentError,
6124+
'one of the arguments --bar --baz is required',
6125+
self.parser.parse_args, [])
6126+
6127+
def test_ambiguous_option(self):
6128+
self.parser.add_argument('--foobaz')
6129+
self.parser.add_argument('--fooble', action='store_true')
6130+
self.assertRaisesRegex(argparse.ArgumentError,
6131+
"ambiguous option: --foob could match --foobaz, --fooble",
6132+
self.parser.parse_args, ['--foob'])
6133+
6134+
def test_os_error(self):
6135+
self.parser.add_argument('file')
6136+
self.assertRaisesRegex(argparse.ArgumentError,
6137+
"No such file or directory: 'no-such-file'",
6138+
self.parser.parse_args, ['@no-such-file'])
6139+
61026140

61036141
def tearDownModule():
61046142
# Remove global references to avoid looking like we have refleaks.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fixed other issues where :class:`argparse.ArgumentParser` did not honor
2+
``exit_on_error=False``.

0 commit comments

Comments
 (0)