gh-126068: Fix exceptions in the argparse module (GH-126069)

* Only error messages for ArgumentError and ArgumentTypeError are now
  translated.
* ArgumentError is now only used for command line errors, not for logical
  errors in the program.
* TypeError is now raised instead of ValueError for some logical errors.
This commit is contained in:
Serhiy Storchaka 2024-10-30 18:14:27 +02:00 committed by GitHub
parent 1f16df4bfe
commit cc9a183993
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 72 additions and 58 deletions

View File

@ -846,7 +846,7 @@ class Action(_AttributeHolder):
return self.option_strings[0] return self.option_strings[0]
def __call__(self, parser, namespace, values, option_string=None): def __call__(self, parser, namespace, values, option_string=None):
raise NotImplementedError(_('.__call__() not defined')) raise NotImplementedError('.__call__() not defined')
class BooleanOptionalAction(Action): class BooleanOptionalAction(Action):
@ -1172,11 +1172,10 @@ class _SubParsersAction(Action):
aliases = kwargs.pop('aliases', ()) aliases = kwargs.pop('aliases', ())
if name in self._name_parser_map: if name in self._name_parser_map:
raise ArgumentError(self, _('conflicting subparser: %s') % name) raise ValueError(f'conflicting subparser: {name}')
for alias in aliases: for alias in aliases:
if alias in self._name_parser_map: if alias in self._name_parser_map:
raise ArgumentError( raise ValueError(f'conflicting subparser alias: {alias}')
self, _('conflicting subparser alias: %s') % alias)
# create a pseudo-action to hold the choice help # create a pseudo-action to hold the choice help
if 'help' in kwargs: if 'help' in kwargs:
@ -1430,8 +1429,8 @@ class _ActionsContainer(object):
chars = self.prefix_chars chars = self.prefix_chars
if not args or len(args) == 1 and args[0][0] not in chars: if not args or len(args) == 1 and args[0][0] not in chars:
if args and 'dest' in kwargs: if args and 'dest' in kwargs:
raise ValueError('dest supplied twice for positional argument,' raise TypeError('dest supplied twice for positional argument,'
' did you mean metavar?') ' did you mean metavar?')
kwargs = self._get_positional_kwargs(*args, **kwargs) kwargs = self._get_positional_kwargs(*args, **kwargs)
# otherwise, we're adding an optional argument # otherwise, we're adding an optional argument
@ -1450,7 +1449,7 @@ class _ActionsContainer(object):
action_name = kwargs.get('action') action_name = kwargs.get('action')
action_class = self._pop_action_class(kwargs) action_class = self._pop_action_class(kwargs)
if not callable(action_class): if not callable(action_class):
raise ValueError('unknown action "%s"' % (action_class,)) raise ValueError('unknown action {action_class!r}')
action = action_class(**kwargs) action = action_class(**kwargs)
# raise an error if action for positional argument does not # raise an error if action for positional argument does not
@ -1461,11 +1460,11 @@ class _ActionsContainer(object):
# raise an error if the action type is not callable # raise an error if the action type is not callable
type_func = self._registry_get('type', action.type, action.type) type_func = self._registry_get('type', action.type, action.type)
if not callable(type_func): if not callable(type_func):
raise ValueError('%r is not callable' % (type_func,)) raise TypeError(f'{type_func!r} is not callable')
if type_func is FileType: if type_func is FileType:
raise ValueError('%r is a FileType class object, instance of it' raise TypeError(f'{type_func!r} is a FileType class object, '
' must be passed' % (type_func,)) f'instance of it must be passed')
# raise an error if the metavar does not match the type # raise an error if the metavar does not match the type
if hasattr(self, "_get_formatter"): if hasattr(self, "_get_formatter"):
@ -1518,8 +1517,8 @@ class _ActionsContainer(object):
if group.title in title_group_map: if group.title in title_group_map:
# This branch could happen if a derived class added # This branch could happen if a derived class added
# groups with duplicated titles in __init__ # groups with duplicated titles in __init__
msg = _('cannot merge actions - two groups are named %r') msg = f'cannot merge actions - two groups are named {group.title!r}'
raise ValueError(msg % (group.title)) raise ValueError(msg)
title_group_map[group.title] = group title_group_map[group.title] = group
# map each action to its group # map each action to its group
@ -1560,7 +1559,7 @@ class _ActionsContainer(object):
def _get_positional_kwargs(self, dest, **kwargs): def _get_positional_kwargs(self, dest, **kwargs):
# make sure required is not specified # make sure required is not specified
if 'required' in kwargs: if 'required' in kwargs:
msg = _("'required' is an invalid argument for positionals") msg = "'required' is an invalid argument for positionals"
raise TypeError(msg) raise TypeError(msg)
# mark positional arguments as required if at least one is # mark positional arguments as required if at least one is
@ -1581,11 +1580,9 @@ class _ActionsContainer(object):
for option_string in args: for option_string in args:
# error on strings that don't start with an appropriate prefix # error on strings that don't start with an appropriate prefix
if not option_string[0] in self.prefix_chars: if not option_string[0] in self.prefix_chars:
args = {'option': option_string, raise ValueError(
'prefix_chars': self.prefix_chars} f'invalid option string {option_string!r}: '
msg = _('invalid option string %(option)r: ' f'must start with a character {self.prefix_chars!r}')
'must start with a character %(prefix_chars)r')
raise ValueError(msg % args)
# strings starting with two prefix characters are long options # strings starting with two prefix characters are long options
option_strings.append(option_string) option_strings.append(option_string)
@ -1601,8 +1598,8 @@ class _ActionsContainer(object):
dest_option_string = option_strings[0] dest_option_string = option_strings[0]
dest = dest_option_string.lstrip(self.prefix_chars) dest = dest_option_string.lstrip(self.prefix_chars)
if not dest: if not dest:
msg = _('dest= is required for options like %r') msg = f'dest= is required for options like {option_string!r}'
raise ValueError(msg % option_string) raise TypeError(msg)
dest = dest.replace('-', '_') dest = dest.replace('-', '_')
# return the updated keyword arguments # return the updated keyword arguments
@ -1618,8 +1615,8 @@ class _ActionsContainer(object):
try: try:
return getattr(self, handler_func_name) return getattr(self, handler_func_name)
except AttributeError: except AttributeError:
msg = _('invalid conflict_resolution value: %r') msg = f'invalid conflict_resolution value: {self.conflict_handler!r}'
raise ValueError(msg % self.conflict_handler) raise ValueError(msg)
def _check_conflict(self, action): def _check_conflict(self, action):
@ -1727,7 +1724,7 @@ class _MutuallyExclusiveGroup(_ArgumentGroup):
def _add_action(self, action): def _add_action(self, action):
if action.required: if action.required:
msg = _('mutually exclusive arguments must be optional') msg = 'mutually exclusive arguments must be optional'
raise ValueError(msg) raise ValueError(msg)
action = self._container._add_action(action) action = self._container._add_action(action)
self._group_actions.append(action) self._group_actions.append(action)
@ -1871,7 +1868,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
# ================================== # ==================================
def add_subparsers(self, **kwargs): def add_subparsers(self, **kwargs):
if self._subparsers is not None: if self._subparsers is not None:
raise ArgumentError(None, _('cannot have multiple subparser arguments')) raise ValueError('cannot have multiple subparser arguments')
# add the parser class to the arguments if it's not present # add the parser class to the arguments if it's not present
kwargs.setdefault('parser_class', type(self)) kwargs.setdefault('parser_class', type(self))
@ -2565,8 +2562,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
def _get_value(self, action, arg_string): def _get_value(self, action, arg_string):
type_func = self._registry_get('type', action.type, action.type) type_func = self._registry_get('type', action.type, action.type)
if not callable(type_func): if not callable(type_func):
msg = _('%r is not callable') raise TypeError(f'{type_func!r} is not callable')
raise ArgumentError(action, msg % type_func)
# convert the value to the appropriate type # convert the value to the appropriate type
try: try:

