diff --git a/Lib/argparse.py b/Lib/argparse.py index a819d2650e85f0..8f892d4eb4cd8b 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -1869,6 +1869,11 @@ def parse_args(self, args=None, namespace=None): args, argv = self.parse_known_args(args, namespace) if argv: msg = _('unrecognized arguments: %s') + if not self.exit_on_error: + raise ArgumentError( + None, + msg % ' '.join(argv) + ) self.error(msg % ' '.join(argv)) return args @@ -2139,8 +2144,11 @@ def consume_positionals(start_index): self._get_value(action, action.default)) if required_actions: - self.error(_('the following arguments are required: %s') % - ', '.join(required_actions)) + raise ArgumentError( + None, + _('the following arguments are required: %s') % + ', '.join(required_actions) + ) # make sure all required groups had one option present for group in self._mutually_exclusive_groups: @@ -2155,7 +2163,10 @@ 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 @@ -2384,7 +2395,10 @@ 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)) + formatted_msg = msg % ' '.join(argv) + if self.exit_on_error: + self.error(formatted_msg) + raise ArgumentError(None, formatted_msg) return args def parse_known_intermixed_args(self, args=None, namespace=None): diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 861da2326d1214..3c417e28d0ba0b 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -5594,7 +5594,8 @@ class TestExitOnError(TestCase): def setUp(self): self.parser = argparse.ArgumentParser(exit_on_error=False) - self.parser.add_argument('--integers', metavar='N', type=int) + self.parser.add_argument( + '--integers', metavar='N', type=int, required=True) def test_exit_on_error_with_good_args(self): ns = self.parser.parse_args('--integers 4'.split()) @@ -5604,6 +5605,28 @@ 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_missing_required_arg(self): + msg = 'the following arguments are required: --integers' + with self.assertRaisesRegex(argparse.ArgumentError, msg): + self.parser.parse_args([]) + + def test_exit_on_error_unknown_arg(self): + msg = 'unrecognized arguments: --unknown' + with self.assertRaisesRegex(argparse.ArgumentError, msg): + self.parser.parse_args('--integers 4 --unknown'.split()) + with self.assertRaisesRegex(argparse.ArgumentError, msg.replace('--', '')): + self.parser.parse_intermixed_args('--integers 4 unknown'.split()) + + def test_exit_on_error_mutually_exclusive_group(self): + other_parser = argparse.ArgumentParser(exit_on_error=False) + group = other_parser.add_mutually_exclusive_group(required=True) + group.add_argument('--up', action='store_true') + group.add_argument('--down', action='store_true') + + msg = 'one of the arguments --up --down is required' + with self.assertRaisesRegex(argparse.ArgumentError, msg): + other_parser.parse_args([]) + def tearDownModule(): # Remove global references to avoid looking like we have refleaks. diff --git a/Misc/NEWS.d/next/Library/2022-01-23-11-58-40.bpo-46440.tMts6U.rst b/Misc/NEWS.d/next/Library/2022-01-23-11-58-40.bpo-46440.tMts6U.rst new file mode 100644 index 00000000000000..92ea080a15d15a --- /dev/null +++ b/Misc/NEWS.d/next/Library/2022-01-23-11-58-40.bpo-46440.tMts6U.rst @@ -0,0 +1,2 @@ +The :attr:`exit_on_error` attribute of :class:`argparse.ArgumentParser` did +not always avoid exiting when set to False.