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