Skip to content

Commit f516bb4

Browse files
Rework to add tests and make optional
1 parent 2b802ec commit f516bb4

File tree

3 files changed

+66
-28
lines changed

3 files changed

+66
-28
lines changed

Lib/argparse.py

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@
8484
'ZERO_OR_MORE',
8585
]
8686

87-
import difflib as _difflib
8887
import os as _os
8988
import re as _re
9089
import sys as _sys
@@ -1716,6 +1715,8 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
17161715
- allow_abbrev -- Allow long options to be abbreviated unambiguously
17171716
- exit_on_error -- Determines whether or not ArgumentParser exits with
17181717
error info when an error occurs
1718+
- suggest_on_error - Enables argument choice and subparser name
1719+
suggestions on user typo
17191720
"""
17201721

17211722
def __init__(self,
@@ -1731,7 +1732,8 @@ def __init__(self,
17311732
conflict_handler='error',
17321733
add_help=True,
17331734
allow_abbrev=True,
1734-
exit_on_error=True):
1735+
exit_on_error=True,
1736+
suggest_on_error=False):
17351737

17361738
superinit = super(ArgumentParser, self).__init__
17371739
superinit(description=description,
@@ -1751,6 +1753,7 @@ def __init__(self,
17511753
self.add_help = add_help
17521754
self.allow_abbrev = allow_abbrev
17531755
self.exit_on_error = exit_on_error
1756+
self.suggest_on_error = suggest_on_error
17541757

17551758
add_group = self.add_argument_group
17561759
self._positionals = add_group(_('positional arguments'))
@@ -2559,22 +2562,23 @@ def _check_value(self, action, value):
25592562

25602563
# converted value must be one of the choices (if specified)
25612564
if action.choices is not None and value not in action.choices:
2562-
try:
2563-
closest_choice = _difflib.get_close_matches(value, action.choices, 1)
2564-
except TypeError:
2565-
closest_choice = []
2566-
25672565
args = {
25682566
'value': value,
25692567
'choices': ', '.join(map(repr, action.choices)),
25702568
}
2571-
if closest_choice:
2572-
closest_choice = closest_choice[0]
2573-
args['closest'] = closest_choice
2574-
msg = _('invalid choice: %(value)r, maybe you meant %(closest)r? '
2575-
'(choose from %(choices)s)')
2576-
else:
2577-
msg = _('invalid choice: %(value)r (choose from %(choices)s)')
2569+
msg = _('invalid choice: %(value)r (choose from %(choices)s)')
2570+
2571+
if self.suggest_on_error:
2572+
try:
2573+
import difflib as _difflib
2574+
closest_choice = _difflib.get_close_matches(value, action.choices, 1)
2575+
if closest_choice:
2576+
closest_choice = closest_choice[0]
2577+
args['closest'] = closest_choice
2578+
msg = _('invalid choice: %(value)r, maybe you meant %(closest)r? '
2579+
'(choose from %(choices)s)')
2580+
except TypeError:
2581+
closest_choice = []
25782582

25792583
raise ArgumentError(action, msg % args)
25802584

Lib/test/test_argparse.py

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2172,6 +2172,53 @@ class TestNegativeNumber(ParserTestCase):
21722172
('--float -.5_000', NS(int=None, float=-0.5)),
21732173
]
21742174

2175+
class TestArgumentAndSubparserSuggestions(TestCase):
2176+
"""Test error handling and suggestion when a user makes a typo"""
2177+
2178+
def test_wrong_argument_error_with_suggestions(self):
2179+
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
2180+
parser.add_argument('foo', choices=['bar', 'baz'])
2181+
with self.assertRaises(ArgumentParserError) as excinfo:
2182+
parser.parse_args(('bazz',))
2183+
self.assertIn(
2184+
"maybe you meant 'baz'? (choose from 'bar', 'baz')",
2185+
excinfo.exception.stderr,
2186+
)
2187+
2188+
def test_wrong_argument_error_no_suggestions(self):
2189+
parser = ErrorRaisingArgumentParser(suggest_on_error=False)
2190+
parser.add_argument('foo', choices=['bar', 'baz'])
2191+
with self.assertRaises(ArgumentParserError) as excinfo:
2192+
parser.parse_args(('bazz',))
2193+
self.assertIn(
2194+
"invalid choice: 'bazz' (choose from 'bar', 'baz')",
2195+
excinfo.exception.stderr,
2196+
)
2197+
2198+
def test_wrong_argument_subparsers_with_suggestions(self):
2199+
parser = ErrorRaisingArgumentParser(suggest_on_error=True)
2200+
subparsers = parser.add_subparsers(required=True)
2201+
subparsers.add_parser('foo')
2202+
subparsers.add_parser('bar')
2203+
with self.assertRaises(ArgumentParserError) as excinfo:
2204+
parser.parse_args(('baz',))
2205+
self.assertIn(
2206+
"maybe you meant 'bar'? (choose from 'foo', 'bar')",
2207+
excinfo.exception.stderr,
2208+
)
2209+
2210+
def test_wrong_argument_subparsers_no_suggestions(self):
2211+
parser = ErrorRaisingArgumentParser(suggest_on_error=False)
2212+
subparsers = parser.add_subparsers(required=True)
2213+
subparsers.add_parser('foo')
2214+
subparsers.add_parser('bar')
2215+
with self.assertRaises(ArgumentParserError) as excinfo:
2216+
parser.parse_args(('baz',))
2217+
self.assertIn(
2218+
"invalid choice: 'baz' (choose from 'foo', 'bar')",
2219+
excinfo.exception.stderr,
2220+
)
2221+
21752222
class TestInvalidAction(TestCase):
21762223
"""Test invalid user defined Action"""
21772224

@@ -2390,18 +2437,6 @@ def test_required_subparsers_no_destination_error(self):
23902437
'error: the following arguments are required: {foo,bar}\n$'
23912438
)
23922439

2393-
def test_wrong_argument_subparsers_no_destination_error(self):
2394-
parser = ErrorRaisingArgumentParser()
2395-
subparsers = parser.add_subparsers(required=True)
2396-
subparsers.add_parser('foo')
2397-
subparsers.add_parser('bar')
2398-
with self.assertRaises(ArgumentParserError) as excinfo:
2399-
parser.parse_args(('baz',))
2400-
self.assertIn(
2401-
"error: argument {foo,bar}: invalid choice: 'baz', maybe you meant 'bar'? (choose from 'foo', 'bar')",
2402-
excinfo.exception.stderr,
2403-
)
2404-
24052440
def test_optional_subparsers(self):
24062441
parser = ErrorRaisingArgumentParser()
24072442
subparsers = parser.add_subparsers(dest='command', required=False)
@@ -2726,7 +2761,7 @@ def test_single_parent_mutex(self):
27262761
parser = ErrorRaisingArgumentParser(parents=[self.ab_mutex_parent])
27272762
self._test_mutex_ab(parser.parse_args)
27282763

2729-
def test_single_granparent_mutex(self):
2764+
def test_single_grandparent_mutex(self):
27302765
parents = [self.ab_mutex_parent]
27312766
parser = ErrorRaisingArgumentParser(add_help=False, parents=parents)
27322767
parser = ErrorRaisingArgumentParser(parents=[parser])

Misc/NEWS.d/next/Core and Builtins/2022-11-25-12-23-56.gh-issue-99749.-S_guS.rst

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)