| 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() |