From 909c2981c1b8ebc5e5be17ea9f0d1e80e72d421e Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sat, 10 May 2025 15:45:39 +0200 Subject: [PATCH 1/6] Pass choices through type before _check_value --- Lib/argparse.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index f13ac82dbc50b3..d0e3eb5bf37181 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2674,7 +2674,17 @@ def _check_value(self, action, value): if isinstance(choices, str): choices = iter(choices) - if value not in choices: + typed_choices = [] + if (self.convert_choices and + self.type and + isinstance(self.choices[0], str)): + try: + typed_choices = [acton.type[v] for v in choices] + except Exception: + # We use a blanket catch here, because type is user provided. + pass + + if value not in choices and value not in typed_choices: args = {'value': str(value), 'choices': ', '.join(map(str, action.choices))} msg = _('invalid choice: %(value)r (choose from %(choices)s)') From 626429d5c8db6aaa63264b3d6d2763ab7fe86ac4 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sun, 20 Apr 2025 10:43:11 +0200 Subject: [PATCH 2/6] Keep the original input during _check_value This will allow better error messages. --- Lib/argparse.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index d0e3eb5bf37181..79cdd270d77c43 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2617,7 +2617,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: @@ -2626,7 +2626,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: @@ -2635,8 +2635,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 @@ -2665,34 +2665,38 @@ 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) typed_choices = [] if (self.convert_choices and - self.type and - isinstance(self.choices[0], str)): + action.type and + all(isinstance(choice, str) for choice in choices) + ): try: - typed_choices = [acton.type[v] for v in choices] + typed_choices = [action.type(v) for v in choices] except Exception: # We use a blanket catch here, because type is user provided. pass if value not in choices and value not in typed_choices: - args = {'value': str(value), + 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? ' From 8e7b4fe97aa7fd93dcfc463c6f7d38a83955413f Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sun, 20 Apr 2025 10:44:03 +0200 Subject: [PATCH 3/6] Add tests --- Lib/test/test_argparse.py | 73 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 5a6be1180c1a3e..c9fe973b8cbaa5 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -1935,6 +1935,46 @@ def setUp(self): ('-x - -', NS(x=eq_bstdin, spam=eq_bstdin)), ] +class TestChoices(ParserTestCase): + """Test the original behavior""" + 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""" + + parser_signature = Sig(convert_choices=True) + argument_signatures = [ + Sig('when', + type=TestChoices.to_dow, choices=["mo", "tu", "we" , "th", "fr", "sa", "su"], + ) + ] + +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() @@ -5469,6 +5509,39 @@ 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', convert_choices=True) + argument_signatures = [ + Sig('when', + type=to_date, + choices=["today", "tomorrow"] + ), + ] + + 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 From b683ddcaad35ee47ffca8c51929e1f6c4f0c3880 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sun, 20 Apr 2025 12:52:30 +0200 Subject: [PATCH 4/6] Add blurb --- .../next/Library/2025-04-20-12-52-20.gh-issue-132558.MoFGmw.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-04-20-12-52-20.gh-issue-132558.MoFGmw.rst diff --git a/Misc/NEWS.d/next/Library/2025-04-20-12-52-20.gh-issue-132558.MoFGmw.rst b/Misc/NEWS.d/next/Library/2025-04-20-12-52-20.gh-issue-132558.MoFGmw.rst new file mode 100644 index 00000000000000..7311ddbb43d369 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-20-12-52-20.gh-issue-132558.MoFGmw.rst @@ -0,0 +1,2 @@ +:mod:`argparse` now allows `choices` to be entered as strings and calls +convert on the available choices during checking. From c9d3f997cc10e16835fda7aad85a980af17058c7 Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sun, 20 Apr 2025 12:59:40 +0200 Subject: [PATCH 5/6] After review comments --- Lib/argparse.py | 6 +----- Lib/test/test_argparse.py | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Lib/argparse.py b/Lib/argparse.py index 79cdd270d77c43..b158f71abcf8e7 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -2682,11 +2682,7 @@ def _check_value(self, action, value, arg_string=None): action.type and all(isinstance(choice, str) for choice in choices) ): - try: - typed_choices = [action.type(v) for v in choices] - except Exception: - # We use a blanket catch here, because type is user provided. - pass + typed_choices = [action.type(v) for v in choices] if value not in choices and value not in typed_choices: args = {'value': arg_string, diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index c9fe973b8cbaa5..1cf8369e7f1657 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -1936,7 +1936,7 @@ def setUp(self): ] class TestChoices(ParserTestCase): - """Test the original behavior""" + """Test integer choices without conversion.""" def to_dow(arg): days = ["mo", "tu", "we", "th", "fr", "sa", "su"] if arg in days: From b1be432d4b9ccdd772d4423bdf13e0ab4b7589fa Mon Sep 17 00:00:00 2001 From: Hans Then Date: Sun, 20 Apr 2025 15:42:02 +0200 Subject: [PATCH 6/6] Update documentation --- Doc/library/argparse.rst | 37 ++++++++++-- Doc/whatsnew/3.15.rst | 3 + Grammar/python.gram | 28 +++++----- Lib/argparse.py | 13 ++++- Lib/test/test_argparse.py | 14 +++-- Lib/test/test_codeop.py | 2 +- Lib/test/test_positional_only_arg.py | 12 ++-- Lib/test/test_pyrepl/test_interact.py | 2 +- Lib/test/test_repl.py | 2 +- Lib/test/test_syntax.py | 56 +++++++++---------- ...4-04-16-41-00.gh-issue-133379.asdjhjdf.rst | 1 + ...-04-20-12-52-20.gh-issue-132558.MoFGmw.rst | 5 +- Parser/parser.c | 28 +++++----- Python/symtable.c | 6 +- 14 files changed, 127 insertions(+), 82 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-04-04-16-41-00.gh-issue-133379.asdjhjdf.rst diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 29396c7a0366a1..6f2d7fd37bfaa9 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -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). @@ -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. @@ -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 diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d1e58c1b764eb9..6ce7f964020fb9 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -75,6 +75,9 @@ New features Other language changes ====================== +* Several error messages incorrectly using the term "argument" have been corrected. + (Contributed by Stan Ulbrych in :gh:`133382`.) + New modules diff --git a/Grammar/python.gram b/Grammar/python.gram index f3ef990923eec3..3a4db11038dc22 100644 --- a/Grammar/python.gram +++ b/Grammar/python.gram @@ -1305,7 +1305,7 @@ invalid_dict_comprehension: RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "dict unpacking cannot be used in dict comprehension") } invalid_parameters: | a="/" ',' { - RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "at least one argument must precede /") } + RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "at least one parameter must precede /") } | (slash_no_default | slash_with_default) param_maybe_default* a='/' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "/ may appear only once") } | slash_no_default? param_no_default* invalid_parameters_helper a=param_no_default { @@ -1319,21 +1319,21 @@ invalid_parameters: invalid_default: | a='=' &(')'|',') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "expected default value expression") } invalid_star_etc: - | a='*' (')' | ',' (')' | '**')) { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "named arguments must follow bare *") } + | a='*' (')' | ',' (')' | '**')) { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "named parameters must follow bare *") } | '*' ',' TYPE_COMMENT { RAISE_SYNTAX_ERROR("bare * has associated type comment") } - | '*' param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-positional argument cannot have default value") } + | '*' param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-positional parameter cannot have default value") } | '*' (param_no_default | ',') param_maybe_default* a='*' (param_no_default | ',') { - RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "* argument may appear only once") } + RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "* may appear only once") } invalid_kwds: - | '**' param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-keyword argument cannot have default value") } - | '**' param ',' a=param { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") } - | '**' param ',' a[Token*]=('*'|'**'|'/') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") } + | '**' param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-keyword parameter cannot have default value") } + | '**' param ',' a=param { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "parameters cannot follow var-keyword parameter") } + | '**' param ',' a[Token*]=('*'|'**'|'/') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "parameters cannot follow var-keyword parameter") } invalid_parameters_helper: # This is only there to avoid type errors | a=slash_with_default { _PyPegen_singleton_seq(p, a) } | param_with_default+ invalid_lambda_parameters: | a="/" ',' { - RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "at least one argument must precede /") } + RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "at least one parameter must precede /") } | (lambda_slash_no_default | lambda_slash_with_default) lambda_param_maybe_default* a='/' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "/ may appear only once") } | 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: | a=lambda_slash_with_default { _PyPegen_singleton_seq(p, a) } | lambda_param_with_default+ invalid_lambda_star_etc: - | '*' (':' | ',' (':' | '**')) { RAISE_SYNTAX_ERROR("named arguments must follow bare *") } - | '*' lambda_param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-positional argument cannot have default value") } + | '*' (':' | ',' (':' | '**')) { RAISE_SYNTAX_ERROR("named parameters must follow bare *") } + | '*' lambda_param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-positional parameter cannot have default value") } | '*' (lambda_param_no_default | ',') lambda_param_maybe_default* a='*' (lambda_param_no_default | ',') { - RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "* argument may appear only once") } + RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "* may appear only once") } invalid_lambda_kwds: - | '**' lambda_param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-keyword argument cannot have default value") } - | '**' lambda_param ',' a=lambda_param { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") } - | '**' lambda_param ',' a[Token*]=('*'|'**'|'/') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "arguments cannot follow var-keyword argument") } + | '**' lambda_param a='=' { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "var-keyword parameter cannot have default value") } + | '**' lambda_param ',' a=lambda_param { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "parameters cannot follow var-keyword parameter") } + | '**' lambda_param ',' a[Token*]=('*'|'**'|'/') { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(a, "parameters cannot follow var-keyword parameter") } invalid_double_type_comments: | TYPE_COMMENT NEWLINE TYPE_COMMENT NEWLINE INDENT { RAISE_SYNTAX_ERROR("Cannot have two type comments on def") } diff --git a/Lib/argparse.py b/Lib/argparse.py index b158f71abcf8e7..6980240eb0584f 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -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. @@ -893,6 +896,7 @@ def __init__(self, default=None, type=None, choices=None, + convert_choices=False, required=False, help=None, metavar=None, @@ -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 @@ -918,6 +923,7 @@ def _get_kwargs(self): 'default', 'type', 'choices', + 'convert_choices', 'required', 'help', 'metavar', @@ -980,6 +986,7 @@ def __init__(self, default=None, type=None, choices=None, + convert_choices=False, required=False, help=None, metavar=None, @@ -998,6 +1005,7 @@ def __init__(self, default=default, type=type, choices=choices, + convert_choices=convert_choices, required=required, help=help, metavar=metavar, @@ -2678,9 +2686,8 @@ def _check_value(self, action, value, arg_string=None): choices = iter(choices) typed_choices = [] - if (self.convert_choices and - action.type and - all(isinstance(choice, str) for choice in choices) + if (action.convert_choices and + action.type ): typed_choices = [action.type(v) for v in choices] diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index 1cf8369e7f1657..20b566a455d063 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -1958,10 +1958,11 @@ def to_dow(arg): class TestTypedChoices(TestChoices): """Test a set of string choices that convert to weekdays""" - parser_signature = Sig(convert_choices=True) argument_signatures = [ Sig('when', - type=TestChoices.to_dow, choices=["mo", "tu", "we" , "th", "fr", "sa", "su"], + type=TestChoices.to_dow, + choices=["mo", "tu", "we" , "th", "fr", "sa", "su"], + convert_choices=True, ) ] @@ -5519,11 +5520,12 @@ def to_date(arg): else: return None - parser_signature = Sig(prog='PROG', convert_choices=True) + parser_signature = Sig(prog='PROG') argument_signatures = [ Sig('when', type=to_date, - choices=["today", "tomorrow"] + choices=["today", "tomorrow"], + convert_choices=True ), ] @@ -5933,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) @@ -5951,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) diff --git a/Lib/test/test_codeop.py b/Lib/test/test_codeop.py index 0eefc22d11bce0..ed10bd3dcb6d2b 100644 --- a/Lib/test/test_codeop.py +++ b/Lib/test/test_codeop.py @@ -322,7 +322,7 @@ def test_syntax_errors(self): dedent("""\ def foo(x,x): pass - """), "duplicate argument 'x' in function definition") + """), "duplicate parameter 'x' in function definition") diff --git a/Lib/test/test_positional_only_arg.py b/Lib/test/test_positional_only_arg.py index eea0625012da6d..e412cb1d58d5db 100644 --- a/Lib/test/test_positional_only_arg.py +++ b/Lib/test/test_positional_only_arg.py @@ -37,8 +37,8 @@ def test_invalid_syntax_errors(self): check_syntax_error(self, "def f(/): pass") check_syntax_error(self, "def f(*, a, /): pass") check_syntax_error(self, "def f(*, /, a): pass") - check_syntax_error(self, "def f(a, /, a): pass", "duplicate argument 'a' in function definition") - check_syntax_error(self, "def f(a, /, *, a): pass", "duplicate argument 'a' in function definition") + check_syntax_error(self, "def f(a, /, a): pass", "duplicate parameter 'a' in function definition") + check_syntax_error(self, "def f(a, /, *, a): pass", "duplicate parameter 'a' in function definition") check_syntax_error(self, "def f(a, b/2, c): pass") check_syntax_error(self, "def f(a, /, c, /): pass") check_syntax_error(self, "def f(a, /, c, /, d): pass") @@ -59,8 +59,8 @@ def test_invalid_syntax_errors_async(self): check_syntax_error(self, "async def f(/): pass") check_syntax_error(self, "async def f(*, a, /): pass") check_syntax_error(self, "async def f(*, /, a): pass") - check_syntax_error(self, "async def f(a, /, a): pass", "duplicate argument 'a' in function definition") - check_syntax_error(self, "async def f(a, /, *, a): pass", "duplicate argument 'a' in function definition") + check_syntax_error(self, "async def f(a, /, a): pass", "duplicate parameter 'a' in function definition") + check_syntax_error(self, "async def f(a, /, *, a): pass", "duplicate parameter 'a' in function definition") check_syntax_error(self, "async def f(a, b/2, c): pass") check_syntax_error(self, "async def f(a, /, c, /): pass") check_syntax_error(self, "async def f(a, /, c, /, d): pass") @@ -247,8 +247,8 @@ def test_invalid_syntax_lambda(self): check_syntax_error(self, "lambda /: None") check_syntax_error(self, "lambda *, a, /: None") check_syntax_error(self, "lambda *, /, a: None") - check_syntax_error(self, "lambda a, /, a: None", "duplicate argument 'a' in function definition") - check_syntax_error(self, "lambda a, /, *, a: None", "duplicate argument 'a' in function definition") + check_syntax_error(self, "lambda a, /, a: None", "duplicate parameter 'a' in function definition") + check_syntax_error(self, "lambda a, /, *, a: None", "duplicate parameter 'a' in function definition") check_syntax_error(self, "lambda a, /, b, /: None") check_syntax_error(self, "lambda a, /, b, /, c: None") check_syntax_error(self, "lambda a, /, b, /, c, *, d: None") diff --git a/Lib/test/test_pyrepl/test_interact.py b/Lib/test/test_pyrepl/test_interact.py index a20719033fc9b7..8c0eeab6dcae96 100644 --- a/Lib/test/test_pyrepl/test_interact.py +++ b/Lib/test/test_pyrepl/test_interact.py @@ -113,7 +113,7 @@ def test_runsource_show_syntax_error_location(self): r = """ def f(x, x): ... ^ -SyntaxError: duplicate argument 'x' in function definition""" +SyntaxError: duplicate parameter 'x' in function definition""" self.assertIn(r, f.getvalue()) def test_runsource_shows_syntax_error_for_failed_compilation(self): diff --git a/Lib/test/test_repl.py b/Lib/test/test_repl.py index 27f16f1ba96566..f4a4634fc62f8a 100644 --- a/Lib/test/test_repl.py +++ b/Lib/test/test_repl.py @@ -197,7 +197,7 @@ def test_runsource_show_syntax_error_location(self): expected_lines = [ ' def f(x, x): ...', ' ^', - "SyntaxError: duplicate argument 'x' in function definition" + "SyntaxError: duplicate parameter 'x' in function definition" ] self.assertEqual(output.splitlines()[4:-1], expected_lines) diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 0ee17849e28121..0eccf03a1a96e3 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -419,7 +419,7 @@ >>> def foo(/,a,b=,c): ... pass Traceback (most recent call last): -SyntaxError: at least one argument must precede / +SyntaxError: at least one parameter must precede / >>> def foo(a,/,/,b,c): ... pass @@ -454,67 +454,67 @@ >>> def foo(a,*b=3,c): ... pass Traceback (most recent call last): -SyntaxError: var-positional argument cannot have default value +SyntaxError: var-positional parameter cannot have default value >>> def foo(a,*b: int=,c): ... pass Traceback (most recent call last): -SyntaxError: var-positional argument cannot have default value +SyntaxError: var-positional parameter cannot have default value >>> def foo(a,**b=3): ... pass Traceback (most recent call last): -SyntaxError: var-keyword argument cannot have default value +SyntaxError: var-keyword parameter cannot have default value >>> def foo(a,**b: int=3): ... pass Traceback (most recent call last): -SyntaxError: var-keyword argument cannot have default value +SyntaxError: var-keyword parameter cannot have default value >>> def foo(a,*a, b, **c, d): ... pass Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> def foo(a,*a, b, **c, d=4): ... pass Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> def foo(a,*a, b, **c, *d): ... pass Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> def foo(a,*a, b, **c, **d): ... pass Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> def foo(a=1,/,**b,/,c): ... pass Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> def foo(*b,*d): ... pass Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> def foo(a,*b,c,*d,*e,c): ... pass Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> def foo(a,b,/,c,*b,c,*d,*e,c): ... pass Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> def foo(a,b,/,c,*b,c,*d,**e): ... pass Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> def foo(a=1,/*,b,c): ... pass @@ -538,7 +538,7 @@ >>> lambda /,a,b,c: None Traceback (most recent call last): -SyntaxError: at least one argument must precede / +SyntaxError: at least one parameter must precede / >>> lambda a,/,/,b,c: None Traceback (most recent call last): @@ -570,47 +570,47 @@ >>> lambda a,*b=3,c: None Traceback (most recent call last): -SyntaxError: var-positional argument cannot have default value +SyntaxError: var-positional parameter cannot have default value >>> lambda a,**b=3: None Traceback (most recent call last): -SyntaxError: var-keyword argument cannot have default value +SyntaxError: var-keyword parameter cannot have default value >>> lambda a, *a, b, **c, d: None Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> lambda a,*a, b, **c, d=4: None Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> lambda a,*a, b, **c, *d: None Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> lambda a,*a, b, **c, **d: None Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> lambda a=1,/,**b,/,c: None Traceback (most recent call last): -SyntaxError: arguments cannot follow var-keyword argument +SyntaxError: parameters cannot follow var-keyword parameter >>> lambda *b,*d: None Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> lambda a,*b,c,*d,*e,c: None Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> lambda a,b,/,c,*b,c,*d,*e,c: None Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> lambda a,b,/,c,*b,c,*d,**e: None Traceback (most recent call last): -SyntaxError: * argument may appear only once +SyntaxError: * may appear only once >>> lambda a=1,d=,c: None Traceback (most recent call last): @@ -1304,7 +1304,7 @@ Traceback (most recent call last): SyntaxError: expected '(' -Parenthesized arguments in function definitions +Parenthesized parameters in function definitions >>> def f(x, (y, z), w): ... pass @@ -2178,7 +2178,7 @@ >>> with (lambda *:0): pass Traceback (most recent call last): - SyntaxError: named arguments must follow bare * + SyntaxError: named parameters must follow bare * Corner-cases that used to crash: diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-04-04-16-41-00.gh-issue-133379.asdjhjdf.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-04-16-41-00.gh-issue-133379.asdjhjdf.rst new file mode 100644 index 00000000000000..cf2e1e4eaff779 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-04-04-16-41-00.gh-issue-133379.asdjhjdf.rst @@ -0,0 +1 @@ +Correct usage of *arguments* in error messages. diff --git a/Misc/NEWS.d/next/Library/2025-04-20-12-52-20.gh-issue-132558.MoFGmw.rst b/Misc/NEWS.d/next/Library/2025-04-20-12-52-20.gh-issue-132558.MoFGmw.rst index 7311ddbb43d369..10bdbae0bd1f54 100644 --- a/Misc/NEWS.d/next/Library/2025-04-20-12-52-20.gh-issue-132558.MoFGmw.rst +++ b/Misc/NEWS.d/next/Library/2025-04-20-12-52-20.gh-issue-132558.MoFGmw.rst @@ -1,2 +1,3 @@ -:mod:`argparse` now allows `choices` to be entered as strings and calls -convert on the available choices during checking. +:mod:`argparse` Add option ``convert_choices`` to ``add_argument``. Combined with ``type``, allows ``choices`` +to be entered as strings and converted during argument checking. + diff --git a/Parser/parser.c b/Parser/parser.c index 509fac7df6e371..b48b1b20ee4d8e 100644 --- a/Parser/parser.c +++ b/Parser/parser.c @@ -22190,7 +22190,7 @@ invalid_parameters_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_parameters[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "\"/\" ','")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "at least one argument must precede /" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "at least one parameter must precede /" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -22456,7 +22456,7 @@ invalid_star_etc_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_star_etc[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'*' (')' | ',' (')' | '**'))")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "named arguments must follow bare *" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "named parameters must follow bare *" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -22516,7 +22516,7 @@ invalid_star_etc_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_star_etc[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'*' param '='")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "var-positional argument cannot have default value" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "var-positional parameter cannot have default value" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -22552,7 +22552,7 @@ invalid_star_etc_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_star_etc[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'*' (param_no_default | ',') param_maybe_default* '*' (param_no_default | ',')")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "* argument may appear only once" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "* may appear only once" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -22601,7 +22601,7 @@ invalid_kwds_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_kwds[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'**' param '='")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "var-keyword argument cannot have default value" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "var-keyword parameter cannot have default value" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -22634,7 +22634,7 @@ invalid_kwds_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_kwds[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'**' param ',' param")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "arguments cannot follow var-keyword argument" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "parameters cannot follow var-keyword parameter" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -22667,7 +22667,7 @@ invalid_kwds_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_kwds[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'**' param ',' ('*' | '**' | '/')")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "arguments cannot follow var-keyword argument" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "parameters cannot follow var-keyword parameter" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -22781,7 +22781,7 @@ invalid_lambda_parameters_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_lambda_parameters[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "\"/\" ','")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "at least one argument must precede /" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "at least one parameter must precede /" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -23065,7 +23065,7 @@ invalid_lambda_star_etc_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_lambda_star_etc[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'*' (':' | ',' (':' | '**'))")); - _res = RAISE_SYNTAX_ERROR ( "named arguments must follow bare *" ); + _res = RAISE_SYNTAX_ERROR ( "named parameters must follow bare *" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -23095,7 +23095,7 @@ invalid_lambda_star_etc_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_lambda_star_etc[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'*' lambda_param '='")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "var-positional argument cannot have default value" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "var-positional parameter cannot have default value" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -23131,7 +23131,7 @@ invalid_lambda_star_etc_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_lambda_star_etc[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'*' (lambda_param_no_default | ',') lambda_param_maybe_default* '*' (lambda_param_no_default | ',')")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "* argument may appear only once" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "* may appear only once" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -23183,7 +23183,7 @@ invalid_lambda_kwds_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_lambda_kwds[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'**' lambda_param '='")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "var-keyword argument cannot have default value" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "var-keyword parameter cannot have default value" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -23216,7 +23216,7 @@ invalid_lambda_kwds_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_lambda_kwds[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'**' lambda_param ',' lambda_param")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "arguments cannot follow var-keyword argument" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "parameters cannot follow var-keyword parameter" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; @@ -23249,7 +23249,7 @@ invalid_lambda_kwds_rule(Parser *p) ) { D(fprintf(stderr, "%*c+ invalid_lambda_kwds[%d-%d]: %s succeeded!\n", p->level, ' ', _mark, p->mark, "'**' lambda_param ',' ('*' | '**' | '/')")); - _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "arguments cannot follow var-keyword argument" ); + _res = RAISE_SYNTAX_ERROR_KNOWN_LOCATION ( a , "parameters cannot follow var-keyword parameter" ); if (_res == NULL && PyErr_Occurred()) { p->error_indicator = 1; p->level--; diff --git a/Python/symtable.c b/Python/symtable.c index f633e281019720..a3d0fff80d24a1 100644 --- a/Python/symtable.c +++ b/Python/symtable.c @@ -380,8 +380,8 @@ static void dump_symtable(PySTEntryObject* ste) } #endif -#define DUPLICATE_ARGUMENT \ -"duplicate argument '%U' in function definition" +#define DUPLICATE_PARAMETER \ +"duplicate parameter '%U' in function definition" static struct symtable * symtable_new(void) @@ -1494,7 +1494,7 @@ symtable_add_def_helper(struct symtable *st, PyObject *name, int flag, struct _s } if ((flag & DEF_PARAM) && (val & DEF_PARAM)) { /* Is it better to use 'mangled' or 'name' here? */ - PyErr_Format(PyExc_SyntaxError, DUPLICATE_ARGUMENT, name); + PyErr_Format(PyExc_SyntaxError, DUPLICATE_PARAMETER, name); SET_ERROR_LOCATION(st->st_filename, loc); goto error; }