8000 gh-132558: add `convert_choices` parameter to `add_argument` by hansthen · Pull Request #133826 · python/cpython · GitHub
[go: up one dir, main page]

Skip to content

gh-132558: add convert_choices parameter to add_argument #133826

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
37 changes: 33 additions & 4 deletions Doc/library/argparse.rst
8000
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,9 @@ The add_argument() method

* choices_ - A sequence of the allowable values for the argument.

* convert_choices_ - Whether to convert the choices_ using the type_ callable
before checking.

* required_ - Whether or not the command-line option may be omitted
(optionals only).

Expand Down Expand Up @@ -1156,10 +1159,6 @@ if the argument was not one of the acceptable values::
game.py: error: argument move: invalid choice: 'fire' (choose from 'rock',
'paper', 'scissors')

Note that inclusion in the *choices* sequence is checked after any type_
conversions have been performed, so the type of the objects in the *choices*
sequence should match the type_ specified.

Any sequence can be passed as the *choices* value, so :class:`list` objects,
:class:`tuple` objects, and custom sequences are all supported.

Expand All @@ -1172,6 +1171,36 @@ from *dest*. This is usually what you want because the user never sees the
many choices), just specify an explicit metavar_.


.. _convert_choices:

convert_choices
^^^^^^^^^^^^^^^

By default, when a user passes both a ``type`` and a ``choices`` argument, the
``choices`` need to be specified in the target type, after conversion.
This can cause confusing ``usage`` and ``help`` strings.
To specify ``choices`` before conversion, set the flag ``convert_choices``::

>>> def to_dow(s):
... return ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su'].index(x)
...
>>> parser = argparse.ArgumentParser()
>>> parser.add_argument('when',
... choices=['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su'],
... convert_choices=True,
... type=to_dow)
>>> parser.print_help()
usage: sphinx-build [-h] {mo,tu,we,th,fr,sa,su}

positional arguments:
{mo,tu,we,th,fr,sa,su}

options:
-h, --help show this help message and exit

.. versionadded:: next


.. _required:

required
Expand Down
35 changes: 26 additions & 9 deletions Lib/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,9 @@ class Action(_AttributeHolder):
type, an exception will be raised if it is not a member of this
collection.

- convert_choices - Runs the ``choices`` through the ``type`` callable
during checking. (default: ``False``)

