8000 Add `convert_choices` to `add_argument` · python/cpython@afaa8ab · GitHub
[go: up one dir, main page]

Skip to content

Commit afaa8ab

Browse files
committed
Add convert_choices to add_argument
This allows users to specify choices in the same vocabulary as the user will enter them.
1 parent 6604962 commit afaa8ab

File tree

14 files changed

+125
-124
lines changed

14 files changed

+125
-124
lines changed

Doc/library/argparse.rst

Lines changed: 33 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,7 @@ ArgumentParser objects
119119
* suggest_on_error_ - Enables suggestions for mistyped argument choices
120120
and subparser names (default: ``False``)
121121

122-
<<<<<<< HEAD
123122
* color_ - Allow color output (default: ``False``)
124-
=======
125-
* convert_choices_ - Runs the ``choices`` through the ``type`` callable
126-
during checking (default: ``False``)
127-
128-
>>>>>>> ad9b851d8f (Update documentation)
129123

130124
.. versionchanged:: 3.5
131125
*allow_abbrev* parameter was added.
@@ -623,7 +617,6 @@ keyword argument::
623617
.. versionadded:: 3.14
624618

625619

626-
<<<<<<< HEAD
627620
color
628621
^^^^^
629622

@@ -649,39 +642,6 @@ keyword argument::
649642
>>> parser.color = True
650643

651644
.. versionadded:: 3.14
652-
=======
653-
convert_choices
654-
^^^^^^^^^^^^^^^
655-
656-
By default, when a user passes both a ``type`` and a ``choices`` argument, the
657-
``choices`` need to be specified in the target type, after conversion.
658-
This can cause confusing ``usage`` and ``help`` strings. If the user would like
659-
to specify ``choices`` in the same vocabulary as the end-user would enter them,
660-
this feature can be enabled by setting ``convert_choices`` to ``True``::
661-
662-
>>> parser = argparse.ArgumentParser(convert_choices=True)
663-
>>> parser.add_argument('when',
664-
... choices=['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su'],
665-
... type=to_dow)
666-
>>> parser.print_help()
667-
usage: example_broken.py [-h] [--days {mo,tu,we,th,fr,sa,su}]
668-
669-
options:
670-
-h, --help show this help message and exit
671-
--days {mo,tu,we,th,fr,sa,su}
672-
673-
674-
If you're writing code that needs to be compatible with older Python versions
675-
and want to opportunistically use ``convert_choices`` when it's available, you
676-
can set it as an attribute after initializing the parser instead of using the
677-
keyword argument::
678-
679-
>>> parser = argparse.ArgumentParser()
680-
>>> parser.convert_choices = True
681-
682-
.. versionadded:: next
683-
>>>>>>> ad9b851d8f (Update documentation)
684-
685645

686646
The add_argument() method
687647
-------------------------
@@ -710,6 +670,9 @@ The add_argument() method
710670

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

673+
* convert_choices_ - Whether to convert the choices_ using the type_ callable
674+
before checking.
675+
713676
* required_ - Whether or not the command-line option may be omitted
714677
(optionals only).
715678

@@ -1195,13 +1158,6 @@ if the argument was not one of the acceptable values::
11951158
game.py: error: argument move: invalid choice: 'fire' (choose from 'rock',
11961159
'paper', 'scissors')
11971160

1198-
Note that, by default, inclusion in the *choices* sequence is checked after
1199-
any type_ conversions have been performed, so the type of the objects in the
1200-
*choices* sequence should match the type_ specified. This can lead to
1201-
confusing ``usage`` messages. If you want to convert *choices* using type_
1202-
before checking, set the ``convert_choices`` flag on :class:`~ArgumentParser`.
1203-
1204-
12051161
Any sequence can be passed as the *choices* value, so :class:`list` objects,
12061162
:class:`tuple` objects, and custom sequences are all supported.
12071163

@@ -1214,6 +1170,36 @@ from *dest*. This is usually what you want because the user never sees the
12141170
many choices), just specify an explicit metavar_.
12151171

12161172

1173+
.. _convert_choices:
1174+
1175+
convert_choices
1176+
^^^^^^^^^^^^^^^
1177+
1178+
By default, when a user passes both a ``type`` and a ``choices`` argument, the
1179+
``choices`` need to be specified in the target type, after conversion.
1180+
This can cause confusing ``usage`` and ``help`` strings.
1181+
To specify ``choices`` before conversion, set the flag ``convert_choices``::
1182+
1183+
>>> def to_dow(s):
1184+
... return ['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su'].index(x)
1185+
...
1186+
>>> parser = argparse.ArgumentParser()
1187+
>>> parser.add_argument('when',
1188+
... choices=['mo', 'tu', 'we', 'th', 'fr', 'sa', 'su'],
1189+
... convert_choices=True,
1190+
... type=to_dow)
1191+
>>> parser.print_help()
1192+
usage: sphinx-build [-h] {mo,tu,we,th,fr,sa,su}
1193+
1194+
positional arguments:
1195+
{mo,tu,we,th,fr,sa,su}
1196+
1197+
options:
1198+
-h, --help show this help message and exit
1199+
1200+
.. versionadded:: next
1201+
1202+
12171203
.. _required:
12181204

12191205
required

Doc/whatsnew/3.15.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ New features
7575
Other language changes
7676
======================
7777

78+
* Several error messages incorrectly using the term "argument" have been corrected.
79+
(Contributed by Stan Ulbrych in :gh:`133382`.)
80+
7881

7982

8083
New modules

Grammar/python.gram

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1305,7 +1305,7 @@ invalid_dict_comprehension:
13051305
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "dict unpacking cannot be used in dict comprehension") }
13061306
invalid_parameters:
13071307
| a="/" ',' {
1308-
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "at least one argument must precede /") }
1308+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "at least one parameter must precede /") }
13091309
| (slash_no_default | slash_with_default) param_maybe_default* a='/' {
13101310
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "/ may appear only once") }
13111311
| slash_no_default? param_no_default* invalid_parameters_helper a=param_no_default {
@@ -1319,21 +1319,21 @@ invalid_parameters:
13191319
invalid_default:
13201320
| a='=' &(')'|',') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "expected default value expression") }
13211321
invalid_star_etc:
1322-
| a='*' (')' | ',' (')' | '**')) { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "named arguments must follow bare *") }
1322+
| a='*' (')' | ',' (')' | '**')) { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "named parameters must follow bare *") }
13231323
| '*' ',' TYPE_COMMENT { RAISE_SYNTAX_ERROR("bare * has associated type comment") }
1324-
| '*' param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-positional argument cannot have default value") }
1324+
| '*' param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-positional parameter cannot have default value") }
13251325
| '*' (param_no_default | ',') param_maybe_default* a='*' (param_no_default | ',') {
1326-
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "* argument may appear only once") }
1326+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "* may appear only once") }
13271327
invalid_kwds:
1328-
| '**' param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-keyword argument cannot have default value") }
1329-
| '**' param ',' a=param { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") }
1330-
| '**' param ',' a[Token*]=('*'|'**'|'/') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") }
1328+
| '**' param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-keyword parameter cannot have default value") }
1329+
| '**' param ',' a=param { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "parameters cannot follow var-keyword parameter") }
1330+
| '**' param ',' a[Token*]=('*'|'**'|'/') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "parameters cannot follow var-keyword parameter") }
13311331
invalid_parameters_helper: # This is only there to avoid type errors
13321332
| a=slash_with_default { _PyPegen_singleton_seq(p, a) }
13331333
| param_with_default+
13341334
invalid_lambda_parameters:
13351335
| a="/" ',' {
1336-
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "at least one argument must precede /") }
1336+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "at least one parameter must precede /") }
13371337
| (lambda_slash_no_default | lambda_slash_with_default) lambda_param_maybe_default* a='/' {
13381338
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "/ may appear only once") }
13391339
| lambda_slash_no_default? lambda_param_no_default* invalid_lambda_parameters_helper a=lambda_param_no_default {
@@ -1348,14 +1348,14 @@ invalid_lambda_parameters_helper:
13481348
| a=lambda_slash_with_default { _PyPegen_singleton_seq(p, a) }
13491349
| lambda_param_with_default+
13501350
invalid_lambda_star_etc:
1351-
| '*' (':' | ',' (':' | '**')) { RAISE_SYNTAX_ERROR("named arguments must follow bare *") }
1352-
| '*' lambda_param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-positional argument cannot have default value") }
1351+
| '*' (':' | ',' (':' | '**')) { RAISE_SYNTAX_ERROR("named parameters must follow bare *") }
1352+
| '*' lambda_param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-positional parameter cannot have default value") }
13531353
| '*' (lambda_param_no_default | ',') lambda_param_maybe_default* a='*' (lambda_param_no_default | ',') {
1354-
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "* argument may appear only once") }
1354+
RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "* may appear only once") }
13551355
invalid_lambda_kwds:
1356-
| '**' lambda_param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-keyword argument cannot have default value") }
1357-
| '**' lambda_param ',' a=lambda_param { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") }
1358-
| '**' lambda_param ',' a[Token*]=('*'|'**'|'/') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") }
1356+
| '**' lambda_param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-keyword parameter cannot have default value") }
1357+
| '**' lambda_param ',' a=lambda_param { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "parameters cannot follow var-keyword parameter") }
1358+
| '**' lambda_param ',' a[Token*]=('*'|'**'|'/') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "parameters cannot follow var-keyword parameter") }
13591359
invalid_double_type_comments:
13601360
| TYPE_COMMENT NEWLINE TYPE_COMMENT NEWLINE INDENT {
13611361
RAISE_SYNTAX_ERROR("Cannot have two type comments on def") }

Lib/argparse.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -875,6 +875,9 @@ class Action(_AttributeHolder):
875875
type, an exception will be raised if it is not a member of this
876876
collection.
877877
878+
- convert_choices - Runs the ``choices`` through the ``type`` callable
879+
during checking. (default: ``False``)
880+
878881
- required -- True if the action must always be specified at the
879882
command line. This is only meaningful for optional command-line
880883
arguments.
@@ -893,6 +896,7 @@ def __init__(self,
893896
default=None,
894897
type=None,
895898
choices=None,
899+
convert_choices=False,
896900
required=False,
897901
help=None,
898902
metavar=None,
@@ -904,6 +908,7 @@ def __init__(self,
904908
self.default = default
905909
self.type = type
906910
self.choices = choices
911+
self.convert_choices = convert_choices
907912
self.required = required
908913
self.help = help
909914
self.metavar = metavar
@@ -918,6 +923,7 @@ def _get_kwargs(self):
918923
'default',
919924
'type',
920925
'choices',
926+
'convert_choices',
921927
'required',
922928
'help',
923929
'metavar',
@@ -980,6 +986,7 @@ def __init__(self,
980986
default=None,
981987
type=None,
982988
choices=None,
989+
convert_choices=False,
983990
required=False,
984991
help=None,
985992
metavar=None,
@@ -998,6 +1005,7 @@ def __init__(self,
9981005
default=default,
9991006
type=type,
10001007
choices=choices,
1008+
convert_choices=convert_choices,
10011009
required=required,
10021010
help=help,
10031011
metavar=metavar,
@@ -2678,9 +2686,8 @@ def _check_value(self, action, value, arg_string=None):
26782686
choices = iter(choices)
26792687

26802688
typed_choices = []
2681-
if (self.convert_choices and
2682-
action.type and
2683-
all(isinstance(choice, str) for choice in choices)
2689+
if (action.convert_choices and
2690+
action.type
26842691
):
26852692
typed_choices = [action.type(v) for v in choices]
26862693

Lib/test/test_argparse.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1958,10 +1958,11 @@ def to_dow(arg):
19581958
class TestTypedChoices(TestChoices):
19591959
"""Test a set of string choices that convert to weekdays"""
19601960

1961-
parser_signature = Sig(convert_choices=True)
19621961
argument_signatures = [
19631962
Sig('when',
1964-
type=TestChoices.to_dow, choices=["mo", "tu", "we" , "th", "fr", "sa", "su"],
1963+
type=TestChoices.to_dow,
1964+
choices=["mo", "tu", "we" , "th", "fr", "sa", "su"],
1965+
convert_choices=True,
19651966
)
19661967
]
19671968

@@ -5519,11 +5520,12 @@ def to_date(arg):
55195520
else:
55205521
return None
55215522

5522-
parser_signature = Sig(prog='PROG', convert_choices=True)
5523+
parser_signature = Sig(prog='PROG')
55235524
argument_signatures = [
55245525
Sig('when',
55255526
type=to_date,
5526-
choices=["today", "tomorrow"]
5527+
choices=["today", "tomorrow"],
5528+
convert_choices=True
55275529
),
55285530
]
55295531

@@ -5933,7 +5935,8 @@ def test_optional(self):
59335935
string = (
59345936
"Action(option_strings=['--foo', '-a', '-b'], dest='b', "
59355937
"nargs='+', const=None, default=42, type='int', "
5936-
"choices=[1, 2, 3], required=False, help='HELP', "
5938+
"choices=[1, 2, 3], convert_choices=False, "
5939+
"required=False, help='HELP', "
59375940
"metavar='METAVAR', deprecated=False)")
59385941
self.assertStringEqual(option, string)
59395942

@@ -5951,6 +5954,7 @@ def test_argument(self):
59515954
string = (
59525955
"Action(option_strings=[], dest='x', nargs='?', "
59535956
"const=None, default=2.5, type=%r, choices=[0.5, 1.5, 2.5], "
5957+
"convert_choices=False, "
59545958
"required=True, help='H HH H', metavar='MV MV MV', "
59555959
"deprecated=False)" % float)
59565960
self.assertStringEqual(argument, string)

Lib/test/test_codeop.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ def test_syntax_errors(self):
322322
dedent("""\
323323
def foo(x,x):
324324
pass
325-
"""), "duplicate argument 'x' in function definition")
325+
"""), "duplicate parameter 'x' in function definition")
326326

327327

328328

Lib/test/test_positional_only_arg.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ def test_invalid_syntax_errors(self):
3737
check_syntax_error(self, "def f(/): pass")
3838
check_syntax_error(self, "def f(*, a, /): pass")
3939
check_syntax_error(self, "def f(*, /, a): pass")
40-
check_syntax_error(self, "def f(a, /, a): pass", "duplicate argument 'a' in function definition")
41-
check_syntax_error(self, "def f(a, /, *, a): pass", "duplicate argument 'a' in function definition")
40+
check_syntax_error(self, "def f(a, /, a): pass", "duplicate parameter 'a' in function definition")
41+
check_syntax_error(self, "def f(a, /, *, a): pass", "duplicate parameter 'a' in function definition")
4242
check_syntax_error(self, "def f(a, b/2, c): pass")
4343
check_syntax_error(self, "def f(a, /, c, /): pass")
4444
check_syntax_error(self, "def f(a, /, c, /, d): pass")
@@ -59,8 +59,8 @@ def test_invalid_syntax_errors_async(self):
5959
check_syntax_error(self, "async def f(/): pass")
6060
check_syntax_error(self, "async def f(*, a, /): pass")
6161
check_syntax_error(self, "async def f(*, /, a): pass")
62-
check_syntax_error(self, "async def f(a, /, a): pass", "duplicate argument 'a' in function definition")
63-
check_syntax_error(self, "async def f(a, /, *, a): pass", "duplicate argument 'a' in function definition")
62+
check_syntax_error(self, "async def f(a, /, a): pass", "duplicate parameter 'a' in function definition")
63+
check_syntax_error(self, "async def f(a, /, *, a): pass", "duplicate parameter 'a' in function definition")
6464
check_syntax_error(self, "async def f(a, b/2, c): pass")
6565
check_syntax_error(self, "async def f(a, /, c, /): pass")
6666
check_syntax_error(self, "async def f(a, /, c, /, d): pass")
@@ -247,8 +247,8 @@ def test_invalid_syntax_lambda(self):
247247
check_syntax_error(self, "lambda /: None")
248248
check_syntax_error(self, "lambda *, a, /: None")
249249
check_syntax_error(self, "lambda *, /, a: None")
250-
check_syntax_error(self, "lambda a, /, a: None", "duplicate argument 'a' in function definition")
251-
check_syntax_error(self, "lambda a, /, *, a: None", "duplicate argument 'a' in function definition")
250+
check_syntax_error(self, "lambda a, /, a: None", "duplicate parameter 'a' in function definition")
251+
check_syntax_error(self, "lambda a, /, *, a: None", "duplicate parameter 'a' in function definition")
252252
check_syntax_error(self, "lambda a, /, b, /: None")
253253
check_syntax_error(self, "lambda a, /, b, /, c: None")
254254
check_syntax_error(self, "lambda a, /, b, /, c, *, d: None")

Lib/test/test_pyrepl/test_interact.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def test_runsource_show_syntax_error_location(self):
113113
r = """
114114
def f(x, x): ...
115115
^
116-
SyntaxError: duplicate argument 'x' in function definition"""
116+
SyntaxError: duplicate parameter 'x' in function definition"""
117117
self.assertIn(r, f.getvalue())
118118

119119
def test_runsource_shows_syntax_error_for_failed_compilation(self):

Lib/test/test_repl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def test_runsource_show_syntax_error_location(self):
197197
expected_lines = [
198198
' def f(x, x): ...',
199199
' ^',
200-
"SyntaxError: duplicate argument 'x' in function definition"
200+
"SyntaxError: duplicate parameter 'x' in function definition"
201201
]
202202
self.assertEqual(output.splitlines()[4:-1], expected_lines)
203203

0 commit comments

Comments
 (0)
0