Skip to content

Commit 624be86

Browse files
pythonGH-99749: Add optional feature to suggest correct names (ArgumentParser) (pythonGH-124456)
1 parent a5a7f5e commit 624be86

File tree

4 files changed

+144
-23
lines changed

4 files changed

+144
-23
lines changed

Doc/library/argparse.rst

+27-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ ArgumentParser objects
6161
formatter_class=argparse.HelpFormatter, \
6262
prefix_chars='-', fromfile_prefix_chars=None, \
6363
argument_default=None, conflict_handler='error', \
64-
add_help=True, allow_abbrev=True, exit_on_error=True)
64+
add_help=True, allow_abbrev=True, exit_on_error=True, \
65+
suggest_on_error=False)
6566

6667
Create a new :class:`ArgumentParser` object. All parameters should be passed
6768
as keyword arguments. Each parameter has its own more detailed description
@@ -103,6 +104,10 @@ ArgumentParser objects
103104
* exit_on_error_ - Determines whether or not ArgumentParser exits with
104105
error info when an error occurs. (default: ``True``)
105106

107+
* suggest_on_error_ - Enables suggestions for mistyped argument choices
108+
and subparser names (default: ``False``)
109+
110+
106111
.. versionchanged:: 3.5
107112
*allow_abbrev* parameter was added.
108113

@@ -559,6 +564,27 @@ If the user would like to catch errors manually, the feature can be enabled by s
559564

560565
.. versionadded:: 3.9
561566

567+
suggest_on_error
568+
^^^^^^^^^^^^^^^^
569+
570+
By default, when a user passes an invalid argument choice or subparser name,
571+
:class:`ArgumentParser` will exit with error info and list the permissible
572+
argument choices (if specified) or subparser names as part of the error message.
573+
574+
If the user would like to enable suggestions for mistyped argument choices and
575+
subparser names, the feature can be enabled by setting ``suggest_on_error`` to
576+
``True``. Note that this only applies for arguments when the choices specified
577+
are strings::
578+
579+
>>> parser = argparse.ArgumentParser(description='Process some integers.', suggest_on_error=True)
580+
>>> parser.add_argument('--action', choices=['sum', 'max'])
581+
>>> parser.add_argument('integers', metavar='N', type=int, nargs='+',
582+
... help='an integer for the accumulator')
583+
>>> parser.parse_args(['--action', 'sumn', 1, 2, 3])
584+
tester.py: error: argument --action: invalid choice: 'sumn', maybe you meant 'sum'? (choose from 'sum', 'max')
585+
586+
.. versionadded:: 3.14
587+
562588

563589
The add_argument() method
564590
-------------------------

Lib/argparse.py

