mirror of https://github.com/python/cpython
538 lines
20 KiB
Python
538 lines
20 KiB
Python
from __future__ import annotations
|
|
import itertools
|
|
import sys
|
|
import textwrap
|
|
from typing import TYPE_CHECKING, Literal, Final
|
|
from operator import attrgetter
|
|
from collections.abc import Iterable
|
|
|
|
import libclinic
|
|
from libclinic import (
|
|
unspecified, fail, Sentinels, VersionTuple)
|
|
from libclinic.codegen import CRenderData, TemplateDict, CodeGen
|
|
from libclinic.language import Language
|
|
from libclinic.function import (
|
|
Module, Class, Function, Parameter,
|
|
permute_optional_groups,
|
|
GETTER, SETTER, METHOD_INIT)
|
|
from libclinic.converters import self_converter
|
|
from libclinic.parse_args import ParseArgsCodeGen
|
|
if TYPE_CHECKING:
|
|
from libclinic.app import Clinic
|
|
|
|
|
|
def c_id(name: str) -> str:
|
|
if len(name) == 1 and ord(name) < 256:
|
|
if name.isalnum():
|
|
return f"_Py_LATIN1_CHR('{name}')"
|
|
else:
|
|
return f'_Py_LATIN1_CHR({ord(name)})'
|
|
else:
|
|
return f'&_Py_ID({name})'
|
|
|
|
|
|
class CLanguage(Language):
|
|
|
|
body_prefix = "#"
|
|
language = 'C'
|
|
start_line = "/*[{dsl_name} input]"
|
|
body_prefix = ""
|
|
stop_line = "[{dsl_name} start generated code]*/"
|
|
checksum_line = "/*[{dsl_name} end generated code: {arguments}]*/"
|
|
|
|
COMPILER_DEPRECATION_WARNING_PROTOTYPE: Final[str] = r"""
|
|
// Emit compiler warnings when we get to Python {major}.{minor}.
|
|
#if PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00C0
|
|
# error {message}
|
|
#elif PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00A0
|
|
# ifdef _MSC_VER
|
|
# pragma message ({message})
|
|
# else
|
|
# warning {message}
|
|
# endif
|
|
#endif
|
|
"""
|
|
DEPRECATION_WARNING_PROTOTYPE: Final[str] = r"""
|
|
if ({condition}) {{{{{errcheck}
|
|
if (PyErr_WarnEx(PyExc_DeprecationWarning,
|
|
{message}, 1))
|
|
{{{{
|
|
goto exit;
|
|
}}}}
|
|
}}}}
|
|
"""
|
|
|
|
def __init__(self, filename: str) -> None:
|
|
super().__init__(filename)
|
|
self.cpp = libclinic.cpp.Monitor(filename)
|
|
|
|
def parse_line(self, line: str) -> None:
|
|
self.cpp.writeline(line)
|
|
|
|
def render(
|
|
self,
|
|
clinic: Clinic,
|
|
signatures: Iterable[Module | Class | Function]
|
|
) -> str:
|
|
function = None
|
|
for o in signatures:
|
|
if isinstance(o, Function):
|
|
if function:
|
|
fail("You may specify at most one function per block.\nFound a block containing at least two:\n\t" + repr(function) + " and " + repr(o))
|
|
function = o
|
|
return self.render_function(clinic, function)
|
|
|
|
def compiler_deprecated_warning(
|
|
self,
|
|
func: Function,
|
|
parameters: list[Parameter],
|
|
) -> str | None:
|
|
minversion: VersionTuple | None = None
|
|
for p in parameters:
|
|
for version in p.deprecated_positional, p.deprecated_keyword:
|
|
if version and (not minversion or minversion > version):
|
|
minversion = version
|
|
if not minversion:
|
|
return None
|
|
|
|
# Format the preprocessor warning and error messages.
|
|
assert isinstance(self.cpp.filename, str)
|
|
message = f"Update the clinic input of {func.full_name!r}."
|
|
code = self.COMPILER_DEPRECATION_WARNING_PROTOTYPE.format(
|
|
major=minversion[0],
|
|
minor=minversion[1],
|
|
message=libclinic.c_repr(message),
|
|
)
|
|
return libclinic.normalize_snippet(code)
|
|
|
|
def deprecate_positional_use(
|
|
self,
|
|
func: Function,
|
|
params: dict[int, Parameter],
|
|
) -> str:
|
|
assert len(params) > 0
|
|
first_pos = next(iter(params))
|
|
last_pos = next(reversed(params))
|
|
|
|
# Format the deprecation message.
|
|
if len(params) == 1:
|
|
condition = f"nargs == {first_pos+1}"
|
|
amount = f"{first_pos+1} " if first_pos else ""
|
|
pl = "s"
|
|
else:
|
|
condition = f"nargs > {first_pos} && nargs <= {last_pos+1}"
|
|
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 = libclinic.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}."
|
|
)
|
|
|
|
# Append deprecation warning to docstring.
|
|
docstring = textwrap.fill(f"Note: {message}")
|
|
func.docstring += f"\n\n{docstring}\n"
|
|
# Format and return the code block.
|
|
code = self.DEPRECATION_WARNING_PROTOTYPE.format(
|
|
condition=condition,
|
|
errcheck="",
|
|
message=libclinic.wrapped_c_string_literal(message, width=64,
|
|
subsequent_indent=20),
|
|
)
|
|
return libclinic.normalize_snippet(code, indent=4)
|
|
|
|
def deprecate_keyword_use(
|
|
self,
|
|
func: Function,
|
|
params: dict[int, Parameter],
|
|
argname_fmt: str | None = None,
|
|
*,
|
|
fastcall: bool,
|
|
codegen: CodeGen,
|
|
) -> str:
|
|
assert len(params) > 0
|
|
last_param = next(reversed(params.values()))
|
|
limited_capi = codegen.limited_capi
|
|
|
|
# Format the deprecation message.
|
|
containscheck = ""
|
|
conditions = []
|
|
for i, p in params.items():
|
|
if p.is_optional():
|
|
if argname_fmt:
|
|
conditions.append(f"nargs < {i+1} && {argname_fmt % i}")
|
|
elif fastcall:
|
|
conditions.append(f"nargs < {i+1} && PySequence_Contains(kwnames, {c_id(p.name)})")
|
|
containscheck = "PySequence_Contains"
|
|
codegen.add_include('pycore_runtime.h', '_Py_ID()')
|
|
else:
|
|
conditions.append(f"nargs < {i+1} && PyDict_Contains(kwargs, {c_id(p.name)})")
|
|
containscheck = "PyDict_Contains"
|
|
codegen.add_include('pycore_runtime.h', '_Py_ID()')
|
|
else:
|
|
conditions = [f"nargs < {i+1}"]
|
|
condition = ") || (".join(conditions)
|
|
if len(conditions) > 1:
|
|
condition = f"(({condition}))"
|
|
if last_param.is_optional():
|
|
if fastcall:
|
|
if limited_capi:
|
|
condition = f"kwnames && PyTuple_Size(kwnames) && {condition}"
|
|
else:
|
|
condition = f"kwnames && PyTuple_GET_SIZE(kwnames) && {condition}"
|
|
else:
|
|
if limited_capi:
|
|
condition = f"kwargs && PyDict_Size(kwargs) && {condition}"
|
|
else:
|
|
condition = f"kwargs && PyDict_GET_SIZE(kwargs) && {condition}"
|
|
names = [repr(p.name) for p in params.values()]
|
|
pstr = libclinic.pprint_words(names)
|
|
pl = 's' if len(params) != 1 else ''
|
|
message = (
|
|
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 = libclinic.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
|
|
goto exit;
|
|
}}}}"""
|
|
else:
|
|
errcheck = ""
|
|
if argname_fmt:
|
|
# Append deprecation warning to docstring.
|
|
docstring = textwrap.fill(f"Note: {message}")
|
|
func.docstring += f"\n\n{docstring}\n"
|
|
# Format and return the code block.
|
|
code = self.DEPRECATION_WARNING_PROTOTYPE.format(
|
|
condition=condition,
|
|
errcheck=errcheck,
|
|
message=libclinic.wrapped_c_string_literal(message, width=64,
|
|
subsequent_indent=20),
|
|
)
|
|
return libclinic.normalize_snippet(code, indent=4)
|
|
|
|
def output_templates(
|
|
self,
|
|
f: Function,
|
|
codegen: CodeGen,
|
|
) -> dict[str, str]:
|
|
args = ParseArgsCodeGen(f, codegen)
|
|
return args.parse_args(self)
|
|
|
|
@staticmethod
|
|
def group_to_variable_name(group: int) -> str:
|
|
adjective = "left_" if group < 0 else "right_"
|
|
return "group_" + adjective + str(abs(group))
|
|
|
|
def render_option_group_parsing(
|
|
self,
|
|
f: Function,
|
|
template_dict: TemplateDict,
|
|
limited_capi: bool,
|
|
) -> None:
|
|
# positional only, grouped, optional arguments!
|
|
# can be optional on the left or right.
|
|
# here's an example:
|
|
#
|
|
# [ [ [ A1 A2 ] B1 B2 B3 ] C1 C2 ] D1 D2 D3 [ E1 E2 E3 [ F1 F2 F3 ] ]
|
|
#
|
|
# Here group D are required, and all other groups are optional.
|
|
# (Group D's "group" is actually None.)
|
|
# We can figure out which sets of arguments we have based on
|
|
# how many arguments are in the tuple.
|
|
#
|
|
# Note that you need to count up on both sides. For example,
|
|
# you could have groups C+D, or C+D+E, or C+D+E+F.
|
|
#
|
|
# What if the number of arguments leads us to an ambiguous result?
|
|
# Clinic prefers groups on the left. So in the above example,
|
|
# five arguments would map to B+C, not C+D.
|
|
|
|
out = []
|
|
parameters = list(f.parameters.values())
|
|
if isinstance(parameters[0].converter, self_converter):
|
|
del parameters[0]
|
|
|
|
group: list[Parameter] | None = None
|
|
left = []
|
|
right = []
|
|
required: list[Parameter] = []
|
|
last: int | Literal[Sentinels.unspecified] = unspecified
|
|
|
|
for p in parameters:
|
|
group_id = p.group
|
|
if group_id != last:
|
|
last = group_id
|
|
group = []
|
|
if group_id < 0:
|
|
left.append(group)
|
|
elif group_id == 0:
|
|
group = required
|
|
else:
|
|
right.append(group)
|
|
assert group is not None
|
|
group.append(p)
|
|
|
|
count_min = sys.maxsize
|
|
count_max = -1
|
|
|
|
if limited_capi:
|
|
nargs = 'PyTuple_Size(args)'
|
|
else:
|
|
nargs = 'PyTuple_GET_SIZE(args)'
|
|
out.append(f"switch ({nargs}) {{\n")
|
|
for subset in permute_optional_groups(left, required, right):
|
|
count = len(subset)
|
|
count_min = min(count_min, count)
|
|
count_max = max(count_max, count)
|
|
|
|
if count == 0:
|
|
out.append(""" case 0:
|
|
break;
|
|
""")
|
|
continue
|
|
|
|
group_ids = {p.group for p in subset} # eliminate duplicates
|
|
d: dict[str, str | int] = {}
|
|
d['count'] = count
|
|
d['name'] = f.name
|
|
d['format_units'] = "".join(p.converter.format_unit for p in subset)
|
|
|
|
parse_arguments: list[str] = []
|
|
for p in subset:
|
|
p.converter.parse_argument(parse_arguments)
|
|
d['parse_arguments'] = ", ".join(parse_arguments)
|
|
|
|
group_ids.discard(0)
|
|
lines = "\n".join([
|
|
self.group_to_variable_name(g) + " = 1;"
|
|
for g in group_ids
|
|
])
|
|
|
|
s = """\
|
|
case {count}:
|
|
if (!PyArg_ParseTuple(args, "{format_units}:{name}", {parse_arguments})) {{
|
|
goto exit;
|
|
}}
|
|
{group_booleans}
|
|
break;
|
|
"""
|
|
s = libclinic.linear_format(s, group_booleans=lines)
|
|
s = s.format_map(d)
|
|
out.append(s)
|
|
|
|
out.append(" default:\n")
|
|
s = ' PyErr_SetString(PyExc_TypeError, "{} requires {} to {} arguments");\n'
|
|
out.append(s.format(f.full_name, count_min, count_max))
|
|
out.append(' goto exit;\n')
|
|
out.append("}")
|
|
|
|
template_dict['option_group_parsing'] = libclinic.format_escape("".join(out))
|
|
|
|
def render_function(
|
|
self,
|
|
clinic: Clinic,
|
|
f: Function | None
|
|
) -> str:
|
|
if f is None:
|
|
return ""
|
|
|
|
codegen = clinic.codegen
|
|
data = CRenderData()
|
|
|
|
assert f.parameters, "We should always have a 'self' at this point!"
|
|
parameters = f.render_parameters
|
|
converters = [p.converter for p in parameters]
|
|
|
|
templates = self.output_templates(f, codegen)
|
|
|
|
f_self = parameters[0]
|
|
selfless = parameters[1:]
|
|
assert isinstance(f_self.converter, self_converter), "No self parameter in " + repr(f.full_name) + "!"
|
|
|
|
if f.critical_section:
|
|
match len(f.target_critical_section):
|
|
case 0:
|
|
lock = 'Py_BEGIN_CRITICAL_SECTION({self_name});'
|
|
unlock = 'Py_END_CRITICAL_SECTION();'
|
|
case 1:
|
|
lock = 'Py_BEGIN_CRITICAL_SECTION({target_critical_section});'
|
|
unlock = 'Py_END_CRITICAL_SECTION();'
|
|
case _:
|
|
lock = 'Py_BEGIN_CRITICAL_SECTION2({target_critical_section});'
|
|
unlock = 'Py_END_CRITICAL_SECTION2();'
|
|
data.lock.append(lock)
|
|
data.unlock.append(unlock)
|
|
|
|
last_group = 0
|
|
first_optional = len(selfless)
|
|
positional = selfless and selfless[-1].is_positional_only()
|
|
has_option_groups = False
|
|
|
|
# offset i by -1 because first_optional needs to ignore self
|
|
for i, p in enumerate(parameters, -1):
|
|
c = p.converter
|
|
|
|
if (i != -1) and (p.default is not unspecified):
|
|
first_optional = min(first_optional, i)
|
|
|
|
if p.is_vararg():
|
|
data.cleanup.append(f"Py_XDECREF({c.parser_name});")
|
|
|
|
# insert group variable
|
|
group = p.group
|
|
if last_group != group:
|
|
last_group = group
|
|
if group:
|
|
group_name = self.group_to_variable_name(group)
|
|
data.impl_arguments.append(group_name)
|
|
data.declarations.append("int " + group_name + " = 0;")
|
|
data.impl_parameters.append("int " + group_name)
|
|
has_option_groups = True
|
|
|
|
c.render(p, data)
|
|
|
|
if has_option_groups and (not positional):
|
|
fail("You cannot use optional groups ('[' and ']') "
|
|
"unless all parameters are positional-only ('/').")
|
|
|
|
# HACK
|
|
# when we're METH_O, but have a custom return converter,
|
|
# we use "impl_parameters" for the parsing function
|
|
# because that works better. but that means we must
|
|
# suppress actually declaring the impl's parameters
|
|
# as variables in the parsing function. but since it's
|
|
# METH_O, we have exactly one anyway, so we know exactly
|
|
# where it is.
|
|
if ("METH_O" in templates['methoddef_define'] and
|
|
'{impl_parameters}' in templates['parser_prototype']):
|
|
data.declarations.pop(0)
|
|
|
|
full_name = f.full_name
|
|
template_dict = {'full_name': full_name}
|
|
template_dict['name'] = f.displayname
|
|
if f.kind in {GETTER, SETTER}:
|
|
template_dict['getset_name'] = f.c_basename.upper()
|
|
template_dict['getset_basename'] = f.c_basename
|
|
if f.kind is GETTER:
|
|
template_dict['c_basename'] = f.c_basename + "_get"
|
|
elif f.kind is SETTER:
|
|
template_dict['c_basename'] = f.c_basename + "_set"
|
|
# Implicitly add the setter value parameter.
|
|
data.impl_parameters.append("PyObject *value")
|
|
data.impl_arguments.append("value")
|
|
else:
|
|
template_dict['methoddef_name'] = f.c_basename.upper() + "_METHODDEF"
|
|
template_dict['c_basename'] = f.c_basename
|
|
|
|
template_dict['docstring'] = libclinic.docstring_for_c_string(f.docstring)
|
|
template_dict['self_name'] = template_dict['self_type'] = template_dict['self_type_check'] = ''
|
|
template_dict['target_critical_section'] = ', '.join(f.target_critical_section)
|
|
for converter in converters:
|
|
converter.set_template_dict(template_dict)
|
|
|
|
if f.kind not in {SETTER, METHOD_INIT}:
|
|
f.return_converter.render(f, data)
|
|
template_dict['impl_return_type'] = f.return_converter.type
|
|
|
|
template_dict['declarations'] = libclinic.format_escape("\n".join(data.declarations))
|
|
template_dict['initializers'] = "\n\n".join(data.initializers)
|
|
template_dict['modifications'] = '\n\n'.join(data.modifications)
|
|
template_dict['keywords_c'] = ' '.join('"' + k + '",'
|
|
for k in data.keywords)
|
|
keywords = [k for k in data.keywords if k]
|
|
template_dict['keywords_py'] = ' '.join(c_id(k) + ','
|
|
for k in keywords)
|
|
template_dict['format_units'] = ''.join(data.format_units)
|
|
template_dict['parse_arguments'] = ', '.join(data.parse_arguments)
|
|
if data.parse_arguments:
|
|
template_dict['parse_arguments_comma'] = ',';
|
|
else:
|
|
template_dict['parse_arguments_comma'] = '';
|
|
template_dict['impl_parameters'] = ", ".join(data.impl_parameters)
|
|
template_dict['impl_arguments'] = ", ".join(data.impl_arguments)
|
|
|
|
template_dict['return_conversion'] = libclinic.format_escape("".join(data.return_conversion).rstrip())
|
|
template_dict['post_parsing'] = libclinic.format_escape("".join(data.post_parsing).rstrip())
|
|
template_dict['cleanup'] = libclinic.format_escape("".join(data.cleanup))
|
|
|
|
template_dict['return_value'] = data.return_value
|
|
template_dict['lock'] = "\n".join(data.lock)
|
|
template_dict['unlock'] = "\n".join(data.unlock)
|
|
|
|
# used by unpack tuple code generator
|
|
unpack_min = first_optional
|
|
unpack_max = len(selfless)
|
|
template_dict['unpack_min'] = str(unpack_min)
|
|
template_dict['unpack_max'] = str(unpack_max)
|
|
|
|
if has_option_groups:
|
|
self.render_option_group_parsing(f, template_dict,
|
|
limited_capi=codegen.limited_capi)
|
|
|
|
# buffers, not destination
|
|
for name, destination in clinic.destination_buffers.items():
|
|
template = templates[name]
|
|
if has_option_groups:
|
|
template = libclinic.linear_format(template,
|
|
option_group_parsing=template_dict['option_group_parsing'])
|
|
template = libclinic.linear_format(template,
|
|
declarations=template_dict['declarations'],
|
|
return_conversion=template_dict['return_conversion'],
|
|
initializers=template_dict['initializers'],
|
|
modifications=template_dict['modifications'],
|
|
post_parsing=template_dict['post_parsing'],
|
|
cleanup=template_dict['cleanup'],
|
|
lock=template_dict['lock'],
|
|
unlock=template_dict['unlock'],
|
|
)
|
|
|
|
# Only generate the "exit:" label
|
|
# if we have any gotos
|
|
label = "exit:" if "goto exit;" in template else ""
|
|
template = libclinic.linear_format(template, exit_label=label)
|
|
|
|
s = template.format_map(template_dict)
|
|
|
|
# mild hack:
|
|
# reflow long impl declarations
|
|
if name in {"impl_prototype", "impl_definition"}:
|
|
s = libclinic.wrap_declarations(s)
|
|
|
|
if clinic.line_prefix:
|
|
s = libclinic.indent_all_lines(s, clinic.line_prefix)
|
|
if clinic.line_suffix:
|
|
s = libclinic.suffix_all_lines(s, clinic.line_suffix)
|
|
|
|
destination.append(s)
|
|
|
|
return clinic.get_destination('block').dump()
|