View File

@ -2055,7 +2055,7 @@ class TestFileTypeMissingInitialization(TestCase):
def test(self): def test(self):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
with self.assertRaises(ValueError) as cm: with self.assertRaises(TypeError) as cm:
parser.add_argument('-x', type=argparse.FileType) parser.add_argument('-x', type=argparse.FileType)
self.assertEqual( self.assertEqual(
@ -2377,11 +2377,24 @@ class TestInvalidAction(TestCase):
self.assertRaises(NotImplementedError, parser.parse_args, ['--foo', 'bar']) self.assertRaises(NotImplementedError, parser.parse_args, ['--foo', 'bar'])
def test_modified_invalid_action(self): def test_modified_invalid_action(self):
parser = ErrorRaisingArgumentParser() parser = argparse.ArgumentParser(exit_on_error=False)
action = parser.add_argument('--foo') action = parser.add_argument('--foo')
# Someone got crazy and did this # Someone got crazy and did this
action.type = 1 action.type = 1
self.assertRaises(ArgumentParserError, parser.parse_args, ['--foo', 'bar']) self.assertRaisesRegex(TypeError, '1 is not callable',
parser.parse_args, ['--foo', 'bar'])
action.type = ()
self.assertRaisesRegex(TypeError, r'\(\) is not callable',
parser.parse_args, ['--foo', 'bar'])
# It is impossible to distinguish a TypeError raised due to a mismatch
# of the required function arguments from a TypeError raised for an incorrect
# argument value, and using the heavy inspection machinery is not worthwhile
# as it does not reliably work in all cases.
# Therefore, a generic ArgumentError is raised to handle this logical error.
action.type = pow
self.assertRaisesRegex(argparse.ArgumentError,
"argument --foo: invalid pow value: 'bar'",
parser.parse_args, ['--foo', 'bar'])
# ================ # ================
@ -2418,7 +2431,7 @@ class TestAddSubparsers(TestCase):
else: else:
subparsers_kwargs['help'] = 'command help' subparsers_kwargs['help'] = 'command help'
subparsers = parser.add_subparsers(**subparsers_kwargs) subparsers = parser.add_subparsers(**subparsers_kwargs)
self.assertRaisesRegex(argparse.ArgumentError, self.assertRaisesRegex(ValueError,
'cannot have multiple subparser arguments', 'cannot have multiple subparser arguments',
parser.add_subparsers) parser.add_subparsers)
@ -5534,20 +5547,27 @@ class TestInvalidArgumentConstructors(TestCase):
self.assertTypeError(action=action) self.assertTypeError(action=action)
def test_invalid_option_strings(self): def test_invalid_option_strings(self):
self.assertValueError('--') self.assertTypeError('-', errmsg='dest= is required')
self.assertValueError('---') self.assertTypeError('--', errmsg='dest= is required')
self.assertTypeError('---', errmsg='dest= is required')
def test_invalid_prefix(self): def test_invalid_prefix(self):
self.assertValueError('--foo', '+foo') self.assertValueError('--foo', '+foo',
errmsg='must start with a character')
def test_invalid_type(self): def test_invalid_type(self):
self.assertValueError('--foo', type='int') self.assertTypeError('--foo', type='int',
self.assertValueError('--foo', type=(int, float)) errmsg="'int' is not callable")
self.assertTypeError('--foo', type=(int, float),
errmsg='is not callable')
def test_invalid_action(self): def test_invalid_action(self):
self.assertValueError('-x', action='foo') self.assertValueError('-x', action='foo',
self.assertValueError('foo', action='baz') errmsg='unknown action')
self.assertValueError('--foo', action=('store', 'append')) self.assertValueError('foo', action='baz',
errmsg='unknown action')
self.assertValueError('--foo', action=('store', 'append'),
errmsg='unknown action')
self.assertValueError('--foo', action="store-true", self.assertValueError('--foo', action="store-true",
errmsg='unknown action') errmsg='unknown action')
@ -5562,7 +5582,7 @@ class TestInvalidArgumentConstructors(TestCase):
def test_multiple_dest(self): def test_multiple_dest(self):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument(dest='foo') parser.add_argument(dest='foo')
with self.assertRaises(ValueError) as cm: with self.assertRaises(TypeError) as cm:
parser.add_argument('bar', dest='baz') parser.add_argument('bar', dest='baz')
self.assertIn('dest supplied twice for positional argument,' self.assertIn('dest supplied twice for positional argument,'
' did you mean metavar?', ' did you mean metavar?',
@ -5733,14 +5753,18 @@ class TestConflictHandling(TestCase):
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
sp = parser.add_subparsers() sp = parser.add_subparsers()
sp.add_parser('fullname', aliases=['alias']) sp.add_parser('fullname', aliases=['alias'])
self.assertRaises(argparse.ArgumentError, self.assertRaisesRegex(ValueError,
sp.add_parser, 'fullname') 'conflicting subparser: fullname',
self.assertRaises(argparse.ArgumentError, sp.add_parser, 'fullname')
sp.add_parser, 'alias') self.assertRaisesRegex(ValueError,
self.assertRaises(argparse.ArgumentError, 'conflicting subparser: alias',
sp.add_parser, 'other', aliases=['fullname']) sp.add_parser, 'alias')
self.assertRaises(argparse.ArgumentError, self.assertRaisesRegex(ValueError,
sp.add_parser, 'other', aliases=['alias']) 'conflicting subparser alias: fullname',
sp.add_parser, 'other', aliases=['fullname'])
self.assertRaisesRegex(ValueError,
'conflicting subparser alias: alias',
sp.add_parser, 'other', aliases=['alias'])
# ============================= # =============================

View File

@ -2,20 +2,12 @@
%(heading)s: %(heading)s:
%(prog)s: error: %(message)s\n %(prog)s: error: %(message)s\n
%(prog)s: warning: %(message)s\n %(prog)s: warning: %(message)s\n
%r is not callable
'required' is an invalid argument for positionals
.__call__() not defined
ambiguous option: %(option)s could match %(matches)s ambiguous option: %(option)s could match %(matches)s
argument "-" with mode %r argument "-" with mode %r
argument %(argument_name)s: %(message)s argument %(argument_name)s: %(message)s
argument '%(argument_name)s' is deprecated argument '%(argument_name)s' is deprecated
can't open '%(filename)s': %(error)s can't open '%(filename)s': %(error)s
cannot have multiple subparser arguments
cannot merge actions - two groups are named %r
command '%(parser_name)s' is deprecated command '%(parser_name)s' is deprecated
conflicting subparser alias: %s
conflicting subparser: %s
dest= is required for options like %r
expected at least one argument expected at least one argument
expected at most one argument expected at most one argument
expected one argument expected one argument
@ -23,9 +15,6 @@ ignored explicit argument %r
invalid %(type)s value: %(value)r invalid %(type)s value: %(value)r
invalid choice: %(value)r (choose from %(choices)s) invalid choice: %(value)r (choose from %(choices)s)
invalid choice: %(value)r, maybe you meant %(closest)r? (choose from %(choices)s) invalid choice: %(value)r, maybe you meant %(closest)r? (choose from %(choices)s)
invalid conflict_resolution value: %r
invalid option string %(option)r: must start with a character %(prefix_chars)r
mutually exclusive arguments must be optional
not allowed with argument %s not allowed with argument %s
one of the arguments %s is required one of the arguments %s is required
option '%(option)s' is deprecated option '%(option)s' is deprecated

View File

@ -0,0 +1,5 @@
Fix exceptions in the :mod:`argparse` module so that only error messages for
ArgumentError and ArgumentTypeError are now translated.
ArgumentError is now only used for command line errors, not for logical
errors in the program. TypeError is now raised instead of ValueError for
some logical errors.