mirror of
https://github.com/python/cpython
synced 2026-09-29 12:10:30 +03:00
gh-95065: Add Argument Clinic support for deprecating positional use of parameters (#95151)
It is now possible to deprecate passing parameters positionally with Argument Clinic, using the new '* [from X.Y]' syntax. (To be read as "keyword-only from Python version X.Y") Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
This commit is contained in:
parent
3c8e8f3cee
commit
33cb0b06ef
5 changed files with 1153 additions and 14 deletions
|
|
@ -347,6 +347,13 @@ def suffix_all_lines(s: str, suffix: str) -> str:
|
|||
return ''.join(final)
|
||||
|
||||
|
||||
def pprint_words(items: list[str]) -> str:
|
||||
if len(items) <= 2:
|
||||
return " and ".join(items)
|
||||
else:
|
||||
return ", ".join(items[:-1]) + " and " + items[-1]
|
||||
|
||||
|
||||
def version_splitter(s: str) -> tuple[int, ...]:
|
||||
"""Splits a version string into a tuple of integers.
|
||||
|
||||
|
|
@ -828,6 +835,22 @@ class CLanguage(Language):
|
|||
#define {methoddef_name}
|
||||
#endif /* !defined({methoddef_name}) */
|
||||
""")
|
||||
DEPRECATED_POSITIONAL_PROTOTYPE: Final[str] = r"""
|
||||
#if PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00C0
|
||||
# error "{cpp_message}"
|
||||
#elif PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00A0
|
||||
# ifdef _MSC_VER
|
||||
# pragma message ("{cpp_message}")
|
||||
# else
|
||||
# warning "{cpp_message}"
|
||||
# endif
|
||||
#endif
|
||||
if ({condition}) {{{{
|
||||
if (PyErr_WarnEx(PyExc_DeprecationWarning, "{depr_message}", 1)) {{{{
|
||||
goto exit;
|
||||
}}}}
|
||||
}}}}
|
||||
"""
|
||||
|
||||
def __init__(self, filename: str) -> None:
|
||||
super().__init__(filename)
|
||||
|
|
@ -850,6 +873,64 @@ class CLanguage(Language):
|
|||
function = o
|
||||
return self.render_function(clinic, function)
|
||||
|
||||
def deprecate_positional_use(
|
||||
self,
|
||||
func: Function,
|
||||
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
|
||||
|
||||
# Format the preprocessor warning and error messages.
|
||||
assert isinstance(self.cpp.filename, str)
|
||||
source = os.path.basename(self.cpp.filename)
|
||||
major, minor = thenceforth
|
||||
cpp_message = (
|
||||
f"In {source}, update parameter(s) {pstr} in the clinic "
|
||||
f"input of {func.full_name!r} to be keyword-only."
|
||||
)
|
||||
# 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 "
|
||||
depr_message = preamble + (
|
||||
f"{func.full_name}() is deprecated. Parameter {pstr} will "
|
||||
f"become a keyword-only parameter in Python {major}.{minor}."
|
||||
)
|
||||
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 "
|
||||
)
|
||||
depr_message = preamble + (
|
||||
f"{func.full_name}() is deprecated. Parameters {pstr} will "
|
||||
f"become keyword-only parameters in Python {major}.{minor}."
|
||||
)
|
||||
# Format and return the code block.
|
||||
code = self.DEPRECATED_POSITIONAL_PROTOTYPE.format(
|
||||
condition=condition,
|
||||
major=major,
|
||||
minor=minor,
|
||||
cpp_message=cpp_message,
|
||||
depr_message=depr_message,
|
||||
)
|
||||
return normalize_snippet(code, indent=4)
|
||||
|
||||
def docstring_for_c_string(
|
||||
self,
|
||||
f: Function
|
||||
|
|
@ -1199,6 +1280,7 @@ class CLanguage(Language):
|
|||
flags = 'METH_METHOD|' + flags
|
||||
parser_prototype = self.PARSER_PROTOTYPE_DEF_CLASS
|
||||
|
||||
deprecated_positionals: dict[int, Parameter] = {}
|
||||
add_label: str | None = None
|
||||
for i, p in enumerate(parameters):
|
||||
if isinstance(p.converter, defining_class_converter):
|
||||
|
|
@ -1213,6 +1295,8 @@ class CLanguage(Language):
|
|||
parser_code.append("%s:" % add_label)
|
||||
add_label = None
|
||||
if not p.is_optional():
|
||||
if p.deprecated_positional:
|
||||
deprecated_positionals[i] = p
|
||||
parser_code.append(normalize_snippet(parsearg, indent=4))
|
||||
elif i < pos_only:
|
||||
add_label = 'skip_optional_posonly'
|
||||
|
|
@ -1242,6 +1326,8 @@ class CLanguage(Language):
|
|||
goto %s;
|
||||
}}
|
||||
""" % add_label, indent=4))
|
||||
if p.deprecated_positional:
|
||||
deprecated_positionals[i] = p
|
||||
if i + 1 == len(parameters):
|
||||
parser_code.append(normalize_snippet(parsearg, indent=4))
|
||||
else:
|
||||
|
|
@ -1257,6 +1343,12 @@ class CLanguage(Language):
|
|||
}}
|
||||
""" % add_label, indent=4))
|
||||
|
||||
if deprecated_positionals:
|
||||
code = self.deprecate_positional_use(f, deprecated_positionals)
|
||||
assert parser_code is not None
|
||||
# Insert the deprecation code before parameter parsing.
|
||||
parser_code.insert(0, code)
|
||||
|
||||
if parser_code is not None:
|
||||
if add_label:
|
||||
parser_code.append("%s:" % add_label)
|
||||
|
|
@ -2592,6 +2684,9 @@ class Function:
|
|||
return f
|
||||
|
||||
|
||||
VersionTuple = tuple[int, int]
|
||||
|
||||
|
||||
@dc.dataclass(repr=False, slots=True)
|
||||
class Parameter:
|
||||
"""
|
||||
|
|
@ -2606,6 +2701,8 @@ class Parameter:
|
|||
annotation: object = inspect.Parameter.empty
|
||||
docstring: str = ''
|
||||
group: int = 0
|
||||
# (`None` signifies that there is no deprecation)
|
||||
deprecated_positional: VersionTuple | None = None
|
||||
right_bracket_count: int = dc.field(init=False, default=0)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
|
|
@ -4430,6 +4527,7 @@ class DSLParser:
|
|||
state: StateKeeper
|
||||
keyword_only: bool
|
||||
positional_only: bool
|
||||
deprecated_positional: VersionTuple | None
|
||||
group: int
|
||||
parameter_state: ParamState
|
||||
indent: IndentStack
|
||||
|
|
@ -4437,6 +4535,11 @@ class DSLParser:
|
|||
coexist: bool
|
||||
parameter_continuation: str
|
||||
preserve_output: bool
|
||||
star_from_version_re = create_regex(
|
||||
before="* [from ",
|
||||
after="]",
|
||||
word=False,
|
||||
)
|
||||
|
||||
def __init__(self, clinic: Clinic) -> None:
|
||||
self.clinic = clinic
|
||||
|
|
@ -4460,6 +4563,7 @@ class DSLParser:
|
|||
self.state = self.state_dsl_start
|
||||
self.keyword_only = False
|
||||
self.positional_only = False
|
||||
self.deprecated_positional = None
|
||||
self.group = 0
|
||||
self.parameter_state: ParamState = ParamState.START
|
||||
self.indent = IndentStack()
|
||||
|
|
@ -4622,7 +4726,7 @@ class DSLParser:
|
|||
exc.lineno = line_number
|
||||
raise
|
||||
|
||||
self.do_post_block_processing_cleanup()
|
||||
self.do_post_block_processing_cleanup(line_number)
|
||||
block.output.extend(self.clinic.language.render(self.clinic, block.signatures))
|
||||
|
||||
if self.preserve_output:
|
||||
|
|
@ -4908,8 +5012,14 @@ class DSLParser:
|
|||
self.parameter_continuation = line[:-1]
|
||||
return
|
||||
|
||||
line = line.lstrip()
|
||||
match = self.star_from_version_re.match(line)
|
||||
if match:
|
||||
self.parse_deprecated_positional(match.group(1))
|
||||
return
|
||||
|
||||
func = self.function
|
||||
match line.lstrip():
|
||||
match line:
|
||||
case '*':
|
||||
self.parse_star(func)
|
||||
case '[':
|
||||
|
|
@ -5182,7 +5292,9 @@ class DSLParser:
|
|||
"after 'self'.")
|
||||
|
||||
|
||||
p = Parameter(parameter_name, kind, function=self.function, converter=converter, default=value, group=self.group)
|
||||
p = Parameter(parameter_name, kind, function=self.function,
|
||||
converter=converter, default=value, group=self.group,
|
||||
deprecated_positional=self.deprecated_positional)
|
||||
|
||||
names = [k.name for k in self.function.parameters.values()]
|
||||
if parameter_name in names[1:]:
|
||||
|
|
@ -5215,10 +5327,28 @@ class DSLParser:
|
|||
"Annotations must be either a name, a function call, or a string."
|
||||
)
|
||||
|
||||
def parse_deprecated_positional(self, thenceforth: str) -> None:
|
||||
assert isinstance(self.function, Function)
|
||||
fname = self.function.full_name
|
||||
|
||||
if self.keyword_only:
|
||||
fail(f"Function {fname!r}: '* [from ...]' must come before '*'")
|
||||
if self.deprecated_positional:
|
||||
fail(f"Function {fname!r} uses '[from ...]' more than once.")
|
||||
try:
|
||||
major, minor = thenceforth.split(".")
|
||||
self.deprecated_positional = int(major), int(minor)
|
||||
except ValueError:
|
||||
fail(
|
||||
f"Function {fname!r}: expected format '* [from major.minor]' "
|
||||
f"where 'major' and 'minor' are integers; got {thenceforth!r}"
|
||||
)
|
||||
|
||||
def parse_star(self, function: Function) -> None:
|
||||
"""Parse keyword-only parameter marker '*'."""
|
||||
if self.keyword_only:
|
||||
fail(f"Function {function.name!r} uses '*' more than once.")
|
||||
self.deprecated_positional = None
|
||||
self.keyword_only = True
|
||||
|
||||
def parse_opening_square_bracket(self, function: Function) -> None:
|
||||
|
|
@ -5586,23 +5716,34 @@ class DSLParser:
|
|||
|
||||
return docstring
|
||||
|
||||
def do_post_block_processing_cleanup(self) -> None:
|
||||
def do_post_block_processing_cleanup(self, lineno: int) -> None:
|
||||
"""
|
||||
Called when processing the block is done.
|
||||
"""
|
||||
if not self.function:
|
||||
return
|
||||
|
||||
if self.keyword_only:
|
||||
values = self.function.parameters.values()
|
||||
if not values:
|
||||
no_parameter_after_star = True
|
||||
def check_remaining(
|
||||
symbol: str,
|
||||
condition: Callable[[Parameter], bool]
|
||||
) -> None:
|
||||
assert isinstance(self.function, Function)
|
||||
|
||||
if values := self.function.parameters.values():
|
||||
last_param = next(reversed(values))
|
||||
no_param_after_symbol = condition(last_param)
|
||||
else:
|
||||
last_parameter = next(reversed(list(values)))
|
||||
no_parameter_after_star = last_parameter.kind != inspect.Parameter.KEYWORD_ONLY
|
||||
if no_parameter_after_star:
|
||||
fail(f"Function {self.function.name!r} specifies '*' "
|
||||
"without any parameters afterwards.")
|
||||
no_param_after_symbol = True
|
||||
if no_param_after_symbol:
|
||||
fname = self.function.full_name
|
||||
fail(f"Function {fname!r} specifies {symbol!r} "
|
||||
"without any parameters afterwards.", line_number=lineno)
|
||||
|
||||
if self.keyword_only:
|
||||
check_remaining("*", lambda p: p.kind != inspect.Parameter.KEYWORD_ONLY)
|
||||
|
||||
if self.deprecated_positional:
|
||||
check_remaining("* [from ...]", lambda p: not p.deprecated_positional)
|
||||
|
||||
self.function.docstring = self.format_docstring()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue