Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-99749: Add optional feature to suggest correct names (ArgumentParser) #124456

Open
wants to merge 49 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
ef8e4fc
Add closet choice if exists in argparser if wrong choice picked
abdulrafey38 Nov 25, 2022
7e8dcf8
📜🤖 Added by blurb_it.
blurb-it[bot] Nov 25, 2022
8c754cd
Fix documentation
abdulrafey38 Nov 25, 2022
4fb8ce6
Added EOL :hammer:
abdulrafey38 Nov 25, 2022
2f197b3
Test cases updated for argparser
abdulrafey38 Nov 25, 2022
940a66e
Fixed typo errors :hammer:
abdulrafey38 Nov 25, 2022
badc5ed
Test case fix :hammer:
abdulrafey38 Nov 25, 2022
2588ef1
Fixed test cse error msg :hammer:
abdulrafey38 Nov 25, 2022
ee05c1e
Fixed error test case assert msg :hammer:
abdulrafey38 Nov 25, 2022
4a36406
assertion fix :hammer:
abdulrafey38 Nov 25, 2022
fe169bc
assertion test case fix
abdulrafey38 Nov 25, 2022
9fe0d95
Test Case fix
abdulrafey38 Nov 25, 2022
d12e1c4
Reveet to assertRegex from assertEqual
abdulrafey38 Nov 25, 2022
35f0961
Remove unused imports :hammer:
abdulrafey38 Nov 25, 2022
087895c
assertion msg fixed :hammer:
abdulrafey38 Nov 25, 2022
6eeae5c
test cases fixed :hammer:
abdulrafey38 Nov 25, 2022
9965694
assert fix
abdulrafey38 Nov 25, 2022
4ef218a
assertion fix
abdulrafey38 Nov 25, 2022
e63a01c
revert testing
abdulrafey38 Nov 25, 2022
bfc9262
test: hammer:
abdulrafey38 Nov 25, 2022
b3b4a9e
fixed
abdulrafey38 Nov 25, 2022
b8bc465
PR review changes :hammer:
abdulrafey38 Nov 26, 2022
ade070f
test case fix :hammer:
abdulrafey38 Nov 26, 2022
3753a0d
test case fixation assertIn
abdulrafey38 Nov 26, 2022
2b802ec
Merge branch 'main' into gh-99773
savannahostrowski Sep 24, 2024
f516bb4
Rework to add tests and make optional
savannahostrowski Sep 24, 2024
c9b40f5
Add to docs
savannahostrowski Sep 24, 2024
981bacd
Add implicit test case
savannahostrowski Sep 24, 2024
f9a6cc1
Appease linter
savannahostrowski Sep 24, 2024
6bdb131
📜🤖 Added by blurb_it.
blurb-it[bot] Sep 24, 2024
2c39866
Merge branch 'main' into gh-99773
savannahostrowski Sep 24, 2024
fb3769c
Undo import rename
savannahostrowski Sep 24, 2024
0eeb543
update docs
savannahostrowski Sep 24, 2024
972cc1e
update spacing
savannahostrowski Sep 24, 2024
22cdcf2
restore newline
savannahostrowski Sep 24, 2024
257c030
add section on suggest_on_error
savannahostrowski Sep 24, 2024
f97de33
fix indent
savannahostrowski Sep 24, 2024
fb5f041
Merge branch 'main' into gh-99773
savannahostrowski Sep 24, 2024
a55483e
Address PR comments
savannahostrowski Sep 26, 2024
1df84e4
add guard for types
savannahostrowski Sep 26, 2024
b5cb94b
remove comment
savannahostrowski Sep 26, 2024
4179a6f
Linting
savannahostrowski Sep 26, 2024
8e4793c
add pr comment
savannahostrowski Sep 26, 2024
b782fbc
merge main
savannahostrowski Oct 11, 2024
73c4bd3
fix merge conflict
savannahostrowski Oct 12, 2024
d2c665d
Merge branch 'main' into gh-99773
savannahostrowski Oct 12, 2024
d1daff1
Appease linter
savannahostrowski Oct 12, 2024
f57b4dc
Merge branch 'gh-99773' of https://github.com/savannahostrowski/cpyth…
savannahostrowski Oct 12, 2024
f2f67e7
Merge branch 'main' into gh-99773
serhiy-storchaka Oct 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion Doc/library/argparse.rst
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ ArgumentParser objects
formatter_class=argparse.HelpFormatter, \
prefix_chars='-', fromfile_prefix_chars=None, \
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
as keyword arguments. Each parameter has its own more detailed description
Expand Down Expand Up @@ -231,6 +232,10 @@ ArgumentParser objects
* exit_on_error_ - Determines whether or not ArgumentParser exits with
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
*allow_abbrev* parameter was added.

