From 64b87d67f73e56ea0d87df0c573c258bab175b69 Mon Sep 17 00:00:00 2001 From: Mihail Feraru Date: Fri, 14 Apr 2023 00:34:18 +0300 Subject: [PATCH] gh-103498: argparse.ArgumentParser will always throw instead of exit if exit_on_error=False --- Doc/library/argparse.rst | 8 ++-- Lib/argparse.py | 39 +++++++++---------- Lib/test/libregrtest/cmdline.py | 4 +- ...-04-13-17-11-47.gh-issue-103498.pZTvNm.rst | 3 ++ 4 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2023-04-13-17-11-47.gh-issue-103498.pZTvNm.rst diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index ee68ac58d3de75..ccd6789d57f33c 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -2199,10 +2199,12 @@ Exiting methods raise Exception(f'Exiting because of an error: {message}') exit(status) -.. method:: ArgumentParser.error(message) +.. method:: ArgumentParser.error(message, action=None) - This method prints a usage message including the *message* to the - standard error and terminates the program with a status code of 2. + This method prints a usage message including the *message* and the *action* + causing the error (if it is set) to the standard error and terminates + the program with a status code of 2, if ``exit_on_error`` parameter is + set to ``True``. Otherwise, it raises an :exc:`ArgumentError`. Intermixed parsing diff --git a/Lib/argparse.py b/Lib/argparse.py index a819d2650e85f0..86769f0cad3723 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -92,6 +92,7 @@ import warnings from gettext import gettext as _, ngettext +from typing import Optional as _Optional SUPPRESS = '==SUPPRESS==' @@ -1896,14 +1897,7 @@ def parse_known_args(self, args=None, namespace=None): if not hasattr(namespace, dest): setattr(namespace, dest, self._defaults[dest]) - # parse the arguments and exit if there are any errors - if self.exit_on_error: - try: - namespace, args = self._parse_known_args(args, namespace) - except ArgumentError as err: - self.error(str(err)) - else: - namespace, args = self._parse_known_args(args, namespace) + namespace, args = self._parse_known_args(args, namespace) if hasattr(namespace, _UNRECOGNIZED_ARGS_ATTR): args.extend(getattr(namespace, _UNRECOGNIZED_ARGS_ATTR)) @@ -1970,7 +1964,7 @@ def take_action(action, argument_strings, option_string=None): if conflict_action in seen_non_default_actions: msg = _('not allowed with argument %s') action_name = _get_action_name(conflict_action) - raise ArgumentError(action, msg % action_name) + self.error(msg % action_name, action=action) # take the action if we didn't receive a SUPPRESS value # (e.g. from a default) @@ -2019,7 +2013,7 @@ def consume_optional(start_index): explicit_arg = new_explicit_arg else: msg = _('ignored explicit argument %r') - raise ArgumentError(action, msg % explicit_arg) + self.error(msg % explicit_arg, action=action) # if the action expect exactly one argument, we've # successfully matched the option; exit the loop @@ -2033,7 +2027,7 @@ def consume_optional(start_index): # explicit argument else: msg = _('ignored explicit argument %r') - raise ArgumentError(action, msg % explicit_arg) + self.error(msg % explicit_arg, action=action) # if there is no explicit argument, try to match the # optional's string arguments with the following strings @@ -2207,7 +2201,7 @@ def _match_argument(self, action, arg_strings_pattern): msg = ngettext('expected %s argument', 'expected %s arguments', action.nargs) % action.nargs - raise ArgumentError(action, msg) + self.error(msg, action=action) # return the number of arguments matched return len(match.group(1)) @@ -2526,7 +2520,7 @@ def _get_value(self, action, arg_string): type_func = self._registry_get('type', action.type, action.type) if not callable(type_func): msg = _('%r is not callable') - raise ArgumentError(action, msg % type_func) + self.error(msg % type_func, action=action) # convert the value to the appropriate type try: @@ -2535,14 +2529,14 @@ def _get_value(self, action, arg_string): # ArgumentTypeErrors indicate errors except ArgumentTypeError as err: msg = str(err) - raise ArgumentError(action, msg) + self.error(msg, action=action) # TypeErrors or ValueErrors also indicate errors except (TypeError, ValueError): name = getattr(action.type, '__name__', repr(action.type)) args = {'type': name, 'value': arg_string} msg = _('invalid %(type)s value: %(value)r') - raise ArgumentError(action, msg % args) + self.error(msg % args, action=action) # return the converted value return result @@ -2553,7 +2547,7 @@ def _check_value(self, action, value): args = {'value': value, 'choices': ', '.join(map(repr, action.choices))} msg = _('invalid choice: %(value)r (choose from %(choices)s)') - raise ArgumentError(action, msg % args) + self.error(msg % args, action=action) # ======================= # Help-formatting methods @@ -2617,15 +2611,18 @@ def exit(self, status=0, message=None): self._print_message(message, _sys.stderr) _sys.exit(status) - def error(self, message): - """error(message: string) - - Prints a usage message incorporating the message to stderr and - exits. + def error(self, message: str, action: _Optional[Action] = None): + """Prints a usage message incorporating the message to stderr and + exits or raises an ArgumentError if self.exit_on_error is False. If you override this in a subclass, it should not return -- it should either exit or raise an exception. """ + if not self.exit_on_error: + raise ArgumentError(action, message) + self.print_usage(_sys.stderr) + # If action is not None, custom formatting will be applied by ArgumentError + message = str(ArgumentError(action, message)) args = {'prog': self.prog, 'message': message} self.exit(2, _('%(prog)s: error: %(message)s\n') % args) diff --git a/Lib/test/libregrtest/cmdline.py b/Lib/test/libregrtest/cmdline.py index ebe57920d9185c..4d157026a271db 100644 --- a/Lib/test/libregrtest/cmdline.py +++ b/Lib/test/libregrtest/cmdline.py @@ -174,8 +174,8 @@ def __init__(self, **kwargs) -> None: class _ArgParser(argparse.ArgumentParser): - def error(self, message): - super().error(message + "\nPass -h or --help for complete help.") + def error(self, message, action=None): + super().error(message + "\nPass -h or --help for complete help.", action=action) def _create_parser(): diff --git a/Misc/NEWS.d/next/Library/2023-04-13-17-11-47.gh-issue-103498.pZTvNm.rst b/Misc/NEWS.d/next/Library/2023-04-13-17-11-47.gh-issue-103498.pZTvNm.rst new file mode 100644 index 00000000000000..cd4893db636994 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-04-13-17-11-47.gh-issue-103498.pZTvNm.rst @@ -0,0 +1,3 @@ +Fixed the bug that caused :class:`argparse.ArgumentParser` to ignore the +argument ``exit_on_error=False`` and exit instead of throwing +for some errors.