blob: d7b86a18680ae46a99944d455a9dcdc332d3ab2b [file]
from __future__ import annotations
import itertools
import textwrap
from typing import TYPE_CHECKING, Final
from operator import attrgetter
from collections.abc import Iterable
import libclinic.cpp
from libclinic import unspecified, fail, VersionTuple
from libclinic.codegen import CRenderData, CodeGen
from libclinic.language import Language
from libclinic.function import (
Module, Class, Function, Parameter,
group_to_variable_name,
GETTER, METHOD_INIT,
ACCESSORS, SETTERS)
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.\n"
"Found a block containing at least two:\n\t"
+ repr(function) + " and " + repr(o),
line_number=o.line_number)
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_str_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)
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)
# insert group variable
group = p.group
if last_group != group:
last_group = group
if group:
group_name = 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 ('/').",
line_number=f.line_number)
# HACK
# when we're METH_O, but have a custom return converter,
# we use "parser_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
'{parser_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 ACCESSORS:
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"
else:
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 SETTERS | {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['parser_parameters'] = ", ".join(data.impl_parameters[1:])
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['parser_retval'] = data.parser_retval
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)
# buffers, not destination
for name, destination in clinic.destination_buffers.items():
template = templates[name]
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()