Expand Down Expand Up @@ -740,6 +745,27 @@ If the user would like to catch errors manually, the feature can be enabled by s

.. 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
-------------------------
Expand Down
24 changes: 21 additions & 3 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1716,6 +1716,8 @@ class ArgumentParser(_AttributeHolder, _ActionsContainer):
- allow_abbrev -- Allow long options to be abbreviated unambiguously
- exit_on_error -- Determines whether or not ArgumentParser exits with
error info when an error occurs
- suggest_on_error - Enables suggestions for mistyped argument choices
and subparser names. (default: ``False``)
"""

def __init__(self,
Expand All @@ -1731,7 +1733,8 @@ def __init__(self,
conflict_handler='error',
add_help=True,
allow_abbrev=True,
exit_on_error=True):
exit_on_error=True,
suggest_on_error=False):

superinit = super(ArgumentParser, self).__init__
superinit(description=description,
Expand All @@ -1751,6 +1754,7 @@ def __init__(self,
self.add_help = add_help
self.allow_abbrev = allow_abbrev
self.exit_on_error = exit_on_error
self.suggest_on_error = suggest_on_error

add_group = self.add_argument_group
self._positionals = add_group(_('positional arguments'))
Expand Down Expand Up @@ -2555,9 +2559,23 @@ def _get_value(self, action, arg_string):
def _check_value(self, action, value):
# converted value must be one of the choices (if specified)
if action.choices is not None and value not in action.choices:
args = {'value': value,
'choices': ', '.join(map(repr, action.choices))}
args = {
'value': value,
'choices': ', '.join(map(repr, action.choices)),
}
msg = _('invalid choice: %(value)r (choose from %(choices)s)')

all_strings = all(isinstance(choice, str) for choice in action.choices)

if self.suggest_on_error and isinstance(value, str) and all_strings:
serhiy-storchaka marked this conversation as resolved.
Show resolved Hide resolved
import difflib
suggestions = difflib.get_close_matches(value, action.choices, 1)
savannahostrowski marked this conversation as resolved.
Show resolved Hide resolved
if suggestions:
suggestions = suggestions[0]
args['closest'] = suggestions
msg = _('invalid choice: %(value)r, maybe you meant %(closest)r? '
'(choose from %(choices)s)')

raise ArgumentError(action, msg % args)

# =======================
Expand Down
102 changes: 89 additions & 13 deletions Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -2172,6 +2172,94 @@ class TestNegativeNumber(ParserTestCase):
('--float -.5_000', NS(int=None, float=-0.5)),
]

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(
"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(
"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(
"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(
"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(
"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(
"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(
"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(
"invalid choice: '3' (choose from 1, '2')",
excinfo.exception.stderr,
)


class TestInvalidAction(TestCase):
"""Test invalid user defined Action"""

Expand Down Expand Up @@ -2390,18 +2478,6 @@ def test_required_subparsers_no_destination_error(self):
'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):
parser = ErrorRaisingArgumentParser()
subparsers = parser.add_subparsers(dest='command', required=False)
Expand Down Expand Up @@ -2726,7 +2802,7 @@ def test_single_parent_mutex(self):
parser = ErrorRaisingArgumentParser(parents=[self.ab_mutex_parent])
self._test_mutex_ab(parser.parse_args)

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