gh-95065, gh-107704: Argument Clinic: support multiple '/ [from ...]' and '* [from ...]' markers (GH-108132)

This commit is contained in:
Serhiy Storchaka 2023-08-21 16:59:58 +03:00 committed by GitHub
parent 13104f3b74
commit 60942cccb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 565 additions and 104 deletions

View file

@ -35,6 +35,7 @@ from collections.abc import (
Iterator,
Sequence,
)
from operator import attrgetter
from types import FunctionType, NoneType
from typing import (
TYPE_CHECKING,
@ -922,41 +923,38 @@ class CLanguage(Language):
params: dict[int, Parameter],
) -> str:
assert len(params) > 0
names = [repr(p.name) for p in params.values()]
first_pos, first_param = next(iter(params.items()))
last_pos, last_param = next(reversed(params.items()))
# Pretty-print list of names.
pstr = pprint_words(names)
# For now, assume there's only one deprecation level.
assert first_param.deprecated_positional == last_param.deprecated_positional
thenceforth = first_param.deprecated_positional
assert thenceforth is not None
major, minor = thenceforth
first_pos = next(iter(params))
last_pos = next(reversed(params))
# Format the deprecation message.
if first_pos == 0:
preamble = "Passing positional arguments to "
if len(params) == 1:
condition = f"nargs == {first_pos+1}"
if first_pos:
preamble = f"Passing {first_pos+1} positional arguments to "
message = preamble + (
f"{func.fulldisplayname}() is deprecated. Parameter {pstr} will "
f"become a keyword-only parameter in Python {major}.{minor}."
)
amount = f"{first_pos+1} " if first_pos else ""
pl = "s"
else:
condition = f"nargs > {first_pos} && nargs <= {last_pos+1}"
if first_pos:
preamble = (
f"Passing more than {first_pos} positional "
f"argument{'s' if first_pos != 1 else ''} to "
amount = f"more than {first_pos} " if first_pos else ""
pl = "s" if first_pos != 1 else ""
message = (
f"Passing {amount}positional argument{pl} to "
f"{func.fulldisplayname}() is deprecated."
)
for (major, minor), group in itertools.groupby(
params.values(), key=attrgetter("deprecated_positional")
):
names = [repr(p.name) for p in group]
pstr = pprint_words(names)
if len(names) == 1:
message += (
f" Parameter {pstr} will become a keyword-only parameter "
f"in Python {major}.{minor}."
)
else:
message += (
f" Parameters {pstr} will become keyword-only parameters "
f"in Python {major}.{minor}."
)
message = preamble + (
f"{func.fulldisplayname}() is deprecated. Parameters {pstr} will "
f"become keyword-only parameters in Python {major}.{minor}."
)
# Append deprecation warning to docstring.
docstring = textwrap.fill(f"Note: {message}")
@ -977,19 +975,8 @@ class CLanguage(Language):
argname_fmt: str | None,
) -> str:
assert len(params) > 0
names = [repr(p.name) for p in params.values()]
first_param = next(iter(params.values()))
last_param = next(reversed(params.values()))
# Pretty-print list of names.
pstr = pprint_words(names)
# For now, assume there's only one deprecation level.
assert first_param.deprecated_keyword == last_param.deprecated_keyword
thenceforth = first_param.deprecated_keyword
assert thenceforth is not None
major, minor = thenceforth
# Format the deprecation message.
containscheck = ""
conditions = []
@ -1013,16 +1000,25 @@ class CLanguage(Language):
condition = f"kwargs && PyDict_GET_SIZE(kwargs) && {condition}"
else:
condition = f"kwnames && PyTuple_GET_SIZE(kwnames) && {condition}"
if len(params) == 1:
what1 = "argument"
what2 = "parameter"
else:
what1 = "arguments"
what2 = "parameters"
names = [repr(p.name) for p in params.values()]
pstr = pprint_words(names)
pl = 's' if len(params) != 1 else ''
message = (
f"Passing keyword {what1} {pstr} to {func.fulldisplayname}() is deprecated. "
f"Corresponding {what2} will become positional-only in Python {major}.{minor}."
f"Passing keyword argument{pl} {pstr} to "
f"{func.fulldisplayname}() is deprecated."
)
for (major, minor), group in itertools.groupby(
params.values(), key=attrgetter("deprecated_keyword")
):
names = [repr(p.name) for p in group]
pstr = pprint_words(names)
pl = 's' if len(names) != 1 else ''
message += (
f" Parameter{pl} {pstr} will become positional-only "
f"in Python {major}.{minor}."
)
if containscheck:
errcheck = f"""
if (PyErr_Occurred()) {{{{ // {containscheck}() above can fail
@ -5528,9 +5524,15 @@ class DSLParser:
self.keyword_only = True
else:
if self.keyword_only:
fail(f"Function {function.name!r}: '* [from ...]' must come before '*'")
fail(f"Function {function.name!r}: '* [from ...]' must precede '*'")
if self.deprecated_positional:
fail(f"Function {function.name!r} uses '* [from ...]' more than once.")
if self.deprecated_positional == version:
fail(f"Function {function.name!r} uses '* [from "
f"{version[0]}.{version[1]}]' more than once.")
if self.deprecated_positional < version:
fail(f"Function {function.name!r}: '* [from "
f"{version[0]}.{version[1]}]' must precede '* [from "
f"{self.deprecated_positional[0]}.{self.deprecated_positional[1]}]'")
self.deprecated_positional = version
def parse_opening_square_bracket(self, function: Function) -> None:
@ -5582,7 +5584,13 @@ class DSLParser:
fail(f"Function {function.name!r} uses '/' more than once.")
else:
if self.deprecated_keyword:
fail(f"Function {function.name!r} uses '/ [from ...]' more than once.")
if self.deprecated_keyword == version:
fail(f"Function {function.name!r} uses '/ [from "
f"{version[0]}.{version[1]}]' more than once.")
if self.deprecated_keyword > version:
fail(f"Function {function.name!r}: '/ [from "
f"{version[0]}.{version[1]}]' must precede '/ [from "
f"{self.deprecated_keyword[0]}.{self.deprecated_keyword[1]}]'")
if self.deprecated_positional:
fail(f"Function {function.name!r}: '/ [from ...]' must precede '* [from ...]'")
if self.keyword_only:
@ -5613,7 +5621,7 @@ class DSLParser:
if p.kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
if version is None:
p.kind = inspect.Parameter.POSITIONAL_ONLY
else:
elif p.deprecated_keyword is None:
p.deprecated_keyword = version
def state_parameter_docstring_start(self, line: str) -> None: