| 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) |
| |
| # 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 "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 {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['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['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() |