mirror of https://github.com/python/cpython
GH-99749: Add optional feature to suggest correct names (ArgumentParser) (GH-124456)
This commit is contained in:
parent
a5a7f5e16d
commit
624be8699a
|
@ -61,7 +61,8 @@ ArgumentParser objects
|
||||||
formatter_class=argparse.HelpFormatter, \
|
formatter_class=argparse.HelpFormatter, \
|
||||||
prefix_chars='-', fromfile_prefix_chars=None, \
|
prefix_chars='-', fromfile_prefix_chars=None, \
|
||||||
argument_default=None, conflict_handler='error', \
|
argument_default=None, conflict_handler='error', \
|
||||||
add_help=True, allow_abbrev=True, exit_on_error=True)
|
add_help=True, allow_abbrev=True, exit_on_error=True, \
|
||||||
|
suggest_on_error=False)
|
||||||
|
|
||||||
Create a new :class:`ArgumentParser` object. All parameters should be passed
|
Create a new :class:`ArgumentParser` object. All parameters should be passed
|
||||||
as keyword arguments. Each parameter has its own more detailed description
|
as keyword arguments. Each parameter has its own more detailed description
|
||||||
|
@ -103,6 +104,10 @@ ArgumentParser objects
|
||||||
* exit_on_error_ - Determines whether or not ArgumentParser exits with
|
* exit_on_error_ - Determines whether or not ArgumentParser exits with
|
||||||
error info when an error occurs. (default: ``True``)
|
error info when an error occurs. (default: ``True``)
|
||||||
|
|
||||||
|
* suggest_on_error_ - Enables suggestions for mistyped argument choices
|
||||||
|
and subparser names (default: ``False``)
|
||||||
|
|
||||||
|
|
||||||
.. versionchanged:: 3.5
|
.. versionchanged:: 3.5
|
||||||
*allow_abbrev* parameter was added.
|
*allow_abbrev* parameter was added.
|
||||||
|
|
||||||
|
@ -559,6 +564,27 @@ If the user would like to catch errors manually, the feature can be enabled by s
|
||||||
|
|
||||||
.. versionadded:: 3.9
|
.. versionadded:: 3.9
|
||||||
|
|
||||||
|
suggest_on_error
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
By default, when a user passes an invalid argument choice or subparser name,
|
||||||
|
:class:`ArgumentParser` will exit with error info and list the permissible
|
||||||
|
argument choices (if specified) or subparser names as part of the error message.
|
||||||
|
|
||||||
|
If the user would like to enable suggestions for mistyped argument choices and
|
||||||
|
subparser names, the feature can be enabled by setting ``suggest_on_error`` to
|
||||||
|
``True``. Note that this only applies for arguments when the choices specified
|
||||||
|
are strings::
|
||||||
|
|
||||||
|
>>> parser = argparse.ArgumentParser(description='Process some integers.', suggest_on_error=True)
|
||||||
|
>>> parser.add_argument('--action', choices=['sum', 'max'])
|
||||||
|
>>> parser.add_argument('integers', metavar='N', type=int, nargs='+',
|
||||||
|
... help='an integer for the accumulator')
|
||||||
|
>>> parser.parse_args(['--action', 'sumn', 1, 2, 3])
|
||||||
|
tester.py: error: argument --action: invalid choice: 'sumn', maybe you meant 'sum'? (choose from 'sum', 'max')
|
||||||
|
|
||||||
|
.. versionadded:: 3.14
|
||||||
|
|
||||||
|
|
||||||
The add_argument() method
|
The add_argument() method
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
|
@ -1773,6 +1773,8 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
- allow_abbrev -- Allow long options to be abbreviated unambiguously
|
- allow_abbrev -- Allow long options to be abbreviated unambiguously
|
||||||
- exit_on_error -- Determines whether or not ArgumentParser exits with
|
- exit_on_error -- Determines whether or not ArgumentParser exits with
|
||||||
error info when an error occurs
|
error info when an error occurs
|
||||||
|
- suggest_on_error - Enables suggestions for mistyped argument choices
|
||||||
|
and subparser names. (default: ``False``)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
|
@ -1788,7 +1790,8 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
conflict_handler='error',
|
conflict_handler='error',
|
||||||
add_help=True,
|
add_help=True,
|
||||||
allow_abbrev=True,
|
allow_abbrev=True,
|
||||||
exit_on_error=True):
|
exit_on_error=True,
|
||||||
|
suggest_on_error=False):
|
||||||
|
|
||||||
superinit = super(ArgumentParser, self).__init__
|
superinit = super(ArgumentParser, self).__init__
|
||||||
superinit(description=description,
|
superinit(description=description,
|
||||||
|
@ -1804,6 +1807,7 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
self.add_help = add_help
|
self.add_help = add_help
|
||||||
self.allow_abbrev = allow_abbrev
|
self.allow_abbrev = allow_abbrev
|
||||||
self.exit_on_error = exit_on_error
|
self.exit_on_error = exit_on_error
|
||||||
|
self.suggest_on_error = suggest_on_error
|
||||||
|
|
||||||
add_group = self.add_argument_group
|
add_group = self.add_argument_group
|
||||||
self._positionals = add_group(_('positional arguments'))
|
self._positionals = add_group(_('positional arguments'))
|
||||||
|
@ -2601,14 +2605,27 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
|
||||||
def _check_value(self, action, value):
|
def _check_value(self, action, value):
|
||||||
# converted value must be one of the choices (if specified)
|
# converted value must be one of the choices (if specified)
|
||||||
choices = action.choices
|
choices = action.choices
|
||||||
if choices is not None:
|
if choices is None:
|
||||||
if isinstance(choices, str):
|
return
|
||||||
choices = iter(choices)
|
|
||||||
if value not in choices:
|
if isinstance(choices, str):
|
||||||
args = {'value': str(value),
|
choices = iter(choices)
|
||||||
'choices': ', '.join(map(str, action.choices))}
|
|
||||||
msg = _('invalid choice: %(value)r (choose from %(choices)s)')
|
if value not in choices:
|
||||||
raise ArgumentError(action, msg % args)
|
args = {'value': str(value),
|
||||||
|
'choices': ', '.join(map(str, action.choices))}
|
||||||
|
msg = _('invalid choice: %(value)r (choose from %(choices)s)')
|
||||||
|
|
||||||
|
if self.suggest_on_error and isinstance(value, str):
|
||||||
|
if all(isinstance(choice, str) for choice in action.choices):
|
||||||
|
import difflib
|
||||||
|
suggestions = difflib.get_close_matches(value, action.choices, 1)
|
||||||
|
if suggestions:
|
||||||
|
args['closest'] = suggestions[0]
|
||||||
|
msg = _('invalid choice: %(value)r, maybe you meant %(closest)r? '
|
||||||
|
'(choose from %(choices)s)')
|
||||||
|
|
||||||
|
raise ArgumentError(action, msg % args)
|
||||||
|
|
||||||
# =======================
|
# =======================
|
||||||
# Help-formatting methods
|
# Help-formatting methods
|
||||||
|
|
|
@ -2253,6 +2253,95 @@ class TestNegativeNumber(ParserTestCase):
|
||||||
('--complex -1e-3j', NS(int=None, float=None, complex=-0.001j)),
|
('--complex -1e-3j', NS(int=None, float=None, complex=-0.001j)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
class TestArgumentAndSubparserSuggestions(TestCase):
|
||||||
|
"""Test error handling and suggestion when a user makes a typo"""
|
||||||
|
|
||||||
|
def test_wrong_argument_error_with_suggestions(self):
|
||||||
|
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
|
||||||
|
parser.add_argument('foo', choices=['bar', 'baz'])
|
||||||
|
with self.assertRaises(ArgumentParserError) as excinfo:
|
||||||
|
parser.parse_args(('bazz',))
|
||||||
|
self.assertIn(
|
||||||
|
"error: argument foo: invalid choice: 'bazz', maybe you meant 'baz'? (choose from bar, baz)",
|
||||||
|
excinfo.exception.stderr
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_wrong_argument_error_no_suggestions(self):
|
||||||
|
parser = ErrorRaisingArgumentParser(suggest_on_error=False)
|
||||||
|
parser.add_argument('foo', choices=['bar', 'baz'])
|
||||||
|
with self.assertRaises(ArgumentParserError) as excinfo:
|
||||||
|
parser.parse_args(('bazz',))
|
||||||
|
self.assertIn(
|
||||||
|
"error: argument foo: invalid choice: 'bazz' (choose from bar, baz)",
|
||||||
|
excinfo.exception.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_wrong_argument_subparsers_with_suggestions(self):
|
||||||
|
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
|
||||||
|
subparsers = parser.add_subparsers(required=True)
|
||||||
|
subparsers.add_parser('foo')
|
||||||
|
subparsers.add_parser('bar')
|
||||||
|
with self.assertRaises(ArgumentParserError) as excinfo:
|
||||||
|
parser.parse_args(('baz',))
|
||||||
|
self.assertIn(
|
||||||
|
"error: argument {foo,bar}: invalid choice: 'baz', maybe you meant"
|
||||||
|
" 'bar'? (choose from foo, bar)",
|
||||||
|
excinfo.exception.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_wrong_argument_subparsers_no_suggestions(self):
|
||||||
|
parser = ErrorRaisingArgumentParser(suggest_on_error=False)
|
||||||
|
subparsers = parser.add_subparsers(required=True)
|
||||||
|
subparsers.add_parser('foo')
|
||||||
|
subparsers.add_parser('bar')
|
||||||
|
with self.assertRaises(ArgumentParserError) as excinfo:
|
||||||
|
parser.parse_args(('baz',))
|
||||||
|
self.assertIn(
|
||||||
|
"error: argument {foo,bar}: invalid choice: 'baz' (choose from foo, bar)",
|
||||||
|
excinfo.exception.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_wrong_argument_no_suggestion_implicit(self):
|
||||||
|
parser = ErrorRaisingArgumentParser()
|
||||||
|
parser.add_argument('foo', choices=['bar', 'baz'])
|
||||||
|
with self.assertRaises(ArgumentParserError) as excinfo:
|
||||||
|
parser.parse_args(('bazz',))
|
||||||
|
self.assertIn(
|
||||||
|
"error: argument foo: invalid choice: 'bazz' (choose from bar, baz)",
|
||||||
|
excinfo.exception.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_suggestions_choices_empty(self):
|
||||||
|
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
|
||||||
|
parser.add_argument('foo', choices=[])
|
||||||
|
with self.assertRaises(ArgumentParserError) as excinfo:
|
||||||
|
parser.parse_args(('bazz',))
|
||||||
|
self.assertIn(
|
||||||
|
"error: argument foo: invalid choice: 'bazz' (choose from )",
|
||||||
|
excinfo.exception.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_suggestions_choices_int(self):
|
||||||
|
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
|
||||||
|
parser.add_argument('foo', choices=[1, 2])
|
||||||
|
with self.assertRaises(ArgumentParserError) as excinfo:
|
||||||
|
parser.parse_args(('3',))
|
||||||
|
self.assertIn(
|
||||||
|
"error: argument foo: invalid choice: '3' (choose from 1, 2)",
|
||||||
|
excinfo.exception.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_suggestions_choices_mixed_types(self):
|
||||||
|
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
|
||||||
|
parser.add_argument('foo', choices=[1, '2'])
|
||||||
|
with self.assertRaises(ArgumentParserError) as excinfo:
|
||||||
|
parser.parse_args(('3',))
|
||||||
|
self.assertIn(
|
||||||
|
"error: argument foo: invalid choice: '3' (choose from 1, 2)",
|
||||||
|
excinfo.exception.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestInvalidAction(TestCase):
|
class TestInvalidAction(TestCase):
|
||||||
"""Test invalid user defined Action"""
|
"""Test invalid user defined Action"""
|
||||||
|
|
||||||
|
@ -2505,18 +2594,6 @@ class TestAddSubparsers(TestCase):
|
||||||
'error: the following arguments are required: {foo,bar}\n$'
|
'error: the following arguments are required: {foo,bar}\n$'
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_wrong_argument_subparsers_no_destination_error(self):
|
|
||||||
parser = ErrorRaisingArgumentParser()
|
|
||||||
subparsers = parser.add_subparsers(required=True)
|
|
||||||
subparsers.add_parser('foo')
|
|
||||||
subparsers.add_parser('bar')
|
|
||||||
with self.assertRaises(ArgumentParserError) as excinfo:
|
|
||||||
parser.parse_args(('baz',))
|
|
||||||
self.assertRegex(
|
|
||||||
excinfo.exception.stderr,
|
|
||||||
r"error: argument {foo,bar}: invalid choice: 'baz' \(choose from foo, bar\)\n$"
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_optional_subparsers(self):
|
def test_optional_subparsers(self):
|
||||||
parser = ErrorRaisingArgumentParser()
|
parser = ErrorRaisingArgumentParser()
|
||||||
subparsers = parser.add_subparsers(dest='command', required=False)
|
subparsers = parser.add_subparsers(dest='command', required=False)
|
||||||
|
@ -2862,7 +2939,7 @@ class TestParentParsers(TestCase):
|
||||||
parser = ErrorRaisingArgumentParser(parents=[self.ab_mutex_parent])
|
parser = ErrorRaisingArgumentParser(parents=[self.ab_mutex_parent])
|
||||||
self._test_mutex_ab(parser.parse_args)
|
self._test_mutex_ab(parser.parse_args)
|
||||||
|
|
||||||
def test_single_granparent_mutex(self):
|
def test_single_grandparent_mutex(self):
|
||||||
parents = [self.ab_mutex_parent]
|
parents = [self.ab_mutex_parent]
|
||||||
parser = ErrorRaisingArgumentParser(add_help=False, parents=parents)
|
parser = ErrorRaisingArgumentParser(add_help=False, parents=parents)
|
||||||
parser = ErrorRaisingArgumentParser(parents=[parser])
|
parser = ErrorRaisingArgumentParser(parents=[parser])
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Adds a feature to optionally enable suggestions for argument choices and subparser names if mistyped by the user.
|
Loading…
Reference in New Issue