+26-9
Original file line numberDiff line numberDiff line change
@@ -1773,6 +1773,8 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
17731773
- allow_abbrev -- Allow long options to be abbreviated unambiguously
17741774
- exit_on_error -- Determines whether or not ArgumentParser exits with
17751775
error info when an error occurs
1776+
- suggest_on_error - Enables suggestions for mistyped argument choices
1777+
and subparser names. (default: ``False``)
17761778
"""
17771779

17781780
def __init__(self,
@@ -1788,7 +1790,8 @@ def __init__(self,
17881790
conflict_handler='error',
17891791
add_help=True,
17901792
allow_abbrev=True,
1791-
exit_on_error=True):
1793+
exit_on_error=True,
1794+
suggest_on_error=False):
17921795

17931796
superinit = super(ArgumentParser, self).__init__
17941797
superinit(description=description,
@@ -1804,6 +1807,7 @@ def __init__(self,
18041807
self.add_help = add_help
18051808
self.allow_abbrev = allow_abbrev
18061809
self.exit_on_error = exit_on_error
1810+
self.suggest_on_error = suggest_on_error
18071811

18081812
add_group = self.add_argument_group
18091813
self._positionals = add_group(_('positional arguments'))
@@ -2601,14 +2605,27 @@ def _get_value(self, action, arg_string):
26012605
def _check_value(self, action, value):
26022606
# converted value must be one of the choices (if specified)
26032607
choices = action.choices
2604-
if choices is not None:
2605-
if isinstance(choices, str):
2606-
choices = iter(choices)
2607-
if value not in choices:
2608-
args = {'value': str(value),
2609-
'choices': ', '.join(map(str, action.choices))}
2610-
msg = _('invalid choice: %(value)r (choose from %(choices)s)')
2611-
raise ArgumentError(action, msg % args)
2608+
if choices is None:
2609+
return
2610+
2611+
if isinstance(choices, str):
2612+
choices = iter(choices)
2613+
2614+
if value not in choices:
2615+
args = {'value': str(value),
2616+
'choices': ', '.join(map(str, action.choices))}
2617+
msg = _('invalid choice: %(value)r (choose from %(choices)s)')
2618+
2619+
if self.suggest_on_error and isinstance(value, str):
2620+
if all(isinstance(choice, str) for choice in action.choices):
2621+
import difflib
2622+
suggestions = difflib.get_close_matches(value, action.choices, 1)
2623+
if suggestions:
2624+
args['closest'] = suggestions[0]
2625+
msg = _('invalid choice: %(value)r, maybe you meant %(closest)r? '
2626+
'(choose from %(choices)s)')
2627+
2628+
raise ArgumentError(action, msg % args)
26122629

26132630
# =======================
26142631
# Help-formatting methods

Lib/test/test_argparse.py

+90-13
Original file line numberDiff line numberDiff line change
@@ -2253,6 +2253,95 @@ class TestNegativeNumber(ParserTestCase):
22532253
('--complex -1e-3j', NS(int=None, float=None, complex=-0.001j)),
22542254
]
22552255

2256+
class TestArgumentAndSubparserSuggestions(TestCase):
2257+
"""Test error handling and suggestion when a user makes a typo"""
2258+
2259+
def test_wrong_argument_error_with_suggestions(self):
2260+
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
2261+
parser.add_argument('foo', choices=['bar', 'baz'])
2262+
with self.assertRaises(ArgumentParserError) as excinfo:
2263+
parser.parse_args(('bazz',))
2264+
self.assertIn(
2265+
"error: argument foo: invalid choice: 'bazz', maybe you meant 'baz'? (choose from bar, baz)",
2266+
excinfo.exception.stderr
2267+
)
2268+
2269+
def test_wrong_argument_error_no_suggestions(self):
2270+
parser = ErrorRaisingArgumentParser(suggest_on_error=False)
2271+
parser.add_argument('foo', choices=['bar', 'baz'])
2272+
with self.assertRaises(ArgumentParserError) as excinfo:
2273+
parser.parse_args(('bazz',))
2274+
self.assertIn(
2275+
"error: argument foo: invalid choice: 'bazz' (choose from bar, baz)",
2276+
excinfo.exception.stderr,
2277+
)
2278+
2279+
def test_wrong_argument_subparsers_with_suggestions(self):
2280+
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
2281+
subparsers = parser.add_subparsers(required=True)
2282+
subparsers.add_parser('foo')
2283+
subparsers.add_parser('bar')
2284+
with self.assertRaises(ArgumentParserError) as excinfo:
2285+
parser.parse_args(('baz',))
2286+
self.assertIn(
2287+
"error: argument {foo,bar}: invalid choice: 'baz', maybe you meant"
2288+
" 'bar'? (choose from foo, bar)",
2289+
excinfo.exception.stderr,
2290+
)
2291+
2292+
def test_wrong_argument_subparsers_no_suggestions(self):
2293+
parser = ErrorRaisingArgumentParser(suggest_on_error=False)
2294+
subparsers = parser.add_subparsers(required=True)
2295+
subparsers.add_parser('foo')
2296+
subparsers.add_parser('bar')
2297+
with self.assertRaises(ArgumentParserError) as excinfo:
2298+
parser.parse_args(('baz',))
2299+
self.assertIn(
2300+
"error: argument {foo,bar}: invalid choice: 'baz' (choose from foo, bar)",
2301+
excinfo.exception.stderr,
2302+
)
2303+
2304+
def test_wrong_argument_no_suggestion_implicit(self):
2305+
parser = ErrorRaisingArgumentParser()
2306+
parser.add_argument('foo', choices=['bar', 'baz'])
2307+
with self.assertRaises(ArgumentParserError) as excinfo:
2308+
parser.parse_args(('bazz',))
2309+
self.assertIn(
2310+
"error: argument foo: invalid choice: 'bazz' (choose from bar, baz)",
2311+
excinfo.exception.stderr,
2312+
)
2313+
2314+
def test_suggestions_choices_empty(self):
2315+
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
2316+
parser.add_argument('foo', choices=[])
2317+
with self.assertRaises(ArgumentParserError) as excinfo:
2318+
parser.parse_args(('bazz',))
2319+
self.assertIn(
2320+
"error: argument foo: invalid choice: 'bazz' (choose from )",
2321+
excinfo.exception.stderr,
2322+
)
2323+
2324+
def test_suggestions_choices_int(self):
2325+
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
2326+
parser.add_argument('foo', choices=[1, 2])
2327+
with self.assertRaises(ArgumentParserError) as excinfo:
2328+
parser.parse_args(('3',))
2329+
self.assertIn(
2330+
"error: argument foo: invalid choice: '3' (choose from 1, 2)",
2331+
excinfo.exception.stderr,
2332+
)
2333+
2334+
def test_suggestions_choices_mixed_types(self):
2335+
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
2336+
parser.add_argument('foo', choices=[1, '2'])
2337+
with self.assertRaises(ArgumentParserError) as excinfo:
2338+
parser.parse_args(('3',))
2339+
self.assertIn(
2340+
"error: argument foo: invalid choice: '3' (choose from 1, 2)",
2341+
excinfo.exception.stderr,
2342+
)
2343+
2344+
22562345
class TestInvalidAction(TestCase):
22572346
"""Test invalid user defined Action"""
22582347

@@ -2505,18 +2594,6 @@ def test_required_subparsers_no_destination_error(self):
25052594
'error: the following arguments are required: {foo,bar}\n$'
25062595
)
25072596

2508-
def test_wrong_argument_subparsers_no_destination_error(self):
2509-
parser = ErrorRaisingArgumentParser()
2510-
subparsers = parser.add_subparsers(required=True)
2511-
subparsers.add_parser('foo')
2512-
subparsers.add_parser('bar')
2513-
with self.assertRaises(ArgumentParserError) as excinfo:
2514-
parser.parse_args(('baz',))
2515-
self.assertRegex(
2516-
excinfo.exception.stderr,
2517-
r"error: argument {foo,bar}: invalid choice: 'baz' \(choose from foo, bar\)\n$"
2518-
)
2519-
25202597
def test_optional_subparsers(self):
25212598
parser = ErrorRaisingArgumentParser()
25222599
subparsers = parser.add_subparsers(dest='command', required=False)
@@ -2862,7 +2939,7 @@ def test_single_parent_mutex(self):
28622939
parser = ErrorRaisingArgumentParser(parents=[self.ab_mutex_parent])
28632940
self._test_mutex_ab(parser.parse_args)
28642941

2865-
def test_single_granparent_mutex(self):
2942+
def test_single_grandparent_mutex(self):
28662943
parents = [self.ab_mutex_parent]
28672944
parser = ErrorRaisingArgumentParser(add_help=False, parents=parents)
28682945
parser = ErrorRaisingArgumentParser(parents=[parser])
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Adds a feature to optionally enable suggestions for argument choices and subparser names if mistyped by the user.

0 commit comments

Comments
 (0)