]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-113317: Argument Clinic: Add libclinic.converters module (#117315)
authorVictor Stinner <vstinner@python.org>
Tue, 2 Apr 2024 10:09:53 +0000 (12:09 +0200)
committerGitHub <noreply@github.com>
Tue, 2 Apr 2024 10:09:53 +0000 (10:09 +0000)
Move the following converter classes to libclinic.converters:

* PyByteArrayObject_converter
* PyBytesObject_converter
* Py_UNICODE_converter
* Py_buffer_converter
* Py_complex_converter
* Py_ssize_t_converter
* bool_converter
* byte_converter
* char_converter
* defining_class_converter
* double_converter
* fildes_converter
* float_converter
* int_converter
* long_converter
* long_long_converter
* object_converter
* self_converter
* short_converter
* size_t_converter
* slice_index_converter
* str_converter
* unicode_converter
* unsigned_char_converter
* unsigned_int_converter
* unsigned_long_converter
* unsigned_long_long_converter
* unsigned_short_converter

Move also the following classes to libclinic.converters:

* buffer
* robuffer
* rwbuffer

Move the following functions to libclinic.converters:

* correct_name_for_self()
* r()
* str_converter_key()

Move Null and NULL to libclinic.utils.

Lib/test/test_clinic.py
Tools/clinic/clinic.py
Tools/clinic/libclinic/__init__.py
Tools/clinic/libclinic/converter.py
Tools/clinic/libclinic/converters.py [new file with mode: 0644]
Tools/clinic/libclinic/function.py
Tools/clinic/libclinic/utils.py

index f95bf858100be6898d00291dd7e2ea80a245671a..a07641d1dfda54356a3f3132782431b87ba76d5c 100644 (file)
@@ -17,6 +17,7 @@ import unittest
 test_tools.skip_if_missing('clinic')
 with test_tools.imports_under_tool('clinic'):
     import libclinic
+    from libclinic.converters import int_converter, str_converter
     import clinic
     from clinic import DSLParser
 
@@ -924,7 +925,7 @@ class ClinicParserTest(TestCase):
         self.assertEqual(2, len(function.parameters))
         p = function.parameters['path']
         self.assertEqual('path', p.name)
-        self.assertIsInstance(p.converter, clinic.int_converter)
+        self.assertIsInstance(p.converter, int_converter)
 
     def test_param_default(self):
         function = self.parse_function("""
@@ -1023,7 +1024,7 @@ class ClinicParserTest(TestCase):
         """)
         self.assertEqual(3, len(function.parameters))
         conv = function.parameters['something_else'].converter
-        self.assertIsInstance(conv, clinic.str_converter)
+        self.assertIsInstance(conv, str_converter)
 
     def test_param_default_parameters_out_of_order(self):
         err = (
@@ -2040,7 +2041,7 @@ class ClinicParserTest(TestCase):
         block = self.parse('module os\nos.access\n   path: "s"')
         module, function = block.signatures
         conv = (function.parameters['path']).converter
-        self.assertIsInstance(conv, clinic.str_converter)
+        self.assertIsInstance(conv, str_converter)
 
     def test_legacy_converters_non_string_constant_annotation(self):
         err = "Annotations must be either a name, a function call, or a string"
index ea480e61ba9a2b16a8c2d30f3dbcaa56d163183d..a4e004d5b124d104b57914e91f40851769558c71 100755 (executable)
@@ -8,7 +8,6 @@ from __future__ import annotations
 
 import argparse
 import ast
-import builtins as bltns
 import contextlib
 import dataclasses as dc
 import enum
@@ -46,7 +45,7 @@ import libclinic
 import libclinic.cpp
 from libclinic import (
     ClinicError, Sentinels, VersionTuple,
-    fail, warn, unspecified, unknown)
+    fail, warn, unspecified, unknown, NULL)
 from libclinic.function import (
     Module, Class, Function, Parameter,
     ClassDict, ModuleDict, FunctionKind,
@@ -56,8 +55,11 @@ from libclinic.language import Language, PythonLanguage
 from libclinic.block_parser import Block, BlockParser
 from libclinic.crenderdata import CRenderData, Include, TemplateDict
 from libclinic.converter import (
-    CConverter, CConverterClassT, ConverterType,
+    CConverter, ConverterType,
     converters, legacy_converters)
+from libclinic.converters import (
+    self_converter, defining_class_converter, object_converter, buffer,
+    robuffer, rwbuffer, correct_name_for_self)
 
 
 # TODO:
@@ -77,15 +79,6 @@ from libclinic.converter import (
 LIMITED_CAPI_REGEX = re.compile(r'# *define +Py_LIMITED_API')
 
 
-# This one needs to be a distinct class, unlike the other two
-class Null:
-    def __repr__(self) -> str:
-        return '<Null>'
-
-
-NULL = Null()
-
-
 ParamTuple = tuple["Parameter", ...]
 
 
@@ -2098,24 +2091,6 @@ __xor__
 ReturnConverterType = Callable[..., "CReturnConverter"]
 
 
-def add_legacy_c_converter(
-        format_unit: str,
-        **kwargs: Any
-) -> Callable[[CConverterClassT], CConverterClassT]:
-    """
-    Adds a legacy converter.
-    """
-    def closure(f: CConverterClassT) -> CConverterClassT:
-        added_f: Callable[..., CConverter]
-        if not kwargs:
-            added_f = f
-        else:
-            added_f = functools.partial(f, **kwargs)
-        if format_unit:
-            legacy_converters[format_unit] = added_f
-        return f
-    return closure
-
 # maps strings to callables.
 # these callables must be of the form:
 #   def foo(*, ...)
@@ -2125,1179 +2100,6 @@ def add_legacy_c_converter(
 ReturnConverterDict = dict[str, ReturnConverterType]
 return_converters: ReturnConverterDict = {}
 
-TypeSet = set[bltns.type[object]]
-
-
-class bool_converter(CConverter):
-    type = 'int'
-    default_type = bool
-    format_unit = 'p'
-    c_ignored_default = '0'
-
-    def converter_init(self, *, accept: TypeSet = {object}) -> None:
-        if accept == {int}:
-            self.format_unit = 'i'
-        elif accept != {object}:
-            fail(f"bool_converter: illegal 'accept' argument {accept!r}")
-        if self.default is not unspecified and self.default is not unknown:
-            self.default = bool(self.default)
-            self.c_default = str(int(self.default))
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'i':
-            return self.format_code("""
-                {paramname} = PyLong_AsInt({argname});
-                if ({paramname} == -1 && PyErr_Occurred()) {{{{
-                    goto exit;
-                }}}}
-                """,
-                argname=argname)
-        elif self.format_unit == 'p':
-            return self.format_code("""
-                {paramname} = PyObject_IsTrue({argname});
-                if ({paramname} < 0) {{{{
-                    goto exit;
-                }}}}
-                """,
-                argname=argname)
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-class defining_class_converter(CConverter):
-    """
-    A special-case converter:
-    this is the default converter used for the defining class.
-    """
-    type = 'PyTypeObject *'
-    format_unit = ''
-    show_in_signature = False
-
-    def converter_init(self, *, type: str | None = None) -> None:
-        self.specified_type = type
-
-    def render(self, parameter: Parameter, data: CRenderData) -> None:
-        self._render_self(parameter, data)
-
-    def set_template_dict(self, template_dict: TemplateDict) -> None:
-        template_dict['defining_class_name'] = self.name
-
-
-class char_converter(CConverter):
-    type = 'char'
-    default_type = (bytes, bytearray)
-    format_unit = 'c'
-    c_ignored_default = "'\0'"
-
-    def converter_init(self) -> None:
-        if isinstance(self.default, self.default_type):
-            if len(self.default) != 1:
-                fail(f"char_converter: illegal default value {self.default!r}")
-
-            self.c_default = repr(bytes(self.default))[1:]
-            if self.c_default == '"\'"':
-                self.c_default = r"'\''"
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'c':
-            return self.format_code("""
-                if (PyBytes_Check({argname}) && PyBytes_GET_SIZE({argname}) == 1) {{{{
-                    {paramname} = PyBytes_AS_STRING({argname})[0];
-                }}}}
-                else if (PyByteArray_Check({argname}) && PyByteArray_GET_SIZE({argname}) == 1) {{{{
-                    {paramname} = PyByteArray_AS_STRING({argname})[0];
-                }}}}
-                else {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'a byte string of length 1', limited_capi=limited_capi),
-            )
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-
-@add_legacy_c_converter('B', bitwise=True)
-class unsigned_char_converter(CConverter):
-    type = 'unsigned char'
-    default_type = int
-    format_unit = 'b'
-    c_ignored_default = "'\0'"
-
-    def converter_init(self, *, bitwise: bool = False) -> None:
-        if bitwise:
-            self.format_unit = 'B'
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'b':
-            return self.format_code("""
-                {{{{
-                    long ival = PyLong_AsLong({argname});
-                    if (ival == -1 && PyErr_Occurred()) {{{{
-                        goto exit;
-                    }}}}
-                    else if (ival < 0) {{{{
-                        PyErr_SetString(PyExc_OverflowError,
-                                        "unsigned byte integer is less than minimum");
-                        goto exit;
-                    }}}}
-                    else if (ival > UCHAR_MAX) {{{{
-                        PyErr_SetString(PyExc_OverflowError,
-                                        "unsigned byte integer is greater than maximum");
-                        goto exit;
-                    }}}}
-                    else {{{{
-                        {paramname} = (unsigned char) ival;
-                    }}}}
-                }}}}
-                """,
-                argname=argname)
-        elif self.format_unit == 'B':
-            return self.format_code("""
-                {{{{
-                    unsigned long ival = PyLong_AsUnsignedLongMask({argname});
-                    if (ival == (unsigned long)-1 && PyErr_Occurred()) {{{{
-                        goto exit;
-                    }}}}
-                    else {{{{
-                        {paramname} = (unsigned char) ival;
-                    }}}}
-                }}}}
-                """,
-                argname=argname)
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-class byte_converter(unsigned_char_converter): pass
-
-class short_converter(CConverter):
-    type = 'short'
-    default_type = int
-    format_unit = 'h'
-    c_ignored_default = "0"
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'h':
-            return self.format_code("""
-                {{{{
-                    long ival = PyLong_AsLong({argname});
-                    if (ival == -1 && PyErr_Occurred()) {{{{
-                        goto exit;
-                    }}}}
-                    else if (ival < SHRT_MIN) {{{{
-                        PyErr_SetString(PyExc_OverflowError,
-                                        "signed short integer is less than minimum");
-                        goto exit;
-                    }}}}
-                    else if (ival > SHRT_MAX) {{{{
-                        PyErr_SetString(PyExc_OverflowError,
-                                        "signed short integer is greater than maximum");
-                        goto exit;
-                    }}}}
-                    else {{{{
-                        {paramname} = (short) ival;
-                    }}}}
-                }}}}
-                """,
-                argname=argname)
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-class unsigned_short_converter(CConverter):
-    type = 'unsigned short'
-    default_type = int
-    c_ignored_default = "0"
-
-    def converter_init(self, *, bitwise: bool = False) -> None:
-        if bitwise:
-            self.format_unit = 'H'
-        else:
-            self.converter = '_PyLong_UnsignedShort_Converter'
-
-    def use_converter(self) -> None:
-        if self.converter == '_PyLong_UnsignedShort_Converter':
-            self.add_include('pycore_long.h',
-                             '_PyLong_UnsignedShort_Converter()')
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'H':
-            return self.format_code("""
-                {paramname} = (unsigned short)PyLong_AsUnsignedLongMask({argname});
-                if ({paramname} == (unsigned short)-1 && PyErr_Occurred()) {{{{
-                    goto exit;
-                }}}}
-                """,
-                argname=argname)
-        if not limited_capi:
-            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-        # NOTE: Raises OverflowError for negative integer.
-        return self.format_code("""
-            {{{{
-                unsigned long uval = PyLong_AsUnsignedLong({argname});
-                if (uval == (unsigned long)-1 && PyErr_Occurred()) {{{{
-                    goto exit;
-                }}}}
-                if (uval > USHRT_MAX) {{{{
-                    PyErr_SetString(PyExc_OverflowError,
-                                    "Python int too large for C unsigned short");
-                    goto exit;
-                }}}}
-                {paramname} = (unsigned short) uval;
-            }}}}
-            """,
-            argname=argname)
-
-@add_legacy_c_converter('C', accept={str})
-class int_converter(CConverter):
-    type = 'int'
-    default_type = int
-    format_unit = 'i'
-    c_ignored_default = "0"
-
-    def converter_init(
-        self, *, accept: TypeSet = {int}, type: str | None = None
-    ) -> None:
-        if accept == {str}:
-            self.format_unit = 'C'
-        elif accept != {int}:
-            fail(f"int_converter: illegal 'accept' argument {accept!r}")
-        if type is not None:
-            self.type = type
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'i':
-            return self.format_code("""
-                {paramname} = PyLong_AsInt({argname});
-                if ({paramname} == -1 && PyErr_Occurred()) {{{{
-                    goto exit;
-                }}}}
-                """,
-                argname=argname)
-        elif self.format_unit == 'C':
-            return self.format_code("""
-                if (!PyUnicode_Check({argname})) {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-                if (PyUnicode_GET_LENGTH({argname}) != 1) {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-                {paramname} = PyUnicode_READ_CHAR({argname}, 0);
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'a unicode character', limited_capi=limited_capi),
-            )
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-class unsigned_int_converter(CConverter):
-    type = 'unsigned int'
-    default_type = int
-    c_ignored_default = "0"
-
-    def converter_init(self, *, bitwise: bool = False) -> None:
-        if bitwise:
-            self.format_unit = 'I'
-        else:
-            self.converter = '_PyLong_UnsignedInt_Converter'
-
-    def use_converter(self) -> None:
-        if self.converter == '_PyLong_UnsignedInt_Converter':
-            self.add_include('pycore_long.h',
-                             '_PyLong_UnsignedInt_Converter()')
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'I':
-            return self.format_code("""
-                {paramname} = (unsigned int)PyLong_AsUnsignedLongMask({argname});
-                if ({paramname} == (unsigned int)-1 && PyErr_Occurred()) {{{{
-                    goto exit;
-                }}}}
-                """,
-                argname=argname)
-        if not limited_capi:
-            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-        # NOTE: Raises OverflowError for negative integer.
-        return self.format_code("""
-            {{{{
-                unsigned long uval = PyLong_AsUnsignedLong({argname});
-                if (uval == (unsigned long)-1 && PyErr_Occurred()) {{{{
-                    goto exit;
-                }}}}
-                if (uval > UINT_MAX) {{{{
-                    PyErr_SetString(PyExc_OverflowError,
-                                    "Python int too large for C unsigned int");
-                    goto exit;
-                }}}}
-                {paramname} = (unsigned int) uval;
-            }}}}
-            """,
-            argname=argname)
-
-class long_converter(CConverter):
-    type = 'long'
-    default_type = int
-    format_unit = 'l'
-    c_ignored_default = "0"
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'l':
-            return self.format_code("""
-                {paramname} = PyLong_AsLong({argname});
-                if ({paramname} == -1 && PyErr_Occurred()) {{{{
-                    goto exit;
-                }}}}
-                """,
-                argname=argname)
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-class unsigned_long_converter(CConverter):
-    type = 'unsigned long'
-    default_type = int
-    c_ignored_default = "0"
-
-    def converter_init(self, *, bitwise: bool = False) -> None:
-        if bitwise:
-            self.format_unit = 'k'
-        else:
-            self.converter = '_PyLong_UnsignedLong_Converter'
-
-    def use_converter(self) -> None:
-        if self.converter == '_PyLong_UnsignedLong_Converter':
-            self.add_include('pycore_long.h',
-                             '_PyLong_UnsignedLong_Converter()')
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'k':
-            return self.format_code("""
-                if (!PyLong_Check({argname})) {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-                {paramname} = PyLong_AsUnsignedLongMask({argname});
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'int', limited_capi=limited_capi),
-            )
-        if not limited_capi:
-            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-        # NOTE: Raises OverflowError for negative integer.
-        return self.format_code("""
-            {paramname} = PyLong_AsUnsignedLong({argname});
-            if ({paramname} == (unsigned long)-1 && PyErr_Occurred()) {{{{
-                goto exit;
-            }}}}
-            """,
-            argname=argname)
-
-class long_long_converter(CConverter):
-    type = 'long long'
-    default_type = int
-    format_unit = 'L'
-    c_ignored_default = "0"
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'L':
-            return self.format_code("""
-                {paramname} = PyLong_AsLongLong({argname});
-                if ({paramname} == -1 && PyErr_Occurred()) {{{{
-                    goto exit;
-                }}}}
-                """,
-                argname=argname)
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-class unsigned_long_long_converter(CConverter):
-    type = 'unsigned long long'
-    default_type = int
-    c_ignored_default = "0"
-
-    def converter_init(self, *, bitwise: bool = False) -> None:
-        if bitwise:
-            self.format_unit = 'K'
-        else:
-            self.converter = '_PyLong_UnsignedLongLong_Converter'
-
-    def use_converter(self) -> None:
-        if self.converter == '_PyLong_UnsignedLongLong_Converter':
-            self.add_include('pycore_long.h',
-                             '_PyLong_UnsignedLongLong_Converter()')
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'K':
-            return self.format_code("""
-                if (!PyLong_Check({argname})) {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-                {paramname} = PyLong_AsUnsignedLongLongMask({argname});
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'int', limited_capi=limited_capi),
-            )
-        if not limited_capi:
-            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-        # NOTE: Raises OverflowError for negative integer.
-        return self.format_code("""
-            {paramname} = PyLong_AsUnsignedLongLong({argname});
-            if ({paramname} == (unsigned long long)-1 && PyErr_Occurred()) {{{{
-                goto exit;
-            }}}}
-            """,
-            argname=argname)
-
-class Py_ssize_t_converter(CConverter):
-    type = 'Py_ssize_t'
-    c_ignored_default = "0"
-
-    def converter_init(self, *, accept: TypeSet = {int}) -> None:
-        if accept == {int}:
-            self.format_unit = 'n'
-            self.default_type = int
-        elif accept == {int, NoneType}:
-            self.converter = '_Py_convert_optional_to_ssize_t'
-        else:
-            fail(f"Py_ssize_t_converter: illegal 'accept' argument {accept!r}")
-
-    def use_converter(self) -> None:
-        if self.converter == '_Py_convert_optional_to_ssize_t':
-            self.add_include('pycore_abstract.h',
-                             '_Py_convert_optional_to_ssize_t()')
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'n':
-            if limited_capi:
-                PyNumber_Index = 'PyNumber_Index'
-            else:
-                PyNumber_Index = '_PyNumber_Index'
-                self.add_include('pycore_abstract.h', '_PyNumber_Index()')
-            return self.format_code("""
-                {{{{
-                    Py_ssize_t ival = -1;
-                    PyObject *iobj = {PyNumber_Index}({argname});
-                    if (iobj != NULL) {{{{
-                        ival = PyLong_AsSsize_t(iobj);
-                        Py_DECREF(iobj);
-                    }}}}
-                    if (ival == -1 && PyErr_Occurred()) {{{{
-                        goto exit;
-                    }}}}
-                    {paramname} = ival;
-                }}}}
-                """,
-                argname=argname,
-                PyNumber_Index=PyNumber_Index)
-        if not limited_capi:
-            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-        return self.format_code("""
-            if ({argname} != Py_None) {{{{
-                if (PyIndex_Check({argname})) {{{{
-                    {paramname} = PyNumber_AsSsize_t({argname}, PyExc_OverflowError);
-                    if ({paramname} == -1 && PyErr_Occurred()) {{{{
-                        goto exit;
-                    }}}}
-                }}}}
-                else {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-            }}}}
-            """,
-            argname=argname,
-            bad_argument=self.bad_argument(displayname, 'integer or None', limited_capi=limited_capi),
-        )
-
-
-class slice_index_converter(CConverter):
-    type = 'Py_ssize_t'
-
-    def converter_init(self, *, accept: TypeSet = {int, NoneType}) -> None:
-        if accept == {int}:
-            self.converter = '_PyEval_SliceIndexNotNone'
-            self.nullable = False
-        elif accept == {int, NoneType}:
-            self.converter = '_PyEval_SliceIndex'
-            self.nullable = True
-        else:
-            fail(f"slice_index_converter: illegal 'accept' argument {accept!r}")
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if not limited_capi:
-            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-        if self.nullable:
-            return self.format_code("""
-                if (!Py_IsNone({argname})) {{{{
-                    if (PyIndex_Check({argname})) {{{{
-                        {paramname} = PyNumber_AsSsize_t({argname}, NULL);
-                        if ({paramname} == -1 && PyErr_Occurred()) {{{{
-                            return 0;
-                        }}}}
-                    }}}}
-                    else {{{{
-                        PyErr_SetString(PyExc_TypeError,
-                                        "slice indices must be integers or "
-                                        "None or have an __index__ method");
-                        goto exit;
-                    }}}}
-                }}}}
-                """,
-                argname=argname)
-        else:
-            return self.format_code("""
-                if (PyIndex_Check({argname})) {{{{
-                    {paramname} = PyNumber_AsSsize_t({argname}, NULL);
-                    if ({paramname} == -1 && PyErr_Occurred()) {{{{
-                        goto exit;
-                    }}}}
-                }}}}
-                else {{{{
-                    PyErr_SetString(PyExc_TypeError,
-                                    "slice indices must be integers or "
-                                    "have an __index__ method");
-                    goto exit;
-                }}}}
-                """,
-                argname=argname)
-
-class size_t_converter(CConverter):
-    type = 'size_t'
-    converter = '_PyLong_Size_t_Converter'
-    c_ignored_default = "0"
-
-    def use_converter(self) -> None:
-        self.add_include('pycore_long.h',
-                         '_PyLong_Size_t_Converter()')
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'n':
-            return self.format_code("""
-                {paramname} = PyNumber_AsSsize_t({argname}, PyExc_OverflowError);
-                if ({paramname} == -1 && PyErr_Occurred()) {{{{
-                    goto exit;
-                }}}}
-                """,
-                argname=argname)
-        if not limited_capi:
-            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-        # NOTE: Raises OverflowError for negative integer.
-        return self.format_code("""
-            {paramname} = PyLong_AsSize_t({argname});
-            if ({paramname} == (size_t)-1 && PyErr_Occurred()) {{{{
-                goto exit;
-            }}}}
-            """,
-            argname=argname)
-
-
-class fildes_converter(CConverter):
-    type = 'int'
-    converter = '_PyLong_FileDescriptor_Converter'
-
-    def use_converter(self) -> None:
-        self.add_include('pycore_fileutils.h',
-                         '_PyLong_FileDescriptor_Converter()')
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        return self.format_code("""
-            {paramname} = PyObject_AsFileDescriptor({argname});
-            if ({paramname} < 0) {{{{
-                goto exit;
-            }}}}
-            """,
-            argname=argname)
-
-
-class float_converter(CConverter):
-    type = 'float'
-    default_type = float
-    format_unit = 'f'
-    c_ignored_default = "0.0"
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'f':
-            if not limited_capi:
-                return self.format_code("""
-                    if (PyFloat_CheckExact({argname})) {{{{
-                        {paramname} = (float) (PyFloat_AS_DOUBLE({argname}));
-                    }}}}
-                    else
-                    {{{{
-                        {paramname} = (float) PyFloat_AsDouble({argname});
-                        if ({paramname} == -1.0 && PyErr_Occurred()) {{{{
-                            goto exit;
-                        }}}}
-                    }}}}
-                    """,
-                    argname=argname)
-            else:
-                return self.format_code("""
-                    {paramname} = (float) PyFloat_AsDouble({argname});
-                    if ({paramname} == -1.0 && PyErr_Occurred()) {{{{
-                        goto exit;
-                    }}}}
-                    """,
-                    argname=argname)
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-class double_converter(CConverter):
-    type = 'double'
-    default_type = float
-    format_unit = 'd'
-    c_ignored_default = "0.0"
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'd':
-            if not limited_capi:
-                return self.format_code("""
-                    if (PyFloat_CheckExact({argname})) {{{{
-                        {paramname} = PyFloat_AS_DOUBLE({argname});
-                    }}}}
-                    else
-                    {{{{
-                        {paramname} = PyFloat_AsDouble({argname});
-                        if ({paramname} == -1.0 && PyErr_Occurred()) {{{{
-                            goto exit;
-                        }}}}
-                    }}}}
-                    """,
-                    argname=argname)
-            else:
-                return self.format_code("""
-                    {paramname} = PyFloat_AsDouble({argname});
-                    if ({paramname} == -1.0 && PyErr_Occurred()) {{{{
-                        goto exit;
-                    }}}}
-                    """,
-                    argname=argname)
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-
-class Py_complex_converter(CConverter):
-    type = 'Py_complex'
-    default_type = complex
-    format_unit = 'D'
-    c_ignored_default = "{0.0, 0.0}"
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'D':
-            return self.format_code("""
-                {paramname} = PyComplex_AsCComplex({argname});
-                if (PyErr_Occurred()) {{{{
-                    goto exit;
-                }}}}
-                """,
-                argname=argname)
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-
-class object_converter(CConverter):
-    type = 'PyObject *'
-    format_unit = 'O'
-
-    def converter_init(
-            self, *,
-            converter: str | None = None,
-            type: str | None = None,
-            subclass_of: str | None = None
-    ) -> None:
-        if converter:
-            if subclass_of:
-                fail("object: Cannot pass in both 'converter' and 'subclass_of'")
-            self.format_unit = 'O&'
-            self.converter = converter
-        elif subclass_of:
-            self.format_unit = 'O!'
-            self.subclass_of = subclass_of
-
-        if type is not None:
-            self.type = type
-
-
-#
-# We define three conventions for buffer types in the 'accept' argument:
-#
-#  buffer  : any object supporting the buffer interface
-#  rwbuffer: any object supporting the buffer interface, but must be writeable
-#  robuffer: any object supporting the buffer interface, but must not be writeable
-#
-
-class buffer: pass
-class rwbuffer: pass
-class robuffer: pass
-
-StrConverterKeyType = tuple[frozenset[type[object]], bool, bool]
-
-def str_converter_key(
-    types: TypeSet, encoding: bool | str | None, zeroes: bool
-) -> StrConverterKeyType:
-    return (frozenset(types), bool(encoding), bool(zeroes))
-
-str_converter_argument_map: dict[StrConverterKeyType, str] = {}
-
-class str_converter(CConverter):
-    type = 'const char *'
-    default_type = (str, Null, NoneType)
-    format_unit = 's'
-
-    def converter_init(
-            self,
-            *,
-            accept: TypeSet = {str},
-            encoding: str | None = None,
-            zeroes: bool = False
-    ) -> None:
-
-        key = str_converter_key(accept, encoding, zeroes)
-        format_unit = str_converter_argument_map.get(key)
-        if not format_unit:
-            fail("str_converter: illegal combination of arguments", key)
-
-        self.format_unit = format_unit
-        self.length = bool(zeroes)
-        if encoding:
-            if self.default not in (Null, None, unspecified):
-                fail("str_converter: Argument Clinic doesn't support default values for encoded strings")
-            self.encoding = encoding
-            self.type = 'char *'
-            # sorry, clinic can't support preallocated buffers
-            # for es# and et#
-            self.c_default = "NULL"
-        if NoneType in accept and self.c_default == "Py_None":
-            self.c_default = "NULL"
-
-    def post_parsing(self) -> str:
-        if self.encoding:
-            name = self.name
-            return f"PyMem_FREE({name});\n"
-        else:
-            return ""
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 's':
-            return self.format_code("""
-                if (!PyUnicode_Check({argname})) {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-                Py_ssize_t {length_name};
-                {paramname} = PyUnicode_AsUTF8AndSize({argname}, &{length_name});
-                if ({paramname} == NULL) {{{{
-                    goto exit;
-                }}}}
-                if (strlen({paramname}) != (size_t){length_name}) {{{{
-                    PyErr_SetString(PyExc_ValueError, "embedded null character");
-                    goto exit;
-                }}}}
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'str', limited_capi=limited_capi),
-                length_name=self.length_name)
-        if self.format_unit == 'z':
-            return self.format_code("""
-                if ({argname} == Py_None) {{{{
-                    {paramname} = NULL;
-                }}}}
-                else if (PyUnicode_Check({argname})) {{{{
-                    Py_ssize_t {length_name};
-                    {paramname} = PyUnicode_AsUTF8AndSize({argname}, &{length_name});
-                    if ({paramname} == NULL) {{{{
-                        goto exit;
-                    }}}}
-                    if (strlen({paramname}) != (size_t){length_name}) {{{{
-                        PyErr_SetString(PyExc_ValueError, "embedded null character");
-                        goto exit;
-                    }}}}
-                }}}}
-                else {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'str or None', limited_capi=limited_capi),
-                length_name=self.length_name)
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-#
-# This is the fourth or fifth rewrite of registering all the
-# string converter format units.  Previous approaches hid
-# bugs--generally mismatches between the semantics of the format
-# unit and the arguments necessary to represent those semantics
-# properly.  Hopefully with this approach we'll get it 100% right.
-#
-# The r() function (short for "register") both registers the
-# mapping from arguments to format unit *and* registers the
-# legacy C converter for that format unit.
-#
-def r(format_unit: str,
-      *,
-      accept: TypeSet,
-      encoding: bool = False,
-      zeroes: bool = False
-) -> None:
-    if not encoding and format_unit != 's':
-        # add the legacy c converters here too.
-        #
-        # note: add_legacy_c_converter can't work for
-        #   es, es#, et, or et#
-        #   because of their extra encoding argument
-        #
-        # also don't add the converter for 's' because
-        # the metaclass for CConverter adds it for us.
-        kwargs: dict[str, Any] = {}
-        if accept != {str}:
-            kwargs['accept'] = accept
-        if zeroes:
-            kwargs['zeroes'] = True
-        added_f = functools.partial(str_converter, **kwargs)
-        legacy_converters[format_unit] = added_f
-
-    d = str_converter_argument_map
-    key = str_converter_key(accept, encoding, zeroes)
-    if key in d:
-        sys.exit("Duplicate keys specified for str_converter_argument_map!")
-    d[key] = format_unit
-
-r('es',  encoding=True,              accept={str})
-r('es#', encoding=True, zeroes=True, accept={str})
-r('et',  encoding=True,              accept={bytes, bytearray, str})
-r('et#', encoding=True, zeroes=True, accept={bytes, bytearray, str})
-r('s',                               accept={str})
-r('s#',                 zeroes=True, accept={robuffer, str})
-r('y',                               accept={robuffer})
-r('y#',                 zeroes=True, accept={robuffer})
-r('z',                               accept={str, NoneType})
-r('z#',                 zeroes=True, accept={robuffer, str, NoneType})
-del r
-
-
-class PyBytesObject_converter(CConverter):
-    type = 'PyBytesObject *'
-    format_unit = 'S'
-    # accept = {bytes}
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'S':
-            return self.format_code("""
-                if (!PyBytes_Check({argname})) {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-                {paramname} = ({type}){argname};
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'bytes', limited_capi=limited_capi),
-                type=self.type)
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-class PyByteArrayObject_converter(CConverter):
-    type = 'PyByteArrayObject *'
-    format_unit = 'Y'
-    # accept = {bytearray}
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'Y':
-            return self.format_code("""
-                if (!PyByteArray_Check({argname})) {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-                {paramname} = ({type}){argname};
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'bytearray', limited_capi=limited_capi),
-                type=self.type)
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-class unicode_converter(CConverter):
-    type = 'PyObject *'
-    default_type = (str, Null, NoneType)
-    format_unit = 'U'
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if self.format_unit == 'U':
-            return self.format_code("""
-                if (!PyUnicode_Check({argname})) {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-                {paramname} = {argname};
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'str', limited_capi=limited_capi),
-            )
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-@add_legacy_c_converter('u')
-@add_legacy_c_converter('u#', zeroes=True)
-@add_legacy_c_converter('Z', accept={str, NoneType})
-@add_legacy_c_converter('Z#', accept={str, NoneType}, zeroes=True)
-class Py_UNICODE_converter(CConverter):
-    type = 'const wchar_t *'
-    default_type = (str, Null, NoneType)
-
-    def converter_init(
-            self, *,
-            accept: TypeSet = {str},
-            zeroes: bool = False
-    ) -> None:
-        format_unit = 'Z' if accept=={str, NoneType} else 'u'
-        if zeroes:
-            format_unit += '#'
-            self.length = True
-            self.format_unit = format_unit
-        else:
-            self.accept = accept
-            if accept == {str}:
-                self.converter = '_PyUnicode_WideCharString_Converter'
-            elif accept == {str, NoneType}:
-                self.converter = '_PyUnicode_WideCharString_Opt_Converter'
-            else:
-                fail(f"Py_UNICODE_converter: illegal 'accept' argument {accept!r}")
-        self.c_default = "NULL"
-
-    def cleanup(self) -> str:
-        if self.length:
-            return ""
-        else:
-            return f"""PyMem_Free((void *){self.parser_name});\n"""
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        if not self.length:
-            if self.accept == {str}:
-                return self.format_code("""
-                    if (!PyUnicode_Check({argname})) {{{{
-                        {bad_argument}
-                        goto exit;
-                    }}}}
-                    {paramname} = PyUnicode_AsWideCharString({argname}, NULL);
-                    if ({paramname} == NULL) {{{{
-                        goto exit;
-                    }}}}
-                    """,
-                    argname=argname,
-                    bad_argument=self.bad_argument(displayname, 'str', limited_capi=limited_capi),
-                )
-            elif self.accept == {str, NoneType}:
-                return self.format_code("""
-                    if ({argname} == Py_None) {{{{
-                        {paramname} = NULL;
-                    }}}}
-                    else if (PyUnicode_Check({argname})) {{{{
-                        {paramname} = PyUnicode_AsWideCharString({argname}, NULL);
-                        if ({paramname} == NULL) {{{{
-                            goto exit;
-                        }}}}
-                    }}}}
-                    else {{{{
-                        {bad_argument}
-                        goto exit;
-                    }}}}
-                    """,
-                    argname=argname,
-                    bad_argument=self.bad_argument(displayname, 'str or None', limited_capi=limited_capi),
-                )
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-@add_legacy_c_converter('s*', accept={str, buffer})
-@add_legacy_c_converter('z*', accept={str, buffer, NoneType})
-@add_legacy_c_converter('w*', accept={rwbuffer})
-class Py_buffer_converter(CConverter):
-    type = 'Py_buffer'
-    format_unit = 'y*'
-    impl_by_reference = True
-    c_ignored_default = "{NULL, NULL}"
-
-    def converter_init(self, *, accept: TypeSet = {buffer}) -> None:
-        if self.default not in (unspecified, None):
-            fail("The only legal default value for Py_buffer is None.")
-
-        self.c_default = self.c_ignored_default
-
-        if accept == {str, buffer, NoneType}:
-            format_unit = 'z*'
-        elif accept == {str, buffer}:
-            format_unit = 's*'
-        elif accept == {buffer}:
-            format_unit = 'y*'
-        elif accept == {rwbuffer}:
-            format_unit = 'w*'
-        else:
-            fail("Py_buffer_converter: illegal combination of arguments")
-
-        self.format_unit = format_unit
-
-    def cleanup(self) -> str:
-        name = self.name
-        return "".join(["if (", name, ".obj) {\n   PyBuffer_Release(&", name, ");\n}\n"])
-
-    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
-        # PyBUF_SIMPLE guarantees that the format units of the buffers are C-contiguous.
-        if self.format_unit == 'y*':
-            return self.format_code("""
-                if (PyObject_GetBuffer({argname}, &{paramname}, PyBUF_SIMPLE) != 0) {{{{
-                    goto exit;
-                }}}}
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'contiguous buffer', limited_capi=limited_capi),
-            )
-        elif self.format_unit == 's*':
-            return self.format_code("""
-                if (PyUnicode_Check({argname})) {{{{
-                    Py_ssize_t len;
-                    const char *ptr = PyUnicode_AsUTF8AndSize({argname}, &len);
-                    if (ptr == NULL) {{{{
-                        goto exit;
-                    }}}}
-                    if (PyBuffer_FillInfo(&{paramname}, {argname}, (void *)ptr, len, 1, PyBUF_SIMPLE) < 0) {{{{
-                        goto exit;
-                    }}}}
-                }}}}
-                else {{{{ /* any bytes-like object */
-                    if (PyObject_GetBuffer({argname}, &{paramname}, PyBUF_SIMPLE) != 0) {{{{
-                        goto exit;
-                    }}}}
-                }}}}
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'contiguous buffer', limited_capi=limited_capi),
-            )
-        elif self.format_unit == 'w*':
-            return self.format_code("""
-                if (PyObject_GetBuffer({argname}, &{paramname}, PyBUF_WRITABLE) < 0) {{{{
-                    {bad_argument}
-                    goto exit;
-                }}}}
-                """,
-                argname=argname,
-                bad_argument=self.bad_argument(displayname, 'read-write bytes-like object', limited_capi=limited_capi),
-                bad_argument2=self.bad_argument(displayname, 'contiguous buffer', limited_capi=limited_capi),
-            )
-        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
-
-
-def correct_name_for_self(
-        f: Function
-) -> tuple[str, str]:
-    if f.kind in {CALLABLE, METHOD_INIT, GETTER, SETTER}:
-        if f.cls:
-            return "PyObject *", "self"
-        return "PyObject *", "module"
-    if f.kind is STATIC_METHOD:
-        return "void *", "null"
-    if f.kind in (CLASS_METHOD, METHOD_NEW):
-        return "PyTypeObject *", "type"
-    raise AssertionError(f"Unhandled type of function f: {f.kind!r}")
-
-
-class self_converter(CConverter):
-    """
-    A special-case converter:
-    this is the default converter used for "self".
-    """
-    type: str | None = None
-    format_unit = ''
-
-    def converter_init(self, *, type: str | None = None) -> None:
-        self.specified_type = type
-
-    def pre_render(self) -> None:
-        f = self.function
-        default_type, default_name = correct_name_for_self(f)
-        self.signature_name = default_name
-        self.type = self.specified_type or self.type or default_type
-
-        kind = self.function.kind
-
-        if kind is STATIC_METHOD or kind.new_or_init:
-            self.show_in_signature = False
-
-    # tp_new (METHOD_NEW) functions are of type newfunc:
-    #     typedef PyObject *(*newfunc)(PyTypeObject *, PyObject *, PyObject *);
-    #
-    # tp_init (METHOD_INIT) functions are of type initproc:
-    #     typedef int (*initproc)(PyObject *, PyObject *, PyObject *);
-    #
-    # All other functions generated by Argument Clinic are stored in
-    # PyMethodDef structures, in the ml_meth slot, which is of type PyCFunction:
-    #     typedef PyObject *(*PyCFunction)(PyObject *, PyObject *);
-    # However!  We habitually cast these functions to PyCFunction,
-    # since functions that accept keyword arguments don't fit this signature
-    # but are stored there anyway.  So strict type equality isn't important
-    # for these functions.
-    #
-    # So:
-    #
-    # * The name of the first parameter to the impl and the parsing function will always
-    #   be self.name.
-    #
-    # * The type of the first parameter to the impl will always be of self.type.
-    #
-    # * If the function is neither tp_new (METHOD_NEW) nor tp_init (METHOD_INIT):
-    #   * The type of the first parameter to the parsing function is also self.type.
-    #     This means that if you step into the parsing function, your "self" parameter
-    #     is of the correct type, which may make debugging more pleasant.
-    #
-    # * Else if the function is tp_new (METHOD_NEW):
-    #   * The type of the first parameter to the parsing function is "PyTypeObject *",
-    #     so the type signature of the function call is an exact match.
-    #   * If self.type != "PyTypeObject *", we cast the first parameter to self.type
-    #     in the impl call.
-    #
-    # * Else if the function is tp_init (METHOD_INIT):
-    #   * The type of the first parameter to the parsing function is "PyObject *",
-    #     so the type signature of the function call is an exact match.
-    #   * If self.type != "PyObject *", we cast the first parameter to self.type
-    #     in the impl call.
-
-    @property
-    def parser_type(self) -> str:
-        assert self.type is not None
-        if self.function.kind in {METHOD_INIT, METHOD_NEW, STATIC_METHOD, CLASS_METHOD}:
-            tp, _ = correct_name_for_self(self.function)
-            return tp
-        return self.type
-
-    def render(self, parameter: Parameter, data: CRenderData) -> None:
-        """
-        parameter is a clinic.Parameter instance.
-        data is a CRenderData instance.
-        """
-        if self.function.kind is STATIC_METHOD:
-            return
-
-        self._render_self(parameter, data)
-
-        if self.type != self.parser_type:
-            # insert cast to impl_argument[0], aka self.
-            # we know we're in the first slot in all the CRenderData lists,
-            # because we render parameters in order, and self is always first.
-            assert len(data.impl_arguments) == 1
-            assert data.impl_arguments[0] == self.name
-            assert self.type is not None
-            data.impl_arguments[0] = '(' + self.type + ")" + data.impl_arguments[0]
-
-    def set_template_dict(self, template_dict: TemplateDict) -> None:
-        template_dict['self_name'] = self.name
-        template_dict['self_type'] = self.parser_type
-        kind = self.function.kind
-        cls = self.function.cls
-
-        if kind.new_or_init and cls and cls.typedef:
-            if kind is METHOD_NEW:
-                type_check = (
-                    '({0} == base_tp || {0}->tp_init == base_tp->tp_init)'
-                 ).format(self.name)
-            else:
-                type_check = ('(Py_IS_TYPE({0}, base_tp) ||\n        '
-                              ' Py_TYPE({0})->tp_new == base_tp->tp_new)'
-                             ).format(self.name)
-
-            line = f'{type_check} &&\n        '
-            template_dict['self_type_check'] = line
-
-            type_object = cls.type_object
-            type_ptr = f'PyTypeObject *base_tp = {type_object};'
-            template_dict['base_type_ptr'] = type_ptr
-
 
 def add_c_return_converter(
         f: ReturnConverterType,
index 32231b82bfdc07694512d02e93d28fa9ee73455c..7c5cede23966777edeed90f511db2e78492002d2 100644 (file)
@@ -25,13 +25,15 @@ from .identifiers import (
 )
 from .utils import (
     FormatCounterFormatter,
+    NULL,
+    Null,
+    Sentinels,
+    VersionTuple,
     compute_checksum,
     create_regex,
-    write_file,
-    VersionTuple,
-    Sentinels,
-    unspecified,
     unknown,
+    unspecified,
+    write_file,
 )
 
 
@@ -61,13 +63,15 @@ __all__ = [
 
     # Utility functions
     "FormatCounterFormatter",
+    "NULL",
+    "Null",
+    "Sentinels",
+    "VersionTuple",
     "compute_checksum",
     "create_regex",
-    "write_file",
-    "VersionTuple",
-    "Sentinels",
-    "unspecified",
     "unknown",
+    "unspecified",
+    "write_file",
 ]
 
 
index 744d03c2c1fea4ee93498569fab322000ec425ca..ac78be3f7958da1921075617e2adaaedb0e868e0 100644 (file)
@@ -531,3 +531,19 @@ converters: ConverterDict = {}
 # these callables follow the same rules as those for "converters" above.
 # note however that they will never be called with keyword-only parameters.
 legacy_converters: ConverterDict = {}
+
+
+def add_legacy_c_converter(
+    format_unit: str,
+    **kwargs: Any
+) -> Callable[[CConverterClassT], CConverterClassT]:
+    def closure(f: CConverterClassT) -> CConverterClassT:
+        added_f: Callable[..., CConverter]
+        if not kwargs:
+            added_f = f
+        else:
+            added_f = functools.partial(f, **kwargs)
+        if format_unit:
+            legacy_converters[format_unit] = added_f
+        return f
+    return closure
diff --git a/Tools/clinic/libclinic/converters.py b/Tools/clinic/libclinic/converters.py
new file mode 100644 (file)
index 0000000..7fc16f1
--- /dev/null
@@ -0,0 +1,1211 @@
+import builtins as bltns
+import functools
+import sys
+from types import NoneType
+from typing import Any
+
+from libclinic import fail, Null, unspecified, unknown
+from libclinic.function import (
+    Function, Parameter,
+    CALLABLE, STATIC_METHOD, CLASS_METHOD, METHOD_INIT, METHOD_NEW,
+    GETTER, SETTER)
+from libclinic.crenderdata import CRenderData, TemplateDict
+from libclinic.converter import (
+    CConverter, legacy_converters, add_legacy_c_converter)
+
+
+TypeSet = set[bltns.type[object]]
+
+
+class bool_converter(CConverter):
+    type = 'int'
+    default_type = bool
+    format_unit = 'p'
+    c_ignored_default = '0'
+
+    def converter_init(self, *, accept: TypeSet = {object}) -> None:
+        if accept == {int}:
+            self.format_unit = 'i'
+        elif accept != {object}:
+            fail(f"bool_converter: illegal 'accept' argument {accept!r}")
+        if self.default is not unspecified and self.default is not unknown:
+            self.default = bool(self.default)
+            self.c_default = str(int(self.default))
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'i':
+            return self.format_code("""
+                {paramname} = PyLong_AsInt({argname});
+                if ({paramname} == -1 && PyErr_Occurred()) {{{{
+                    goto exit;
+                }}}}
+                """,
+                argname=argname)
+        elif self.format_unit == 'p':
+            return self.format_code("""
+                {paramname} = PyObject_IsTrue({argname});
+                if ({paramname} < 0) {{{{
+                    goto exit;
+                }}}}
+                """,
+                argname=argname)
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+class defining_class_converter(CConverter):
+    """
+    A special-case converter:
+    this is the default converter used for the defining class.
+    """
+    type = 'PyTypeObject *'
+    format_unit = ''
+    show_in_signature = False
+
+    def converter_init(self, *, type: str | None = None) -> None:
+        self.specified_type = type
+
+    def render(self, parameter: Parameter, data: CRenderData) -> None:
+        self._render_self(parameter, data)
+
+    def set_template_dict(self, template_dict: TemplateDict) -> None:
+        template_dict['defining_class_name'] = self.name
+
+
+class char_converter(CConverter):
+    type = 'char'
+    default_type = (bytes, bytearray)
+    format_unit = 'c'
+    c_ignored_default = "'\0'"
+
+    def converter_init(self) -> None:
+        if isinstance(self.default, self.default_type):
+            if len(self.default) != 1:
+                fail(f"char_converter: illegal default value {self.default!r}")
+
+            self.c_default = repr(bytes(self.default))[1:]
+            if self.c_default == '"\'"':
+                self.c_default = r"'\''"
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'c':
+            return self.format_code("""
+                if (PyBytes_Check({argname}) && PyBytes_GET_SIZE({argname}) == 1) {{{{
+                    {paramname} = PyBytes_AS_STRING({argname})[0];
+                }}}}
+                else if (PyByteArray_Check({argname}) && PyByteArray_GET_SIZE({argname}) == 1) {{{{
+                    {paramname} = PyByteArray_AS_STRING({argname})[0];
+                }}}}
+                else {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'a byte string of length 1', limited_capi=limited_capi),
+            )
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+@add_legacy_c_converter('B', bitwise=True)
+class unsigned_char_converter(CConverter):
+    type = 'unsigned char'
+    default_type = int
+    format_unit = 'b'
+    c_ignored_default = "'\0'"
+
+    def converter_init(self, *, bitwise: bool = False) -> None:
+        if bitwise:
+            self.format_unit = 'B'
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'b':
+            return self.format_code("""
+                {{{{
+                    long ival = PyLong_AsLong({argname});
+                    if (ival == -1 && PyErr_Occurred()) {{{{
+                        goto exit;
+                    }}}}
+                    else if (ival < 0) {{{{
+                        PyErr_SetString(PyExc_OverflowError,
+                                        "unsigned byte integer is less than minimum");
+                        goto exit;
+                    }}}}
+                    else if (ival > UCHAR_MAX) {{{{
+                        PyErr_SetString(PyExc_OverflowError,
+                                        "unsigned byte integer is greater than maximum");
+                        goto exit;
+                    }}}}
+                    else {{{{
+                        {paramname} = (unsigned char) ival;
+                    }}}}
+                }}}}
+                """,
+                argname=argname)
+        elif self.format_unit == 'B':
+            return self.format_code("""
+                {{{{
+                    unsigned long ival = PyLong_AsUnsignedLongMask({argname});
+                    if (ival == (unsigned long)-1 && PyErr_Occurred()) {{{{
+                        goto exit;
+                    }}}}
+                    else {{{{
+                        {paramname} = (unsigned char) ival;
+                    }}}}
+                }}}}
+                """,
+                argname=argname)
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+class byte_converter(unsigned_char_converter):
+    pass
+
+
+class short_converter(CConverter):
+    type = 'short'
+    default_type = int
+    format_unit = 'h'
+    c_ignored_default = "0"
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'h':
+            return self.format_code("""
+                {{{{
+                    long ival = PyLong_AsLong({argname});
+                    if (ival == -1 && PyErr_Occurred()) {{{{
+                        goto exit;
+                    }}}}
+                    else if (ival < SHRT_MIN) {{{{
+                        PyErr_SetString(PyExc_OverflowError,
+                                        "signed short integer is less than minimum");
+                        goto exit;
+                    }}}}
+                    else if (ival > SHRT_MAX) {{{{
+                        PyErr_SetString(PyExc_OverflowError,
+                                        "signed short integer is greater than maximum");
+                        goto exit;
+                    }}}}
+                    else {{{{
+                        {paramname} = (short) ival;
+                    }}}}
+                }}}}
+                """,
+                argname=argname)
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+class unsigned_short_converter(CConverter):
+    type = 'unsigned short'
+    default_type = int
+    c_ignored_default = "0"
+
+    def converter_init(self, *, bitwise: bool = False) -> None:
+        if bitwise:
+            self.format_unit = 'H'
+        else:
+            self.converter = '_PyLong_UnsignedShort_Converter'
+
+    def use_converter(self) -> None:
+        if self.converter == '_PyLong_UnsignedShort_Converter':
+            self.add_include('pycore_long.h',
+                             '_PyLong_UnsignedShort_Converter()')
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'H':
+            return self.format_code("""
+                {paramname} = (unsigned short)PyLong_AsUnsignedLongMask({argname});
+                if ({paramname} == (unsigned short)-1 && PyErr_Occurred()) {{{{
+                    goto exit;
+                }}}}
+                """,
+                argname=argname)
+        if not limited_capi:
+            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+        # NOTE: Raises OverflowError for negative integer.
+        return self.format_code("""
+            {{{{
+                unsigned long uval = PyLong_AsUnsignedLong({argname});
+                if (uval == (unsigned long)-1 && PyErr_Occurred()) {{{{
+                    goto exit;
+                }}}}
+                if (uval > USHRT_MAX) {{{{
+                    PyErr_SetString(PyExc_OverflowError,
+                                    "Python int too large for C unsigned short");
+                    goto exit;
+                }}}}
+                {paramname} = (unsigned short) uval;
+            }}}}
+            """,
+            argname=argname)
+
+
+@add_legacy_c_converter('C', accept={str})
+class int_converter(CConverter):
+    type = 'int'
+    default_type = int
+    format_unit = 'i'
+    c_ignored_default = "0"
+
+    def converter_init(
+        self, *, accept: TypeSet = {int}, type: str | None = None
+    ) -> None:
+        if accept == {str}:
+            self.format_unit = 'C'
+        elif accept != {int}:
+            fail(f"int_converter: illegal 'accept' argument {accept!r}")
+        if type is not None:
+            self.type = type
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'i':
+            return self.format_code("""
+                {paramname} = PyLong_AsInt({argname});
+                if ({paramname} == -1 && PyErr_Occurred()) {{{{
+                    goto exit;
+                }}}}
+                """,
+                argname=argname)
+        elif self.format_unit == 'C':
+            return self.format_code("""
+                if (!PyUnicode_Check({argname})) {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+                if (PyUnicode_GET_LENGTH({argname}) != 1) {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+                {paramname} = PyUnicode_READ_CHAR({argname}, 0);
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'a unicode character', limited_capi=limited_capi),
+            )
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+class unsigned_int_converter(CConverter):
+    type = 'unsigned int'
+    default_type = int
+    c_ignored_default = "0"
+
+    def converter_init(self, *, bitwise: bool = False) -> None:
+        if bitwise:
+            self.format_unit = 'I'
+        else:
+            self.converter = '_PyLong_UnsignedInt_Converter'
+
+    def use_converter(self) -> None:
+        if self.converter == '_PyLong_UnsignedInt_Converter':
+            self.add_include('pycore_long.h',
+                             '_PyLong_UnsignedInt_Converter()')
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'I':
+            return self.format_code("""
+                {paramname} = (unsigned int)PyLong_AsUnsignedLongMask({argname});
+                if ({paramname} == (unsigned int)-1 && PyErr_Occurred()) {{{{
+                    goto exit;
+                }}}}
+                """,
+                argname=argname)
+        if not limited_capi:
+            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+        # NOTE: Raises OverflowError for negative integer.
+        return self.format_code("""
+            {{{{
+                unsigned long uval = PyLong_AsUnsignedLong({argname});
+                if (uval == (unsigned long)-1 && PyErr_Occurred()) {{{{
+                    goto exit;
+                }}}}
+                if (uval > UINT_MAX) {{{{
+                    PyErr_SetString(PyExc_OverflowError,
+                                    "Python int too large for C unsigned int");
+                    goto exit;
+                }}}}
+                {paramname} = (unsigned int) uval;
+            }}}}
+            """,
+            argname=argname)
+
+
+class long_converter(CConverter):
+    type = 'long'
+    default_type = int
+    format_unit = 'l'
+    c_ignored_default = "0"
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'l':
+            return self.format_code("""
+                {paramname} = PyLong_AsLong({argname});
+                if ({paramname} == -1 && PyErr_Occurred()) {{{{
+                    goto exit;
+                }}}}
+                """,
+                argname=argname)
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+class unsigned_long_converter(CConverter):
+    type = 'unsigned long'
+    default_type = int
+    c_ignored_default = "0"
+
+    def converter_init(self, *, bitwise: bool = False) -> None:
+        if bitwise:
+            self.format_unit = 'k'
+        else:
+            self.converter = '_PyLong_UnsignedLong_Converter'
+
+    def use_converter(self) -> None:
+        if self.converter == '_PyLong_UnsignedLong_Converter':
+            self.add_include('pycore_long.h',
+                             '_PyLong_UnsignedLong_Converter()')
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'k':
+            return self.format_code("""
+                if (!PyLong_Check({argname})) {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+                {paramname} = PyLong_AsUnsignedLongMask({argname});
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'int', limited_capi=limited_capi),
+            )
+        if not limited_capi:
+            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+        # NOTE: Raises OverflowError for negative integer.
+        return self.format_code("""
+            {paramname} = PyLong_AsUnsignedLong({argname});
+            if ({paramname} == (unsigned long)-1 && PyErr_Occurred()) {{{{
+                goto exit;
+            }}}}
+            """,
+            argname=argname)
+
+
+class long_long_converter(CConverter):
+    type = 'long long'
+    default_type = int
+    format_unit = 'L'
+    c_ignored_default = "0"
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'L':
+            return self.format_code("""
+                {paramname} = PyLong_AsLongLong({argname});
+                if ({paramname} == -1 && PyErr_Occurred()) {{{{
+                    goto exit;
+                }}}}
+                """,
+                argname=argname)
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+class unsigned_long_long_converter(CConverter):
+    type = 'unsigned long long'
+    default_type = int
+    c_ignored_default = "0"
+
+    def converter_init(self, *, bitwise: bool = False) -> None:
+        if bitwise:
+            self.format_unit = 'K'
+        else:
+            self.converter = '_PyLong_UnsignedLongLong_Converter'
+
+    def use_converter(self) -> None:
+        if self.converter == '_PyLong_UnsignedLongLong_Converter':
+            self.add_include('pycore_long.h',
+                             '_PyLong_UnsignedLongLong_Converter()')
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'K':
+            return self.format_code("""
+                if (!PyLong_Check({argname})) {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+                {paramname} = PyLong_AsUnsignedLongLongMask({argname});
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'int', limited_capi=limited_capi),
+            )
+        if not limited_capi:
+            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+        # NOTE: Raises OverflowError for negative integer.
+        return self.format_code("""
+            {paramname} = PyLong_AsUnsignedLongLong({argname});
+            if ({paramname} == (unsigned long long)-1 && PyErr_Occurred()) {{{{
+                goto exit;
+            }}}}
+            """,
+            argname=argname)
+
+
+class Py_ssize_t_converter(CConverter):
+    type = 'Py_ssize_t'
+    c_ignored_default = "0"
+
+    def converter_init(self, *, accept: TypeSet = {int}) -> None:
+        if accept == {int}:
+            self.format_unit = 'n'
+            self.default_type = int
+        elif accept == {int, NoneType}:
+            self.converter = '_Py_convert_optional_to_ssize_t'
+        else:
+            fail(f"Py_ssize_t_converter: illegal 'accept' argument {accept!r}")
+
+    def use_converter(self) -> None:
+        if self.converter == '_Py_convert_optional_to_ssize_t':
+            self.add_include('pycore_abstract.h',
+                             '_Py_convert_optional_to_ssize_t()')
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'n':
+            if limited_capi:
+                PyNumber_Index = 'PyNumber_Index'
+            else:
+                PyNumber_Index = '_PyNumber_Index'
+                self.add_include('pycore_abstract.h', '_PyNumber_Index()')
+            return self.format_code("""
+                {{{{
+                    Py_ssize_t ival = -1;
+                    PyObject *iobj = {PyNumber_Index}({argname});
+                    if (iobj != NULL) {{{{
+                        ival = PyLong_AsSsize_t(iobj);
+                        Py_DECREF(iobj);
+                    }}}}
+                    if (ival == -1 && PyErr_Occurred()) {{{{
+                        goto exit;
+                    }}}}
+                    {paramname} = ival;
+                }}}}
+                """,
+                argname=argname,
+                PyNumber_Index=PyNumber_Index)
+        if not limited_capi:
+            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+        return self.format_code("""
+            if ({argname} != Py_None) {{{{
+                if (PyIndex_Check({argname})) {{{{
+                    {paramname} = PyNumber_AsSsize_t({argname}, PyExc_OverflowError);
+                    if ({paramname} == -1 && PyErr_Occurred()) {{{{
+                        goto exit;
+                    }}}}
+                }}}}
+                else {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+            }}}}
+            """,
+            argname=argname,
+            bad_argument=self.bad_argument(displayname, 'integer or None', limited_capi=limited_capi),
+        )
+
+
+class slice_index_converter(CConverter):
+    type = 'Py_ssize_t'
+
+    def converter_init(self, *, accept: TypeSet = {int, NoneType}) -> None:
+        if accept == {int}:
+            self.converter = '_PyEval_SliceIndexNotNone'
+            self.nullable = False
+        elif accept == {int, NoneType}:
+            self.converter = '_PyEval_SliceIndex'
+            self.nullable = True
+        else:
+            fail(f"slice_index_converter: illegal 'accept' argument {accept!r}")
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if not limited_capi:
+            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+        if self.nullable:
+            return self.format_code("""
+                if (!Py_IsNone({argname})) {{{{
+                    if (PyIndex_Check({argname})) {{{{
+                        {paramname} = PyNumber_AsSsize_t({argname}, NULL);
+                        if ({paramname} == -1 && PyErr_Occurred()) {{{{
+                            return 0;
+                        }}}}
+                    }}}}
+                    else {{{{
+                        PyErr_SetString(PyExc_TypeError,
+                                        "slice indices must be integers or "
+                                        "None or have an __index__ method");
+                        goto exit;
+                    }}}}
+                }}}}
+                """,
+                argname=argname)
+        else:
+            return self.format_code("""
+                if (PyIndex_Check({argname})) {{{{
+                    {paramname} = PyNumber_AsSsize_t({argname}, NULL);
+                    if ({paramname} == -1 && PyErr_Occurred()) {{{{
+                        goto exit;
+                    }}}}
+                }}}}
+                else {{{{
+                    PyErr_SetString(PyExc_TypeError,
+                                    "slice indices must be integers or "
+                                    "have an __index__ method");
+                    goto exit;
+                }}}}
+                """,
+                argname=argname)
+
+
+class size_t_converter(CConverter):
+    type = 'size_t'
+    converter = '_PyLong_Size_t_Converter'
+    c_ignored_default = "0"
+
+    def use_converter(self) -> None:
+        self.add_include('pycore_long.h',
+                         '_PyLong_Size_t_Converter()')
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'n':
+            return self.format_code("""
+                {paramname} = PyNumber_AsSsize_t({argname}, PyExc_OverflowError);
+                if ({paramname} == -1 && PyErr_Occurred()) {{{{
+                    goto exit;
+                }}}}
+                """,
+                argname=argname)
+        if not limited_capi:
+            return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+        # NOTE: Raises OverflowError for negative integer.
+        return self.format_code("""
+            {paramname} = PyLong_AsSize_t({argname});
+            if ({paramname} == (size_t)-1 && PyErr_Occurred()) {{{{
+                goto exit;
+            }}}}
+            """,
+            argname=argname)
+
+
+class fildes_converter(CConverter):
+    type = 'int'
+    converter = '_PyLong_FileDescriptor_Converter'
+
+    def use_converter(self) -> None:
+        self.add_include('pycore_fileutils.h',
+                         '_PyLong_FileDescriptor_Converter()')
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        return self.format_code("""
+            {paramname} = PyObject_AsFileDescriptor({argname});
+            if ({paramname} < 0) {{{{
+                goto exit;
+            }}}}
+            """,
+            argname=argname)
+
+
+class float_converter(CConverter):
+    type = 'float'
+    default_type = float
+    format_unit = 'f'
+    c_ignored_default = "0.0"
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'f':
+            if not limited_capi:
+                return self.format_code("""
+                    if (PyFloat_CheckExact({argname})) {{{{
+                        {paramname} = (float) (PyFloat_AS_DOUBLE({argname}));
+                    }}}}
+                    else
+                    {{{{
+                        {paramname} = (float) PyFloat_AsDouble({argname});
+                        if ({paramname} == -1.0 && PyErr_Occurred()) {{{{
+                            goto exit;
+                        }}}}
+                    }}}}
+                    """,
+                    argname=argname)
+            else:
+                return self.format_code("""
+                    {paramname} = (float) PyFloat_AsDouble({argname});
+                    if ({paramname} == -1.0 && PyErr_Occurred()) {{{{
+                        goto exit;
+                    }}}}
+                    """,
+                    argname=argname)
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+class double_converter(CConverter):
+    type = 'double'
+    default_type = float
+    format_unit = 'd'
+    c_ignored_default = "0.0"
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'd':
+            if not limited_capi:
+                return self.format_code("""
+                    if (PyFloat_CheckExact({argname})) {{{{
+                        {paramname} = PyFloat_AS_DOUBLE({argname});
+                    }}}}
+                    else
+                    {{{{
+                        {paramname} = PyFloat_AsDouble({argname});
+                        if ({paramname} == -1.0 && PyErr_Occurred()) {{{{
+                            goto exit;
+                        }}}}
+                    }}}}
+                    """,
+                    argname=argname)
+            else:
+                return self.format_code("""
+                    {paramname} = PyFloat_AsDouble({argname});
+                    if ({paramname} == -1.0 && PyErr_Occurred()) {{{{
+                        goto exit;
+                    }}}}
+                    """,
+                    argname=argname)
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+class Py_complex_converter(CConverter):
+    type = 'Py_complex'
+    default_type = complex
+    format_unit = 'D'
+    c_ignored_default = "{0.0, 0.0}"
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'D':
+            return self.format_code("""
+                {paramname} = PyComplex_AsCComplex({argname});
+                if (PyErr_Occurred()) {{{{
+                    goto exit;
+                }}}}
+                """,
+                argname=argname)
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+class object_converter(CConverter):
+    type = 'PyObject *'
+    format_unit = 'O'
+
+    def converter_init(
+            self, *,
+            converter: str | None = None,
+            type: str | None = None,
+            subclass_of: str | None = None
+    ) -> None:
+        if converter:
+            if subclass_of:
+                fail("object: Cannot pass in both 'converter' and 'subclass_of'")
+            self.format_unit = 'O&'
+            self.converter = converter
+        elif subclass_of:
+            self.format_unit = 'O!'
+            self.subclass_of = subclass_of
+
+        if type is not None:
+            self.type = type
+
+
+#
+# We define three conventions for buffer types in the 'accept' argument:
+#
+#  buffer  : any object supporting the buffer interface
+#  rwbuffer: any object supporting the buffer interface, but must be writeable
+#  robuffer: any object supporting the buffer interface, but must not be writeable
+#
+
+class buffer:
+    pass
+class rwbuffer:
+    pass
+class robuffer:
+    pass
+
+
+StrConverterKeyType = tuple[frozenset[type[object]], bool, bool]
+
+def str_converter_key(
+    types: TypeSet, encoding: bool | str | None, zeroes: bool
+) -> StrConverterKeyType:
+    return (frozenset(types), bool(encoding), bool(zeroes))
+
+str_converter_argument_map: dict[StrConverterKeyType, str] = {}
+
+
+class str_converter(CConverter):
+    type = 'const char *'
+    default_type = (str, Null, NoneType)
+    format_unit = 's'
+
+    def converter_init(
+            self,
+            *,
+            accept: TypeSet = {str},
+            encoding: str | None = None,
+            zeroes: bool = False
+    ) -> None:
+
+        key = str_converter_key(accept, encoding, zeroes)
+        format_unit = str_converter_argument_map.get(key)
+        if not format_unit:
+            fail("str_converter: illegal combination of arguments", key)
+
+        self.format_unit = format_unit
+        self.length = bool(zeroes)
+        if encoding:
+            if self.default not in (Null, None, unspecified):
+                fail("str_converter: Argument Clinic doesn't support default values for encoded strings")
+            self.encoding = encoding
+            self.type = 'char *'
+            # sorry, clinic can't support preallocated buffers
+            # for es# and et#
+            self.c_default = "NULL"
+        if NoneType in accept and self.c_default == "Py_None":
+            self.c_default = "NULL"
+
+    def post_parsing(self) -> str:
+        if self.encoding:
+            name = self.name
+            return f"PyMem_FREE({name});\n"
+        else:
+            return ""
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 's':
+            return self.format_code("""
+                if (!PyUnicode_Check({argname})) {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+                Py_ssize_t {length_name};
+                {paramname} = PyUnicode_AsUTF8AndSize({argname}, &{length_name});
+                if ({paramname} == NULL) {{{{
+                    goto exit;
+                }}}}
+                if (strlen({paramname}) != (size_t){length_name}) {{{{
+                    PyErr_SetString(PyExc_ValueError, "embedded null character");
+                    goto exit;
+                }}}}
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'str', limited_capi=limited_capi),
+                length_name=self.length_name)
+        if self.format_unit == 'z':
+            return self.format_code("""
+                if ({argname} == Py_None) {{{{
+                    {paramname} = NULL;
+                }}}}
+                else if (PyUnicode_Check({argname})) {{{{
+                    Py_ssize_t {length_name};
+                    {paramname} = PyUnicode_AsUTF8AndSize({argname}, &{length_name});
+                    if ({paramname} == NULL) {{{{
+                        goto exit;
+                    }}}}
+                    if (strlen({paramname}) != (size_t){length_name}) {{{{
+                        PyErr_SetString(PyExc_ValueError, "embedded null character");
+                        goto exit;
+                    }}}}
+                }}}}
+                else {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'str or None', limited_capi=limited_capi),
+                length_name=self.length_name)
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+#
+# This is the fourth or fifth rewrite of registering all the
+# string converter format units.  Previous approaches hid
+# bugs--generally mismatches between the semantics of the format
+# unit and the arguments necessary to represent those semantics
+# properly.  Hopefully with this approach we'll get it 100% right.
+#
+# The r() function (short for "register") both registers the
+# mapping from arguments to format unit *and* registers the
+# legacy C converter for that format unit.
+#
+def r(format_unit: str,
+      *,
+      accept: TypeSet,
+      encoding: bool = False,
+      zeroes: bool = False
+) -> None:
+    if not encoding and format_unit != 's':
+        # add the legacy c converters here too.
+        #
+        # note: add_legacy_c_converter can't work for
+        #   es, es#, et, or et#
+        #   because of their extra encoding argument
+        #
+        # also don't add the converter for 's' because
+        # the metaclass for CConverter adds it for us.
+        kwargs: dict[str, Any] = {}
+        if accept != {str}:
+            kwargs['accept'] = accept
+        if zeroes:
+            kwargs['zeroes'] = True
+        added_f = functools.partial(str_converter, **kwargs)
+        legacy_converters[format_unit] = added_f
+
+    d = str_converter_argument_map
+    key = str_converter_key(accept, encoding, zeroes)
+    if key in d:
+        sys.exit("Duplicate keys specified for str_converter_argument_map!")
+    d[key] = format_unit
+
+r('es',  encoding=True,              accept={str})
+r('es#', encoding=True, zeroes=True, accept={str})
+r('et',  encoding=True,              accept={bytes, bytearray, str})
+r('et#', encoding=True, zeroes=True, accept={bytes, bytearray, str})
+r('s',                               accept={str})
+r('s#',                 zeroes=True, accept={robuffer, str})
+r('y',                               accept={robuffer})
+r('y#',                 zeroes=True, accept={robuffer})
+r('z',                               accept={str, NoneType})
+r('z#',                 zeroes=True, accept={robuffer, str, NoneType})
+del r
+
+
+class PyBytesObject_converter(CConverter):
+    type = 'PyBytesObject *'
+    format_unit = 'S'
+    # accept = {bytes}
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'S':
+            return self.format_code("""
+                if (!PyBytes_Check({argname})) {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+                {paramname} = ({type}){argname};
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'bytes', limited_capi=limited_capi),
+                type=self.type)
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+class PyByteArrayObject_converter(CConverter):
+    type = 'PyByteArrayObject *'
+    format_unit = 'Y'
+    # accept = {bytearray}
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'Y':
+            return self.format_code("""
+                if (!PyByteArray_Check({argname})) {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+                {paramname} = ({type}){argname};
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'bytearray', limited_capi=limited_capi),
+                type=self.type)
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+class unicode_converter(CConverter):
+    type = 'PyObject *'
+    default_type = (str, Null, NoneType)
+    format_unit = 'U'
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if self.format_unit == 'U':
+            return self.format_code("""
+                if (!PyUnicode_Check({argname})) {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+                {paramname} = {argname};
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'str', limited_capi=limited_capi),
+            )
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+@add_legacy_c_converter('u')
+@add_legacy_c_converter('u#', zeroes=True)
+@add_legacy_c_converter('Z', accept={str, NoneType})
+@add_legacy_c_converter('Z#', accept={str, NoneType}, zeroes=True)
+class Py_UNICODE_converter(CConverter):
+    type = 'const wchar_t *'
+    default_type = (str, Null, NoneType)
+
+    def converter_init(
+            self, *,
+            accept: TypeSet = {str},
+            zeroes: bool = False
+    ) -> None:
+        format_unit = 'Z' if accept=={str, NoneType} else 'u'
+        if zeroes:
+            format_unit += '#'
+            self.length = True
+            self.format_unit = format_unit
+        else:
+            self.accept = accept
+            if accept == {str}:
+                self.converter = '_PyUnicode_WideCharString_Converter'
+            elif accept == {str, NoneType}:
+                self.converter = '_PyUnicode_WideCharString_Opt_Converter'
+            else:
+                fail(f"Py_UNICODE_converter: illegal 'accept' argument {accept!r}")
+        self.c_default = "NULL"
+
+    def cleanup(self) -> str:
+        if self.length:
+            return ""
+        else:
+            return f"""PyMem_Free((void *){self.parser_name});\n"""
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        if not self.length:
+            if self.accept == {str}:
+                return self.format_code("""
+                    if (!PyUnicode_Check({argname})) {{{{
+                        {bad_argument}
+                        goto exit;
+                    }}}}
+                    {paramname} = PyUnicode_AsWideCharString({argname}, NULL);
+                    if ({paramname} == NULL) {{{{
+                        goto exit;
+                    }}}}
+                    """,
+                    argname=argname,
+                    bad_argument=self.bad_argument(displayname, 'str', limited_capi=limited_capi),
+                )
+            elif self.accept == {str, NoneType}:
+                return self.format_code("""
+                    if ({argname} == Py_None) {{{{
+                        {paramname} = NULL;
+                    }}}}
+                    else if (PyUnicode_Check({argname})) {{{{
+                        {paramname} = PyUnicode_AsWideCharString({argname}, NULL);
+                        if ({paramname} == NULL) {{{{
+                            goto exit;
+                        }}}}
+                    }}}}
+                    else {{{{
+                        {bad_argument}
+                        goto exit;
+                    }}}}
+                    """,
+                    argname=argname,
+                    bad_argument=self.bad_argument(displayname, 'str or None', limited_capi=limited_capi),
+                )
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+@add_legacy_c_converter('s*', accept={str, buffer})
+@add_legacy_c_converter('z*', accept={str, buffer, NoneType})
+@add_legacy_c_converter('w*', accept={rwbuffer})
+class Py_buffer_converter(CConverter):
+    type = 'Py_buffer'
+    format_unit = 'y*'
+    impl_by_reference = True
+    c_ignored_default = "{NULL, NULL}"
+
+    def converter_init(self, *, accept: TypeSet = {buffer}) -> None:
+        if self.default not in (unspecified, None):
+            fail("The only legal default value for Py_buffer is None.")
+
+        self.c_default = self.c_ignored_default
+
+        if accept == {str, buffer, NoneType}:
+            format_unit = 'z*'
+        elif accept == {str, buffer}:
+            format_unit = 's*'
+        elif accept == {buffer}:
+            format_unit = 'y*'
+        elif accept == {rwbuffer}:
+            format_unit = 'w*'
+        else:
+            fail("Py_buffer_converter: illegal combination of arguments")
+
+        self.format_unit = format_unit
+
+    def cleanup(self) -> str:
+        name = self.name
+        return "".join(["if (", name, ".obj) {\n   PyBuffer_Release(&", name, ");\n}\n"])
+
+    def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
+        # PyBUF_SIMPLE guarantees that the format units of the buffers are C-contiguous.
+        if self.format_unit == 'y*':
+            return self.format_code("""
+                if (PyObject_GetBuffer({argname}, &{paramname}, PyBUF_SIMPLE) != 0) {{{{
+                    goto exit;
+                }}}}
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'contiguous buffer', limited_capi=limited_capi),
+            )
+        elif self.format_unit == 's*':
+            return self.format_code("""
+                if (PyUnicode_Check({argname})) {{{{
+                    Py_ssize_t len;
+                    const char *ptr = PyUnicode_AsUTF8AndSize({argname}, &len);
+                    if (ptr == NULL) {{{{
+                        goto exit;
+                    }}}}
+                    if (PyBuffer_FillInfo(&{paramname}, {argname}, (void *)ptr, len, 1, PyBUF_SIMPLE) < 0) {{{{
+                        goto exit;
+                    }}}}
+                }}}}
+                else {{{{ /* any bytes-like object */
+                    if (PyObject_GetBuffer({argname}, &{paramname}, PyBUF_SIMPLE) != 0) {{{{
+                        goto exit;
+                    }}}}
+                }}}}
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'contiguous buffer', limited_capi=limited_capi),
+            )
+        elif self.format_unit == 'w*':
+            return self.format_code("""
+                if (PyObject_GetBuffer({argname}, &{paramname}, PyBUF_WRITABLE) < 0) {{{{
+                    {bad_argument}
+                    goto exit;
+                }}}}
+                """,
+                argname=argname,
+                bad_argument=self.bad_argument(displayname, 'read-write bytes-like object', limited_capi=limited_capi),
+                bad_argument2=self.bad_argument(displayname, 'contiguous buffer', limited_capi=limited_capi),
+            )
+        return super().parse_arg(argname, displayname, limited_capi=limited_capi)
+
+
+def correct_name_for_self(
+        f: Function
+) -> tuple[str, str]:
+    if f.kind in {CALLABLE, METHOD_INIT, GETTER, SETTER}:
+        if f.cls:
+            return "PyObject *", "self"
+        return "PyObject *", "module"
+    if f.kind is STATIC_METHOD:
+        return "void *", "null"
+    if f.kind in (CLASS_METHOD, METHOD_NEW):
+        return "PyTypeObject *", "type"
+    raise AssertionError(f"Unhandled type of function f: {f.kind!r}")
+
+
+class self_converter(CConverter):
+    """
+    A special-case converter:
+    this is the default converter used for "self".
+    """
+    type: str | None = None
+    format_unit = ''
+
+    def converter_init(self, *, type: str | None = None) -> None:
+        self.specified_type = type
+
+    def pre_render(self) -> None:
+        f = self.function
+        default_type, default_name = correct_name_for_self(f)
+        self.signature_name = default_name
+        self.type = self.specified_type or self.type or default_type
+
+        kind = self.function.kind
+
+        if kind is STATIC_METHOD or kind.new_or_init:
+            self.show_in_signature = False
+
+    # tp_new (METHOD_NEW) functions are of type newfunc:
+    #     typedef PyObject *(*newfunc)(PyTypeObject *, PyObject *, PyObject *);
+    #
+    # tp_init (METHOD_INIT) functions are of type initproc:
+    #     typedef int (*initproc)(PyObject *, PyObject *, PyObject *);
+    #
+    # All other functions generated by Argument Clinic are stored in
+    # PyMethodDef structures, in the ml_meth slot, which is of type PyCFunction:
+    #     typedef PyObject *(*PyCFunction)(PyObject *, PyObject *);
+    # However!  We habitually cast these functions to PyCFunction,
+    # since functions that accept keyword arguments don't fit this signature
+    # but are stored there anyway.  So strict type equality isn't important
+    # for these functions.
+    #
+    # So:
+    #
+    # * The name of the first parameter to the impl and the parsing function will always
+    #   be self.name.
+    #
+    # * The type of the first parameter to the impl will always be of self.type.
+    #
+    # * If the function is neither tp_new (METHOD_NEW) nor tp_init (METHOD_INIT):
+    #   * The type of the first parameter to the parsing function is also self.type.
+    #     This means that if you step into the parsing function, your "self" parameter
+    #     is of the correct type, which may make debugging more pleasant.
+    #
+    # * Else if the function is tp_new (METHOD_NEW):
+    #   * The type of the first parameter to the parsing function is "PyTypeObject *",
+    #     so the type signature of the function call is an exact match.
+    #   * If self.type != "PyTypeObject *", we cast the first parameter to self.type
+    #     in the impl call.
+    #
+    # * Else if the function is tp_init (METHOD_INIT):
+    #   * The type of the first parameter to the parsing function is "PyObject *",
+    #     so the type signature of the function call is an exact match.
+    #   * If self.type != "PyObject *", we cast the first parameter to self.type
+    #     in the impl call.
+
+    @property
+    def parser_type(self) -> str:
+        assert self.type is not None
+        if self.function.kind in {METHOD_INIT, METHOD_NEW, STATIC_METHOD, CLASS_METHOD}:
+            tp, _ = correct_name_for_self(self.function)
+            return tp
+        return self.type
+
+    def render(self, parameter: Parameter, data: CRenderData) -> None:
+        """
+        parameter is a clinic.Parameter instance.
+        data is a CRenderData instance.
+        """
+        if self.function.kind is STATIC_METHOD:
+            return
+
+        self._render_self(parameter, data)
+
+        if self.type != self.parser_type:
+            # insert cast to impl_argument[0], aka self.
+            # we know we're in the first slot in all the CRenderData lists,
+            # because we render parameters in order, and self is always first.
+            assert len(data.impl_arguments) == 1
+            assert data.impl_arguments[0] == self.name
+            assert self.type is not None
+            data.impl_arguments[0] = '(' + self.type + ")" + data.impl_arguments[0]
+
+    def set_template_dict(self, template_dict: TemplateDict) -> None:
+        template_dict['self_name'] = self.name
+        template_dict['self_type'] = self.parser_type
+        kind = self.function.kind
+        cls = self.function.cls
+
+        if kind.new_or_init and cls and cls.typedef:
+            if kind is METHOD_NEW:
+                type_check = (
+                    '({0} == base_tp || {0}->tp_init == base_tp->tp_init)'
+                 ).format(self.name)
+            else:
+                type_check = ('(Py_IS_TYPE({0}, base_tp) ||\n        '
+                              ' Py_TYPE({0})->tp_new == base_tp->tp_new)'
+                             ).format(self.name)
+
+            line = f'{type_check} &&\n        '
+            template_dict['self_type_check'] = line
+
+            type_object = cls.type_object
+            type_ptr = f'PyTypeObject *base_tp = {type_object};'
+            template_dict['base_type_ptr'] = type_ptr
index 4fafedb617115c8efbd4c80aa16ae66aa02d2ce9..b0dd08446e802d706ff0bf0738d9d25598c07869 100644 (file)
@@ -6,8 +6,9 @@ import functools
 import inspect
 from typing import Final, Any, TYPE_CHECKING
 if TYPE_CHECKING:
-    from clinic import Clinic, CReturnConverter, self_converter
+    from clinic import Clinic, CReturnConverter
     from libclinic.converter import CConverter
+    from libclinic.converters import self_converter
 
 from libclinic import VersionTuple, unspecified
 
index 95a69f70c5499d319f5a4c0818bf8ad219fe4b16..17e8f35be73bf421bef0fb57f49bf311266b761c 100644 (file)
@@ -82,3 +82,12 @@ class Sentinels(enum.Enum):
 
 unspecified: Final = Sentinels.unspecified
 unknown: Final = Sentinels.unknown
+
+
+# This one needs to be a distinct class, unlike the other two
+class Null:
+    def __repr__(self) -> str:
+        return '<Null>'
+
+
+NULL = Null()