Skip to content

Commit

Permalink
pythongh-121018: Fix more cases of exiting in argparse when exit_on_e…
Browse files Browse the repository at this point in the history
…rror=False (pythonGH-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.
  • Loading branch information
serhiy-storchaka authored Jun 28, 2024
1 parent 43709d5 commit 81a654a
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 16 deletions.
22 changes: 13 additions & 9 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1791,7 +1791,7 @@ def _get_kwargs(self):
# ==================================
def add_subparsers(self, **kwargs):
if self._subparsers is not None:
self.error(_('cannot have multiple subparser arguments'))
raise ArgumentError(None, _('cannot have multiple subparser arguments'))

# add the parser class to the arguments if it's not present
kwargs.setdefault('parser_class', type(self))
Expand Down Expand Up @@ -1846,7 +1846,8 @@ def parse_args(self, args=None, namespace=None):
msg = _('unrecognized arguments: %s') % ' '.join(argv)
if self.exit_on_error:
self.error(msg)
raise ArgumentError(None, msg)
else:
raise ArgumentError(None, msg)
return args

def parse_known_args(self, args=None, namespace=None):
Expand Down Expand Up @@ -2135,7 +2136,7 @@ def consume_positionals(start_index):
self._get_value(action, action.default))

if required_actions:
self.error(_('the following arguments are required: %s') %
raise ArgumentError(None, _('the following arguments are required: %s') %
', '.join(required_actions))

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

# return the updated namespace and the extra arguments
return namespace, extras
Expand All @@ -2178,7 +2179,7 @@ def _read_args_from_files(self, arg_strings):
arg_strings = self._read_args_from_files(arg_strings)
new_arg_strings.extend(arg_strings)
except OSError as err:
self.error(str(err))
raise ArgumentError(None, str(err))

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

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

# shouldn't ever get here
else:
self.error(_('unexpected option string: %s') % option_string)
raise ArgumentError(None, _('unexpected option string: %s') % option_string)

# return the collected option tuples
return result
Expand Down Expand Up @@ -2375,8 +2376,11 @@ def _get_nargs_pattern(self, action):
def parse_intermixed_args(self, args=None, namespace=None):
args, argv = self.parse_known_intermixed_args(args, namespace)
if argv:
msg = _('unrecognized arguments: %s')
self.error(msg % ' '.join(argv))
msg = _('unrecognized arguments: %s') % ' '.join(argv)
if self.exit_on_error:
self.error(msg)
else:
raise ArgumentError(None, msg)
return args

def parse_known_intermixed_args(self, args=None, namespace=None):
Expand Down
48 changes: 43 additions & 5 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2147,7 +2147,9 @@ def _get_parser(self, subparser_help=False, prefix_chars=None,
else:
subparsers_kwargs['help'] = 'command help'
subparsers = parser.add_subparsers(**subparsers_kwargs)
self.assertArgumentParserError(parser.add_subparsers)
self.assertRaisesRegex(argparse.ArgumentError,
'cannot have multiple subparser arguments',
parser.add_subparsers)

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

def setUp(self):
self.parser = argparse.ArgumentParser(exit_on_error=False)
self.parser = argparse.ArgumentParser(exit_on_error=False,
fromfile_prefix_chars='@')
self.parser.add_argument('--integers', metavar='N', type=int)

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

def test_exit_on_error_with_unrecognized_args(self):
with self.assertRaises(argparse.ArgumentError):
self.parser.parse_args('--foo bar'.split())
def test_unrecognized_args(self):
self.assertRaisesRegex(argparse.ArgumentError,
'unrecognized arguments: --foo bar',
self.parser.parse_args, '--foo bar'.split())

def test_unrecognized_intermixed_args(self):
self.assertRaisesRegex(argparse.ArgumentError,
'unrecognized arguments: --foo bar',
self.parser.parse_intermixed_args, '--foo bar'.split())

def test_required_args(self):
self.parser.add_argument('bar')
self.parser.add_argument('baz')
self.assertRaisesRegex(argparse.ArgumentError,
'the following arguments are required: bar, baz',
self.parser.parse_args, [])

def test_required_mutually_exclusive_args(self):
group = self.parser.add_mutually_exclusive_group(required=True)
group.add_argument('--bar')
group.add_argument('--baz')
self.assertRaisesRegex(argparse.ArgumentError,
'one of the arguments --bar --baz is required',
self.parser.parse_args, [])

def test_ambiguous_option(self):
self.parser.add_argument('--foobaz')
self.parser.add_argument('--fooble', action='store_true')
self.assertRaisesRegex(argparse.ArgumentError,
"ambiguous option: --foob could match --foobaz, --fooble",
self.parser.parse_args, ['--foob'])

def test_os_error(self):
self.parser.add_argument('file')
self.assertRaisesRegex(argparse.ArgumentError,
"No such file or directory: 'no-such-file'",
self.parser.parse_args, ['@no-such-file'])


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

0 comments on commit 81a654a

Please sign in to comment.