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.codegen 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})) {{{{
if (PyBytes_GET_SIZE({argname}) != 1) {{{{
PyErr_Format(PyExc_TypeError,
"{{name}}(): {displayname} must be a byte string of length 1, "
"not a bytes object of length %zd",
PyBytes_GET_SIZE({argname}));
goto exit;
}}}}
{paramname} = PyBytes_AS_STRING({argname})[0];
}}}}
else if (PyByteArray_Check({argname})) {{{{
if (PyByteArray_GET_SIZE({argname}) != 1) {{{{
PyErr_Format(PyExc_TypeError,
"{{name}}(): {displayname} must be a byte string of length 1, "
"not a bytearray object of length %zd",
PyByteArray_GET_SIZE({argname}));
goto exit;
}}}}
{paramname} = PyByteArray_AS_STRING({argname})[0];
}}}}
else {{{{
{bad_argument}
goto exit;
}}}}
""",
argname=argname,
displayname=displayname,
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) {{{{
PyErr_Format(PyExc_TypeError,
"{{name}}(): {displayname} must be a unicode character, "
"not a string of length %zd",
PyUnicode_GET_LENGTH({argname}));
goto exit;
}}}}
{paramname} = PyUnicode_READ_CHAR({argname}, 0);
""",
argname=argname,
displayname=displayname,
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
# Converters for var-positional parameter.
class VarPosCConverter(CConverter):
format_unit = ''
def parse_arg(self, argname: str, displayname: str, *, limited_capi: bool) -> str | None:
raise AssertionError('should never be called')
def parse_vararg(self, *, pos_only: int, min_pos: int, max_pos: int,
fastcall: bool, limited_capi: bool) -> str:
raise NotImplementedError
class varpos_tuple_converter(VarPosCConverter):
type = 'PyObject *'
format_unit = ''
c_default = 'NULL'
def cleanup(self) -> str:
return f"""Py_XDECREF({self.parser_name});\n"""
def parse_vararg(self, *, pos_only: int, min_pos: int, max_pos: int,
fastcall: bool, limited_capi: bool) -> str:
paramname = self.parser_name
if fastcall:
if limited_capi:
if min(pos_only, min_pos) < max_pos:
size = f'Py_MAX(nargs - {max_pos}, 0)'
else:
size = f'nargs - {max_pos}' if max_pos else 'nargs'
return f"""
{paramname} = PyTuple_New({size});
if (!{paramname}) {{{{
goto exit;
}}}}
for (Py_ssize_t i = {max_pos}; i < nargs; ++i) {{{{
PyTuple_SET_ITEM({paramname}, i - {max_pos}, Py_NewRef(args[i]));
}}}}
"""
else:
self.add_include('pycore_tuple.h', '_PyTuple_FromArray()')
start = f'args + {max_pos}' if max_pos else 'args'
size = f'nargs - {max_pos}' if max_pos else 'nargs'
if min(pos_only, min_pos) < max_pos:
return f"""
{paramname} = nargs > {max_pos}
? _PyTuple_FromArray({start}, {size})
: PyTuple_New(0);
if ({paramname} == NULL) {{{{
goto exit;
}}}}
"""
else:
return f"""
{paramname} = _PyTuple_FromArray({start}, {size});
if ({paramname} == NULL) {{{{
goto exit;
}}}}
"""
else:
if max_pos:
return f"""
{paramname} = PyTuple_GetSlice(args, {max_pos}, PY_SSIZE_T_MAX);
if (!{paramname}) {{{{
goto exit;
}}}}
"""
else:
return f"{paramname} = Py_NewRef(args);\n"
class varpos_array_converter(VarPosCConverter):
type = 'PyObject * const *'
length = True
c_ignored_default = ''
def parse_vararg(self, *, pos_only: int, min_pos: int, max_pos: int,
fastcall: bool, limited_capi: bool) -> str:
paramname = self.parser_name
if not fastcall:
self.add_include('pycore_tuple.h', '_PyTuple_ITEMS()')
start = 'args' if fastcall else '_PyTuple_ITEMS(args)'
size = 'nargs' if fastcall else 'PyTuple_GET_SIZE(args)'
if max_pos:
if min(pos_only, min_pos) < max_pos:
start = f'{size} > {max_pos} ? {start} + {max_pos} : {start}'
size = f'Py_MAX(0, {size} - {max_pos})'
else:
start = f'{start} + {max_pos}'
size = f'{size} - {max_pos}'
return f"""
{paramname} = {start};
{self.length_name} = {size};
"""