- required -- True if the action must always be specified at the
command line. This is only meaningful for optional command-line
arguments.
Expand All @@ -893,6 +896,7 @@ def __init__(self,
default=None,
type=None,
choices=None,
convert_choices=False,
required=False,
help=None,
metavar=None,
Expand All @@ -904,6 +908,7 @@ def __init__(self,
self.default = default
self.type = type
self.choices = choices
self.convert_choices = convert_choices
self.required = required
self.help = help
self.metavar = metavar
Expand All @@ -918,6 +923,7 @@ def _get_kwargs(self):
'default',
'type',
'choices',
'convert_choices',
'required',
'help',
'metavar',
Expand Down Expand Up @@ -980,6 +986,7 @@ def __init__(self,
default=None,
type=None,
choices=None,
convert_choices=False,
required=False,
help=None,
metavar=None,
Expand All @@ -998,6 +1005,7 @@ def __init__(self,
default=default,
type=type,
choices=choices,
convert_choices=convert_choices,
required=required,
help=help,
metavar=metavar,
Expand Down Expand Up @@ -2617,7 +2625,7 @@ def _get_values(self, action, arg_strings):
elif len(arg_strings) == 1 and action.nargs in [None, OPTIONAL]:
arg_string, = arg_strings
value = self._get_value(action, arg_string)
self._check_value(action, value)
self._check_value(action, value, arg_string)

# REMAINDER arguments convert all values, checking none
elif action.nargs == REMAINDER:
Expand All @@ -2626,7 +2634,7 @@ def _get_values(self, action, arg_strings):
# PARSER arguments convert all values, but check only the first
elif action.nargs == PARSER:
value = [self._get_value(action, v) for v in arg_strings]
self._check_value(action, value[0])
self._check_value(action, value[0], arg_strings[0])

# SUPPRESS argument does not put anything in the namespace
elif action.nargs == SUPPRESS:
Expand All @@ -2635,8 +2643,8 @@ def _get_values(self, action, arg_strings):
# all other types of nargs produce a list
else:
value = [self._get_value(action, v) for v in arg_strings]
for v in value:
self._check_value(action, v)
for v, s in zip(value, arg_strings):
self._check_value(action, v, s)

# return the converted value
return value
Expand Down Expand Up @@ -2665,24 +2673,33 @@ def _get_value(self, action, arg_string):
# return the converted value
return result

def _check_value(self, action, value):
def _check_value(self, action, value, arg_string=None):
# converted value must be one of the choices (if specified)
choices = action.choices
if choices is None:
return

if arg_string is None:
arg_string = value

if isinstance(choices, str):
choices = iter(choices)

if value not in choices:
args = {'value': str(value),
typed_choices = []
if (action.convert_choices and
action.type
):
typed_choices = [action.type(v) for v in choices]

if value not in choices and value not in typed_choices:
args = {'value': arg_string,
'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 self.suggest_on_error:
if all(isinstance(choice, str) for choice in action.choices):
import difflib
suggestions = difflib.get_close_matches(value, action.choices, 1)
suggestions = difflib.get_close_matches(arg_string, action.choices, 1)
if suggestions:
args['closest'] = suggestions[0]
msg = _('invalid choice: %(value)r, maybe you meant %(closest)r? '
Expand Down
79 changes: 78 additions & 1 deletion Lib/test/test_argparse.py
Original file line number Diff line number Diff line change
67E6 Expand Up @@ -1935,6 +1935,47 @@ def setUp(self):
('-x - -', NS(x=eq_bstdin, spam=eq_bstdin)),
]

class TestChoices(ParserTestCase):
"""Test integer choices without conversion."""
def to_dow(arg):
days = ["mo", "tu", "we", "th", "fr", "sa", "su"]
if arg in days:
return days.index(arg) + 1
else:
return None

argument_signatures = [
Sig('when',
type=to_dow, choices=[1, 2, 3, 4, 5, 6, 7],
)
]
failures = ['now', '1']
successes = [
('mo', NS(when=1)),
('su', NS(when=7)),
]

class TestTypedChoices(TestChoices):
"""Test a set of string choices that convert to weekdays"""

argument_signatures = [
Sig('when',
type=TestChoices.to_dow,
choices=["mo", "tu", "we" , "th", "fr", "sa", "su"],
convert_choices=True,
)
]

class TestTypedChoicesNoFlag(TestChoices):
"""Without the feature flag we fail"""
argument_signatures = [
Sig('when',
type=TestChoices.to_dow, choices=["mo", "tu", "we" , "th", "fr", "sa", "su"],
)
]
failures = ['mo']
successes = []


class WFile(object):
seen = set()
Expand Down Expand Up @@ -5469,6 +5510,40 @@ def custom_type(string):
version = ''


class TestHelpTypedChoices(HelpTestCase):
from datetime import date, timedelta
def to_date(arg):
if arg == "today":
return date.today()
elif arg == "tomorrow":
return date.today() + timedelta(days=1).date()
else:
return None

parser_signature = Sig(prog='PROG')
argument_signatures = [
Sig('when',
type=to_date,
choices=["today", "tomorrow"],
convert_choices=True
),
]

usage = '''\
usage: PROG [-h] {today,tomorrow}
'''
help = usage + '''\

positional arguments:
{today,tomorrow}

options:
-h, --help show this help message and exit
'''
version = ''



class TestHelpUsageLongSubparserCommand(TestCase):
"""Test that subparser commands are formatted correctly in help"""
maxDiff = None
Expand Down Expand Up @@ -5860,7 +5935,8 @@ def test_optional(self):
string = (
"Action(option_strings=['--foo', '-a', '-b'], dest='b', "
"nargs='+', const=None, default=42, type='int', "
"choices=[1, 2, 3], required=False, help='HELP', "
"choices=[1, 2, 3], convert_choices=False, "
"required=False, help='HELP', "
"metavar='METAVAR', deprecated=False)")
self.assertStringEqual(option, string)

Expand All @@ -5878,6 +5954,7 @@ def test_argument(self):
string = (
"Action(option_strings=[], dest='x', nargs='?', "
"const=None, default=2.5, type=%r, choices=[0.5, 1.5, 2.5], "
"convert_choices=False, "
"required=True, help='H HH H', metavar='MV MV MV', "
"deprecated=False)" % float)
self.assertStringEqual(argument, string)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:mod:`argparse` Add option ``convert_choices`` to ``add_argument``. Combined with ``type``, allows ``choices``
to be entered as strings and converted during argument checking.

Loading
0