• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1from __future__ import annotations
2import itertools
3import sys
4import textwrap
5from typing import TYPE_CHECKING, Literal, Final
6from operator import attrgetter
7from collections.abc import Iterable
8
9import libclinic
10from libclinic import (
11    unspecified, fail, Sentinels, VersionTuple)
12from libclinic.codegen import CRenderData, TemplateDict, CodeGen
13from libclinic.language import Language
14from libclinic.function import (
15    Module, Class, Function, Parameter,
16    permute_optional_groups,
17    GETTER, SETTER, METHOD_INIT)
18from libclinic.converters import self_converter
19from libclinic.parse_args import ParseArgsCodeGen
20if TYPE_CHECKING:
21    from libclinic.app import Clinic
22
23
24def c_id(name: str) -> str:
25    if len(name) == 1 and ord(name) < 256:
26        if name.isalnum():
27            return f"_Py_LATIN1_CHR('{name}')"
28        else:
29            return f'_Py_LATIN1_CHR({ord(name)})'
30    else:
31        return f'&_Py_ID({name})'
32
33
34class CLanguage(Language):
35
36    body_prefix   = "#"
37    language      = 'C'
38    start_line    = "/*[{dsl_name} input]"
39    body_prefix   = ""
40    stop_line     = "[{dsl_name} start generated code]*/"
41    checksum_line = "/*[{dsl_name} end generated code: {arguments}]*/"
42
43    COMPILER_DEPRECATION_WARNING_PROTOTYPE: Final[str] = r"""
44        // Emit compiler warnings when we get to Python {major}.{minor}.
45        #if PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00C0
46        #  error {message}
47        #elif PY_VERSION_HEX >= 0x{major:02x}{minor:02x}00A0
48        #  ifdef _MSC_VER
49        #    pragma message ({message})
50        #  else
51        #    warning {message}
52        #  endif
53        #endif
54    """
55    DEPRECATION_WARNING_PROTOTYPE: Final[str] = r"""
56        if ({condition}) {{{{{errcheck}
57            if (PyErr_WarnEx(PyExc_DeprecationWarning,
58                    {message}, 1))
59            {{{{
60                goto exit;
61            }}}}
62        }}}}
63    """
64
65    def __init__(self, filename: str) -> None:
66        super().__init__(filename)
67        self.cpp = libclinic.cpp.Monitor(filename)
68
69    def parse_line(self, line: str) -> None:
70        self.cpp.writeline(line)
71
72    def render(
73        self,
74        clinic: Clinic,
75        signatures: Iterable[Module | Class | Function]
76    ) -> str:
77        function = None
78        for o in signatures:
79            if isinstance(o, Function):
80                if function:
81                    fail("You may specify at most one function per block.\nFound a block containing at least two:\n\t" + repr(function) + " and " + repr(o))
82                function = o
83        return self.render_function(clinic, function)
84
85    def compiler_deprecated_warning(
86        self,
87        func: Function,
88        parameters: list[Parameter],
89    ) -> str | None:
90        minversion: VersionTuple | None = None
91        for p in parameters:
92            for version in p.deprecated_positional, p.deprecated_keyword:
93                if version and (not minversion or minversion > version):
94                    minversion = version
95        if not minversion:
96            return None
97
98        # Format the preprocessor warning and error messages.
99        assert isinstance(self.cpp.filename, str)
100        message = f"Update the clinic input of {func.full_name!r}."
101        code = self.COMPILER_DEPRECATION_WARNING_PROTOTYPE.format(
102            major=minversion[0],
103            minor=minversion[1],
104            message=libclinic.c_repr(message),
105        )
106        return libclinic.normalize_snippet(code)
107
108    def deprecate_positional_use(
109        self,
110        func: Function,
111        params: dict[int, Parameter],
112    ) -> str:
113        assert len(params) > 0
114        first_pos = next(iter(params))
115        last_pos = next(reversed(params))
116
117        # Format the deprecation message.
118        if len(params) == 1:
119            condition = f"nargs == {first_pos+1}"
120            amount = f"{first_pos+1} " if first_pos else ""
121            pl = "s"
122        else:
123            condition = f"nargs > {first_pos} && nargs <= {last_pos+1}"
124            amount = f"more than {first_pos} " if first_pos else ""
125            pl = "s" if first_pos != 1 else ""
126        message = (
127            f"Passing {amount}positional argument{pl} to "
128            f"{func.fulldisplayname}() is deprecated."
129        )
130
131        for (major, minor), group in itertools.groupby(
132            params.values(), key=attrgetter("deprecated_positional")
133        ):
134            names = [repr(p.name) for p in group]
135            pstr = libclinic.pprint_words(names)
136            if len(names) == 1:
137                message += (
138                    f" Parameter {pstr} will become a keyword-only parameter "
139                    f"in Python {major}.{minor}."
140                )
141            else:
142                message += (
143                    f" Parameters {pstr} will become keyword-only parameters "
144                    f"in Python {major}.{minor}."
145                )
146
147        # Append deprecation warning to docstring.
148        docstring = textwrap.fill(f"Note: {message}")
149        func.docstring += f"\n\n{docstring}\n"
150        # Format and return the code block.
151        code = self.DEPRECATION_WARNING_PROTOTYPE.format(
152            condition=condition,
153            errcheck="",
154            message=libclinic.wrapped_c_string_literal(message, width=64,
155                                                       subsequent_indent=20),
156        )
157        return libclinic.normalize_snippet(code, indent=4)
158
159    def deprecate_keyword_use(
160        self,
161        func: Function,
162        params: dict[int, Parameter],
163        argname_fmt: str | None = None,
164        *,
165        fastcall: bool,
166        codegen: CodeGen,
167    ) -> str:
168        assert len(params) > 0
169        last_param = next(reversed(params.values()))
170        limited_capi = codegen.limited_capi
171
172        # Format the deprecation message.
173        containscheck = ""
174        conditions = []
175        for i, p in params.items():
176            if p.is_optional():
177                if argname_fmt:
178                    conditions.append(f"nargs < {i+1} && {argname_fmt % i}")
179                elif fastcall:
180                    conditions.append(f"nargs < {i+1} && PySequence_Contains(kwnames, {c_id(p.name)})")
181                    containscheck = "PySequence_Contains"
182                    codegen.add_include('pycore_runtime.h', '_Py_ID()')
183                else:
184                    conditions.append(f"nargs < {i+1} && PyDict_Contains(kwargs, {c_id(p.name)})")
185                    containscheck = "PyDict_Contains"
186                    codegen.add_include('pycore_runtime.h', '_Py_ID()')
187            else:
188                conditions = [f"nargs < {i+1}"]
189        condition = ") || (".join(conditions)
190        if len(conditions) > 1:
191            condition = f"(({condition}))"
192        if last_param.is_optional():
193            if fastcall:
194                if limited_capi:
195                    condition = f"kwnames && PyTuple_Size(kwnames) && {condition}"
196                else:
197                    condition = f"kwnames && PyTuple_GET_SIZE(kwnames) && {condition}"
198            else:
199                if limited_capi:
200                    condition = f"kwargs && PyDict_Size(kwargs) && {condition}"
201                else:
202                    condition = f"kwargs && PyDict_GET_SIZE(kwargs) && {condition}"
203        names = [repr(p.name) for p in params.values()]
204        pstr = libclinic.pprint_words(names)
205        pl = 's' if len(params) != 1 else ''
206        message = (
207            f"Passing keyword argument{pl} {pstr} to "
208            f"{func.fulldisplayname}() is deprecated."
209        )
210
211        for (major, minor), group in itertools.groupby(
212            params.values(), key=attrgetter("deprecated_keyword")
213        ):
214            names = [repr(p.name) for p in group]
215            pstr = libclinic.pprint_words(names)
216            pl = 's' if len(names) != 1 else ''
217            message += (
218                f" Parameter{pl} {pstr} will become positional-only "
219                f"in Python {major}.{minor}."
220            )
221
222        if containscheck:
223            errcheck = f"""
224            if (PyErr_Occurred()) {{{{ // {containscheck}() above can fail
225                goto exit;
226            }}}}"""
227        else:
228            errcheck = ""
229        if argname_fmt:
230            # Append deprecation warning to docstring.
231            docstring = textwrap.fill(f"Note: {message}")
232            func.docstring += f"\n\n{docstring}\n"
233        # Format and return the code block.
234        code = self.DEPRECATION_WARNING_PROTOTYPE.format(
235            condition=condition,
236            errcheck=errcheck,
237            message=libclinic.wrapped_c_string_literal(message, width=64,
238                                                       subsequent_indent=20),
239        )
240        return libclinic.normalize_snippet(code, indent=4)
241
242    def output_templates(
243        self,
244        f: Function,
245        codegen: CodeGen,
246    ) -> dict[str, str]:
247        args = ParseArgsCodeGen(f, codegen)
248        return args.parse_args(self)
249
250    @staticmethod
251    def group_to_variable_name(group: int) -> str:
252        adjective = "left_" if group < 0 else "right_"
253        return "group_" + adjective + str(abs(group))
254
255    def render_option_group_parsing(
256        self,
257        f: Function,
258        template_dict: TemplateDict,
259        limited_capi: bool,
260    ) -> None:
261        # positional only, grouped, optional arguments!
262        # can be optional on the left or right.
263        # here's an example:
264        #
265        # [ [ [ A1 A2 ] B1 B2 B3 ] C1 C2 ] D1 D2 D3 [ E1 E2 E3 [ F1 F2 F3 ] ]
266        #
267        # Here group D are required, and all other groups are optional.
268        # (Group D's "group" is actually None.)
269        # We can figure out which sets of arguments we have based on
270        # how many arguments are in the tuple.
271        #
272        # Note that you need to count up on both sides.  For example,
273        # you could have groups C+D, or C+D+E, or C+D+E+F.
274        #
275        # What if the number of arguments leads us to an ambiguous result?
276        # Clinic prefers groups on the left.  So in the above example,
277        # five arguments would map to B+C, not C+D.
278
279        out = []
280        parameters = list(f.parameters.values())
281        if isinstance(parameters[0].converter, self_converter):
282            del parameters[0]
283
284        group: list[Parameter] | None = None
285        left = []
286        right = []
287        required: list[Parameter] = []
288        last: int | Literal[Sentinels.unspecified] = unspecified
289
290        for p in parameters:
291            group_id = p.group
292            if group_id != last:
293                last = group_id
294                group = []
295                if group_id < 0:
296                    left.append(group)
297                elif group_id == 0:
298                    group = required
299                else:
300                    right.append(group)
301            assert group is not None
302            group.append(p)
303
304        count_min = sys.maxsize
305        count_max = -1
306
307        if limited_capi:
308            nargs = 'PyTuple_Size(args)'
309        else:
310            nargs = 'PyTuple_GET_SIZE(args)'
311        out.append(f"switch ({nargs}) {{\n")
312        for subset in permute_optional_groups(left, required, right):
313            count = len(subset)
314            count_min = min(count_min, count)
315            count_max = max(count_max, count)
316
317            if count == 0:
318                out.append("""    case 0:
319        break;
320""")
321                continue
322
323            group_ids = {p.group for p in subset}  # eliminate duplicates
324            d: dict[str, str | int] = {}
325            d['count'] = count
326            d['name'] = f.name
327            d['format_units'] = "".join(p.converter.format_unit for p in subset)
328
329            parse_arguments: list[str] = []
330            for p in subset:
331                p.converter.parse_argument(parse_arguments)
332            d['parse_arguments'] = ", ".join(parse_arguments)
333
334            group_ids.discard(0)
335            lines = "\n".join([
336                self.group_to_variable_name(g) + " = 1;"
337                for g in group_ids
338            ])
339
340            s = """\
341    case {count}:
342        if (!PyArg_ParseTuple(args, "{format_units}:{name}", {parse_arguments})) {{
343            goto exit;
344        }}
345        {group_booleans}
346        break;
347"""
348            s = libclinic.linear_format(s, group_booleans=lines)
349            s = s.format_map(d)
350            out.append(s)
351
352        out.append("    default:\n")
353        s = '        PyErr_SetString(PyExc_TypeError, "{} requires {} to {} arguments");\n'
354        out.append(s.format(f.full_name, count_min, count_max))
355        out.append('        goto exit;\n')
356        out.append("}")
357
358        template_dict['option_group_parsing'] = libclinic.format_escape("".join(out))
359
360    def render_function(
361        self,
362        clinic: Clinic,
363        f: Function | None
364    ) -> str:
365        if f is None:
366            return ""
367
368        codegen = clinic.codegen
369        data = CRenderData()
370
371        assert f.parameters, "We should always have a 'self' at this point!"
372        parameters = f.render_parameters
373        converters = [p.converter for p in parameters]
374
375        templates = self.output_templates(f, codegen)
376
377        f_self = parameters[0]
378        selfless = parameters[1:]
379        assert isinstance(f_self.converter, self_converter), "No self parameter in " + repr(f.full_name) + "!"
380
381        if f.critical_section:
382            match len(f.target_critical_section):
383                case 0:
384                    lock = 'Py_BEGIN_CRITICAL_SECTION({self_name});'
385                    unlock = 'Py_END_CRITICAL_SECTION();'
386                case 1:
387                    lock = 'Py_BEGIN_CRITICAL_SECTION({target_critical_section});'
388                    unlock = 'Py_END_CRITICAL_SECTION();'
389                case _:
390                    lock = 'Py_BEGIN_CRITICAL_SECTION2({target_critical_section});'
391                    unlock = 'Py_END_CRITICAL_SECTION2();'
392            data.lock.append(lock)
393            data.unlock.append(unlock)
394
395        last_group = 0
396        first_optional = len(selfless)
397        positional = selfless and selfless[-1].is_positional_only()
398        has_option_groups = False
399
400        # offset i by -1 because first_optional needs to ignore self
401        for i, p in enumerate(parameters, -1):
402            c = p.converter
403
404            if (i != -1) and (p.default is not unspecified):
405                first_optional = min(first_optional, i)
406
407            if p.is_vararg():
408                data.cleanup.append(f"Py_XDECREF({c.parser_name});")
409
410            # insert group variable
411            group = p.group
412            if last_group != group:
413                last_group = group
414                if group:
415                    group_name = self.group_to_variable_name(group)
416                    data.impl_arguments.append(group_name)
417                    data.declarations.append("int " + group_name + " = 0;")
418                    data.impl_parameters.append("int " + group_name)
419                    has_option_groups = True
420
421            c.render(p, data)
422
423        if has_option_groups and (not positional):
424            fail("You cannot use optional groups ('[' and ']') "
425                 "unless all parameters are positional-only ('/').")
426
427        # HACK
428        # when we're METH_O, but have a custom return converter,
429        # we use "impl_parameters" for the parsing function
430        # because that works better.  but that means we must
431        # suppress actually declaring the impl's parameters
432        # as variables in the parsing function.  but since it's
433        # METH_O, we have exactly one anyway, so we know exactly
434        # where it is.
435        if ("METH_O" in templates['methoddef_define'] and
436            '{impl_parameters}' in templates['parser_prototype']):
437            data.declarations.pop(0)
438
439        full_name = f.full_name
440        template_dict = {'full_name': full_name}
441        template_dict['name'] = f.displayname
442        if f.kind in {GETTER, SETTER}:
443            template_dict['getset_name'] = f.c_basename.upper()
444            template_dict['getset_basename'] = f.c_basename
445            if f.kind is GETTER:
446                template_dict['c_basename'] = f.c_basename + "_get"
447            elif f.kind is SETTER:
448                template_dict['c_basename'] = f.c_basename + "_set"
449                # Implicitly add the setter value parameter.
450                data.impl_parameters.append("PyObject *value")
451                data.impl_arguments.append("value")
452        else:
453            template_dict['methoddef_name'] = f.c_basename.upper() + "_METHODDEF"
454            template_dict['c_basename'] = f.c_basename
455
456        template_dict['docstring'] = libclinic.docstring_for_c_string(f.docstring)
457        template_dict['self_name'] = template_dict['self_type'] = template_dict['self_type_check'] = ''
458        template_dict['target_critical_section'] = ', '.join(f.target_critical_section)
459        for converter in converters:
460            converter.set_template_dict(template_dict)
461
462        if f.kind not in {SETTER, METHOD_INIT}:
463            f.return_converter.render(f, data)
464        template_dict['impl_return_type'] = f.return_converter.type
465
466        template_dict['declarations'] = libclinic.format_escape("\n".join(data.declarations))
467        template_dict['initializers'] = "\n\n".join(data.initializers)
468        template_dict['modifications'] = '\n\n'.join(data.modifications)
469        template_dict['keywords_c'] = ' '.join('"' + k + '",'
470                                               for k in data.keywords)
471        keywords = [k for k in data.keywords if k]
472        template_dict['keywords_py'] = ' '.join(c_id(k) + ','
473                                                for k in keywords)
474        template_dict['format_units'] = ''.join(data.format_units)
475        template_dict['parse_arguments'] = ', '.join(data.parse_arguments)
476        if data.parse_arguments:
477            template_dict['parse_arguments_comma'] = ',';
478        else:
479            template_dict['parse_arguments_comma'] = '';
480        template_dict['impl_parameters'] = ", ".join(data.impl_parameters)
481        template_dict['impl_arguments'] = ", ".join(data.impl_arguments)
482
483        template_dict['return_conversion'] = libclinic.format_escape("".join(data.return_conversion).rstrip())
484        template_dict['post_parsing'] = libclinic.format_escape("".join(data.post_parsing).rstrip())
485        template_dict['cleanup'] = libclinic.format_escape("".join(data.cleanup))
486
487        template_dict['return_value'] = data.return_value
488        template_dict['lock'] = "\n".join(data.lock)
489        template_dict['unlock'] = "\n".join(data.unlock)
490
491        # used by unpack tuple code generator
492        unpack_min = first_optional
493        unpack_max = len(selfless)
494        template_dict['unpack_min'] = str(unpack_min)
495        template_dict['unpack_max'] = str(unpack_max)
496
497        if has_option_groups:
498            self.render_option_group_parsing(f, template_dict,
499                                             limited_capi=codegen.limited_capi)
500
501        # buffers, not destination
502        for name, destination in clinic.destination_buffers.items():
503            template = templates[name]
504            if has_option_groups:
505                template = libclinic.linear_format(template,
506                        option_group_parsing=template_dict['option_group_parsing'])
507            template = libclinic.linear_format(template,
508                declarations=template_dict['declarations'],
509                return_conversion=template_dict['return_conversion'],
510                initializers=template_dict['initializers'],
511                modifications=template_dict['modifications'],
512                post_parsing=template_dict['post_parsing'],
513                cleanup=template_dict['cleanup'],
514                lock=template_dict['lock'],
515                unlock=template_dict['unlock'],
516                )
517
518            # Only generate the "exit:" label
519            # if we have any gotos
520            label = "exit:" if "goto exit;" in template else ""
521            template = libclinic.linear_format(template, exit_label=label)
522
523            s = template.format_map(template_dict)
524
525            # mild hack:
526            # reflow long impl declarations
527            if name in {"impl_prototype", "impl_definition"}:
528                s = libclinic.wrap_declarations(s)
529
530            if clinic.line_prefix:
531                s = libclinic.indent_all_lines(s, clinic.line_prefix)
532            if clinic.line_suffix:
533                s = libclinic.suffix_all_lines(s, clinic.line_suffix)
534
535            destination.append(s)
536
537        return clinic.get_destination('block').dump()
538