From 1f17cae732f0301e71123343db2d1a1dff2982d0 Mon Sep 17 00:00:00 2001 From: Vizonex <114684698+Vizonex@users.noreply.github.com> Date: Tue, 24 Jun 2025 23:53:55 -0500 Subject: [PATCH 1/4] Upload C Version of this library --- src/propcache/_propcache_c.c | 356 +++ src/propcache/_propcache_c.pyi | 40 + src/propcache/_propcachelib/pc.h | 163 ++ .../_propcachelib/propcache_objects.h | 42 + .../_propcachelib/pythoncapi_compat.h | 2283 +++++++++++++++++ src/propcache/_propcachelib/state.h | 94 + 6 files changed, 2978 insertions(+) create mode 100644 src/propcache/_propcache_c.c create mode 100644 src/propcache/_propcache_c.pyi create mode 100644 src/propcache/_propcachelib/pc.h create mode 100644 src/propcache/_propcachelib/propcache_objects.h create mode 100644 src/propcache/_propcachelib/pythoncapi_compat.h create mode 100644 src/propcache/_propcachelib/state.h diff --git a/src/propcache/_propcache_c.c b/src/propcache/_propcache_c.c new file mode 100644 index 0000000..56b2b48 --- /dev/null +++ b/src/propcache/_propcache_c.c @@ -0,0 +1,356 @@ +/* _propcache_c.c + * + * A replacement of the cython _helper_c.pyx with a few more optimized + * parts for better care and speed... + */ + + +#include + +#include "_propcachelib/pc.h" +#include "_propcachelib/propcache_objects.h" +#include "_propcachelib/pythoncapi_compat.h" +#include "_propcachelib/state.h" + + + + +/****************** UnderCachedPropertyObject Methods ******************/ + + +static int +under_cached_property_tp_init(UnderCachedPropertyObject *self, PyObject* args, PyObject* kwds){ + static char *kwlist[] = {"wrapped", NULL}; + PyObject* wrapped = NULL; + if (!PyArg_ParseTupleAndKeywords( args, kwds, "|O", kwlist, wrapped)){ + return -1; + } + return uc_prop_init(self, wrapped); +} + +static PyObject* +under_cached_property_get__doc__(UnderCachedPropertyObject* self, PyObject *Py_UNUSED(ignored)){ + if (self->doc == NULL){ + self->doc = __propcache_get_func_doc(self->wrapped); + } + return self->doc; +}; + +static PyObject* +under_cached_property_set__doc__(UnderCachedPropertyObject* self, PyObject* new_doc){ + self->doc = new_doc; + return new_doc; +}; + + +static PyObject* +under_cached_property__get__(UnderCachedPropertyObject* self, PyObject* inst, void* Py_UNUSED(owner)){ + return uc_prop__get__(self, inst); +} + +/* under_cached_property is immutable */ + +static PyObject* +under_cached_property__set__(UnderCachedPropertyObject* self, void* Py_UNUSED(inst), void* Py_UNUSED(owner)){ + PyErr_SetString(PyExc_AttributeError, "cached property is read-only"); + return NULL; +}; + + + + + +static int +under_cached_property_tp_traverse(UnderCachedPropertyObject *self, visitproc visit, void *arg){ + Py_VISIT(Py_TYPE(self)); + Py_VISIT(self->wrapped); + Py_VISIT(self->name); + Py_VISIT(self->doc); + return 0; +} + +static int +under_cached_property_clear(UnderCachedPropertyObject *self) +{ + Py_CLEAR(self->wrapped); + Py_CLEAR(self->name); + Py_CLEAR(self->doc); + return 0; +} + +/************ UnderCachedPropertyObject Type Information ***************/ + +// TODO: Maybe consider adding __isabstractmethod__ & __name__ using PyGetSetDef + +static PyGetSetDef under_cached_property_getsetlist[] = { + {"__doc__", under_cached_property_get__doc__, under_cached_property_set__doc__, NULL, NULL}, + {0, 0, 0, 0, 0} +}; + + +PyDoc_STRVAR( + UnderCachedProperty_Doc, + "Use as a class method decorator. It operates almost exactly like" + "the Python `@property` decorator, but it puts the result of the" + "method it decorates into the instance dict after the first call," + "effectively replacing the function it decorates with an instance" + "variable. It is, in Python parlance, a data descriptor."); + + + +static PyMemberDef under_cached_property_members[] = { + { + "name", + Py_T_OBJECT_EX, + offsetof(UnderCachedPropertyObject, name), + Py_READONLY + }, + { + "wrapped", + Py_T_OBJECT_EX, + offsetof(UnderCachedPropertyObject, wrapped), + Py_READONLY, + }, + {NULL} /* Sentinel */ +}; + +static PyMethodDef under_cached_property_methods[] = { + {"__class_getitem__", (PyCFunction)Py_GenericAlias, METH_O | METH_CLASS,NULL}, + {NULL} +}; + + +static PyType_Slot under_cached_property_type_slots[] = { + {Py_tp_descr_get, under_cached_property__get__}, + {Py_tp_descr_set, under_cached_property__set__}, + {Py_tp_init, under_cached_property_tp_init}, + {Py_tp_traverse, under_cached_property_tp_traverse}, + {Py_tp_doc, UnderCachedProperty_Doc}, + {Py_tp_members, under_cached_property_members}, + {Py_tp_clear, under_cached_property_clear}, + {Py_tp_new, PyType_GenericNew}, + {Py_tp_del, PyObject_GC_Del}, + {Py_tp_methods, under_cached_property_methods}, + {Py_tp_getset, under_cached_property_getsetlist}, + {0, NULL} +}; + +static PyType_Spec UnderCachedPropertySpec = { + .basicsize = sizeof(UnderCachedPropertyObject), + .flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_HAVE_VERSION_TAG|Py_TPFLAGS_BASETYPE|Py_TPFLAGS_HAVE_GC, + .itemsize = 0, + .name = "propcache._propcache_c.under_cached_property", + .slots = under_cached_property_type_slots +}; + + + +/*************************** CachedPropertyObject **************************/ +/* An optimized version of functool's cached-property */ +/* For more info on how this object works it's recommended to see + * Either functool's version or _propcache/pc.h */ + + +static int +cached_property_tp_init(CachedPropertyObject *self, PyObject* args, PyObject* kwds){ + static char *kwlist[] = {"func", NULL}; + PyObject* func = NULL; + if (!PyArg_ParseTupleAndKeywords( args, kwds, "|O", kwlist, func)){ + return -1; + } + c_prop_init(self, func); + return 0; +} + +static int +cached_property__set_name__(CachedPropertyObject* self, PyObject *const *args, Py_ssize_t nargs){ + if (nargs < 2){ + PyErr_Format(PyExc_TypeError, "__set_name__ requires 2 positional arguments not %zu", nargs); + } + return c_prop__set_name__(self, args[0]); +} + +static PyObject* +cached_property__get__(CachedPropertyObject* self, PyObject* inst, void* Py_UNUSED(owner)){ + return c_prop__get__(self, inst); +}; + + +static PyObject* +cached_property_get__doc__(CachedPropertyObject* self, void *Py_UNUSED(ignored)){ + if (self->doc == NULL){ + self->doc = __propcache_get_func_doc(self->func); + } + return self->doc; +}; + +static PyObject* +cached_property_set__doc__(CachedPropertyObject* self, PyObject* new_doc){ + self->doc = new_doc; + return new_doc; +}; + + +static int +cached_property_tp_traverse(CachedPropertyObject *self, visitproc visit, void *arg){ + Py_VISIT(Py_TYPE(self)); + Py_VISIT(self->func); + Py_VISIT(self->name); + Py_VISIT(self->doc); + return 0; +} + + +static int +cached_property_tp_clear(CachedPropertyObject *self, visitproc visit, void *arg){ + Py_CLEAR(self->func); + Py_CLEAR(self->name); + Py_CLEAR(self->doc); + return 0; +} + + + +/************ CachedPropertyObject Type Information ***************/ + +PyDoc_STRVAR( + CachedProperty_Doc, + "Use as a class method decorator. It operates almost exactly like" + "the Python `@property` decorator, but it puts the result of the" + "method it decorates into the instance dict after the first call," + "effectively replacing the function it decorates with an instance" + "variable. It is, in Python parlance, a data descriptor." +); + + +static PyGetSetDef cached_property_getsetlist[] = { + {"__doc__", cached_property_get__doc__, cached_property_set__doc__, NULL, NULL}, + {0, 0, 0, 0, 0} +}; + +static PyMemberDef cached_property_members[] = { + { + "name", + Py_T_OBJECT_EX, + offsetof(CachedPropertyObject, name), + Py_READONLY + }, + { + "func", + Py_T_OBJECT_EX, + offsetof(CachedPropertyObject, func), + Py_READONLY, + }, + {NULL} /* Sentinel */ +}; + +static PyMethodDef cached_property_methods[] = { + {"__class_getitem__", (PyCFunction)Py_GenericAlias, METH_O | METH_CLASS,NULL}, + {"__set_name__", (PyCFunction)cached_property__set_name__, METH_FASTCALL, NULL}, + {NULL} +}; + + +static PyType_Slot cached_property_type_slots[] = { + {Py_tp_descr_get, cached_property__get__}, + // {Py_tp_descr_set, cached_property__set__}, <- TBD + {Py_tp_init, under_cached_property_tp_init}, + {Py_tp_traverse, under_cached_property_tp_traverse}, + {Py_tp_doc, (char*)CachedProperty_Doc}, + {Py_tp_members, cached_property_members}, + {Py_tp_clear, cached_property_tp_clear}, + {Py_tp_new, PyType_GenericNew}, + {Py_tp_del, PyObject_GC_Del}, + {Py_tp_methods, cached_property_methods}, + {Py_tp_getset, cached_property_getsetlist}, + {0, NULL} +}; + + + +static PyType_Spec CachedPropertySpec = { + .basicsize = sizeof(CachedPropertyObject), + .flags = Py_TPFLAGS_DEFAULT|Py_TPFLAGS_HAVE_VERSION_TAG|Py_TPFLAGS_BASETYPE|Py_TPFLAGS_HAVE_GC, + .itemsize = 0, + .name = "propcache._propcache_c.cached_property", + .slots = cached_property_type_slots +}; + + +static int +module_exec(PyObject *mod) +{ + mod_state *state = get_mod_state(mod); + state->CachedPropertyType = (PyTypeObject*)PyType_FromModuleAndSpec(mod, &CachedPropertySpec, NULL);; + if (state->CachedPropertyType == NULL){ + return -1; + } + if (PyModule_AddType(mod, state->CachedPropertyType) < 0){ + return -1; + }; + + state->UnderCachedPropertyType = (PyTypeObject*)PyType_FromModuleAndSpec(mod, &UnderCachedPropertySpec, NULL); + if (state->UnderCachedPropertyType == NULL){ + Py_CLEAR(state->CachedPropertyType); + return -1; + } + if (PyModule_AddType(mod, state->UnderCachedPropertyType) < 0){ + return -1; + }; + return 0; +} + + +static int +module_traverse(PyObject *mod, visitproc visit, void *arg) +{ + mod_state *state = get_mod_state(mod); + Py_VISIT(state->CachedPropertyType); + Py_VISIT(state->UnderCachedPropertyType); + return 0; +} + +static int +module_clear(PyObject *mod) +{ + mod_state *state = get_mod_state(mod); + Py_CLEAR(state->CachedPropertyType); + Py_CLEAR(state->UnderCachedPropertyType); + return 0; +} + +static void +module_free(void *mod) +{ + (void)module_clear((PyObject *)mod); +} + + +static struct PyModuleDef_Slot module_slots[] = { + {Py_mod_exec, module_exec}, +#if PY_VERSION_HEX >= 0x030c00f0 + {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, +#endif +#if PY_VERSION_HEX >= 0x030d00f0 + {Py_mod_gil, Py_MOD_GIL_NOT_USED}, +#endif + {0, NULL}, +}; + + + +static PyModuleDef propcache_c_module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "_propcache_c", + .m_size = sizeof(mod_state), + .m_slots = module_slots, + .m_traverse = module_traverse, + .m_clear = module_clear, + .m_free = (freefunc)module_free, +}; + +PyMODINIT_FUNC +PyInit__propcache_c(void) +{ + return PyModuleDef_Init(&propcache_c_module); +} \ No newline at end of file diff --git a/src/propcache/_propcache_c.pyi b/src/propcache/_propcache_c.pyi new file mode 100644 index 0000000..841bdf9 --- /dev/null +++ b/src/propcache/_propcache_c.pyi @@ -0,0 +1,40 @@ +from __future__ import annotations + +import sys +from typing import Any, Callable, Generic, Mapping, Protocol, Type, TypeVar, overload + +if sys.version_info >= (3, 11): + from typing import Self +else: + Self = Any + +_T = TypeVar("_T") +# We use Mapping to make it possible to use TypedDict, but this isn't +# technically type safe as we need to assign into the dict. +_Cache = TypeVar("_Cache", bound=Mapping[str, Any]) + +class _CacheImpl(Protocol[_Cache]): + _cache: _Cache + +class cached_property(Generic[_T]): + func: Callable[[Any], _T] + name: str + def __init__(self, func:Callable[[Any], _T]) -> None: ... + @classmethod + def __class_getitem__(cls, *args, **kwargs): ... + def __get__(self, instance:_T, owner:Type[Any]) -> _T: ... + def __set_name__(self, name:str, owner:Type[Any]): ... + +class under_cached_property(Generic[_T]): + name: str + wrapped: Callable[[Any], _T] + def __init__(self, wrapped: Callable[[Any], _T]) -> None: ... + @classmethod + def __class_getitem__(cls, *args, **kwargs): ... + def __delete__(self, *args, **kwargs): ... + @overload + def __get__(self, inst: None, owner: type[object] | None = None) -> Self: ... + @overload + def __get__(self, inst: _CacheImpl[Any], owner: type[object] | None = None) -> _T: ... # type: ignore[misc] + def __get__(self, inst: _CacheImpl[Any] | None, owner: type[object] | None = None): ... + def __set__(self, inst: _CacheImpl[Any] | None, value: Any): ... diff --git a/src/propcache/_propcachelib/pc.h b/src/propcache/_propcachelib/pc.h new file mode 100644 index 0000000..37c4420 --- /dev/null +++ b/src/propcache/_propcachelib/pc.h @@ -0,0 +1,163 @@ +#ifndef __PC_H__ +#define __PC_H__ + +/* PC is short for propcache in this case... */ + +#include "propcache_objects.h" +#include "pythoncapi_compat.h" +#include "state.h" +#ifdef __cplusplus +extern "C" { +#endif + + +/************* under_cached_property / cached_property methods ********************/ + +/* OTHER NOTES: + * - Made sense to not use -1 to express failure and rather handle everything in + * True/False Fashion don't see the day this gets requested to be wrapped as a c-api + * module but incase requested this is why true/false is utilized + */ + +static inline PyObject* +__propcache_get_func_doc(PyObject* func){ + PyObject* result; + PyObject_GetOptionalAttrString(func, "__doc__", &result); + if (result == NULL){ + Py_RETURN_NONE; + } + Py_INCREF(result); + return result; +} + +static inline int +__propcache_set_func_doc(PyObject* func, PyObject* doc){ + return PyObject_SetAttrString(func, "__doc__", doc); +} + +// cached / under_cached use the same caching mechanism +// so combining the two under an inlined funciton made +// perfect sense... + +static inline PyObject* +__propcache_get_value(PyObject* name, PyObject* func, PyObject* inst, const char* attr_name){ + PyObject* cache = PyObject_GetAttrString(inst, attr_name); + if (cache == NULL){ + return NULL; + } + PyObject* val = PyDict_GetItem(cache, name); + if (val == NULL){ + val = PyObject_CallOneArg(func, inst); + if (PyDict_SetItem(cache, name, val) < 0){ + return NULL; + } + } else { + Py_INCREF(val); + } + // TODO: Try Looking into returning val with Py_NewRef + return val; +} + + +/************* under_cached_property methods ********************/ + +static inline int +uc_prop_init(UnderCachedPropertyObject* self, PyObject* wrapped){ + PyObject* name = PyObject_GetAttrString(wrapped, "__name__"); + if (name == NULL){ + return 0; + } + if (!PyCallable_Check(wrapped)){ + PyErr_Format( + PyExc_TypeError, + "wrapped method named %R must be callable", + name + ); + return 0; + } + self->name = name; + self->wrapped = wrapped; + self->doc = NULL; + return 1; +} + + + + +static inline PyObject* +uc_prop__get__(UnderCachedPropertyObject* self, PyObject* inst){ + if (inst == NULL || Py_IsNone(inst)){ + return (PyObject*)self; + } + return __propcache_get_value(self->name, self->wrapped, inst, "_cache"); +} + + + + +/************************ cached_property *********************** */ + +static inline void +c_prop_init(CachedPropertyObject* self, PyObject* func){ + /* In the old cython version we used None This time + We can now utilize NULL to speedup checking in __set_name__ */ + self->func = func; + self->name = NULL; +} + + +static inline int +c_prop__set_name__(CachedPropertyObject* self, PyObject* name){ + if (self->name == NULL){ + self->name = name; + return 1; + } else { + switch (PyObject_RichCompareBool(self->name, name, Py_EQ)){ + case 1: { + /* Success */ + return 1; + } + case 0: { + /* Error */ + PyErr_Format( + PyExc_TypeError, + "Cannot assign the same cached_property to two different names %R and %R", + self->name, + name + ); + return 0; + } + + default : { + /* Error but likely for a different reason */ + return 0; + } + } + } +} + +static inline PyObject* +c_prop__get__(CachedPropertyObject* self, PyObject* inst){ + if (inst == NULL || (Py_IsNone(inst))){ + return (PyObject*)self; + } + if (self->name == NULL || (Py_IsNone(self->name))) { + PyErr_SetString( + PyExc_TypeError, + "Cannot use cached_property instance" + " without calling __set_name__ on it." + ); + return NULL; + } + /* Incase object was using __slots__ instead of __dict__ + * PyObject_GetAttrString will catch it and throw an error */ + return __propcache_get_value(self->name, self->func, inst, "__dict__"); +} + + +#ifdef __cplusplus +} +#endif + + +#endif // __PC_H__ \ No newline at end of file diff --git a/src/propcache/_propcachelib/propcache_objects.h b/src/propcache/_propcachelib/propcache_objects.h new file mode 100644 index 0000000..2d64e65 --- /dev/null +++ b/src/propcache/_propcachelib/propcache_objects.h @@ -0,0 +1,42 @@ +#ifndef __PROPCACHE_OBJECTS_H__ +#define __PROPCACHE_OBJECTS_H__ +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// IDK Yet... +// #if PY_VERSION_HEX >= 0x030c00f0 +// #define MANAGED_WEAKREFS +// #endif + + + +/****************** Propcache Objects ***************/ + +typedef struct _under_cached_property_object { + PyObject_HEAD + PyObject* wrapped; + PyObject* name; + PyObject* doc; + // TODO: Maybe implement __isabstract__ for @abc.abstractmethod() + // PyObject* isabstract; +} UnderCachedPropertyObject; + + +typedef struct _cached_property { + PyObject_HEAD + PyObject* func; + PyObject* name; + PyObject* doc; + // TBD + // PyObject* isabstract; +} CachedPropertyObject; + + +#ifdef __cplusplus +}; +#endif + +#endif // __PROPCACHE_OBJECTS_H__ \ No newline at end of file diff --git a/src/propcache/_propcachelib/pythoncapi_compat.h b/src/propcache/_propcachelib/pythoncapi_compat.h new file mode 100644 index 0000000..3320f68 --- /dev/null +++ b/src/propcache/_propcachelib/pythoncapi_compat.h @@ -0,0 +1,2283 @@ +// Header file providing new C API functions to old Python versions. +// +// File distributed under the Zero Clause BSD (0BSD) license. +// Copyright Contributors to the pythoncapi_compat project. +// +// Homepage: +// https://github.com/python/pythoncapi_compat +// +// Latest version: +// https://raw.githubusercontent.com/python/pythoncapi-compat/main/pythoncapi_compat.h +// +// SPDX-License-Identifier: 0BSD + +#ifndef PYTHONCAPI_COMPAT +#define PYTHONCAPI_COMPAT + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include // offsetof() + +// Python 3.11.0b4 added PyFrame_Back() to Python.h +#if PY_VERSION_HEX < 0x030b00B4 && !defined(PYPY_VERSION) +# include "frameobject.h" // PyFrameObject, PyFrame_GetBack() +#endif +#if PY_VERSION_HEX < 0x030C00A3 +# include // T_SHORT, READONLY +#endif + + +#ifndef _Py_CAST +# define _Py_CAST(type, expr) ((type)(expr)) +#endif + +// Static inline functions should use _Py_NULL rather than using directly NULL +// to prevent C++ compiler warnings. On C23 and newer and on C++11 and newer, +// _Py_NULL is defined as nullptr. +#ifndef _Py_NULL +# if (defined (__STDC_VERSION__) && __STDC_VERSION__ > 201710L) \ + || (defined(__cplusplus) && __cplusplus >= 201103) +# define _Py_NULL nullptr +# else +# define _Py_NULL NULL +# endif +#endif + +// Cast argument to PyObject* type. +#ifndef _PyObject_CAST +# define _PyObject_CAST(op) _Py_CAST(PyObject*, op) +#endif + +#ifndef Py_BUILD_ASSERT +# define Py_BUILD_ASSERT(cond) \ + do { \ + (void)sizeof(char [1 - 2 * !(cond)]); \ + } while(0) +#endif + + +// bpo-42262 added Py_NewRef() to Python 3.10.0a3 +#if PY_VERSION_HEX < 0x030A00A3 && !defined(Py_NewRef) +static inline PyObject* _Py_NewRef(PyObject *obj) +{ + Py_INCREF(obj); + return obj; +} +#define Py_NewRef(obj) _Py_NewRef(_PyObject_CAST(obj)) +#endif + + +// bpo-42262 added Py_XNewRef() to Python 3.10.0a3 +#if PY_VERSION_HEX < 0x030A00A3 && !defined(Py_XNewRef) +static inline PyObject* _Py_XNewRef(PyObject *obj) +{ + Py_XINCREF(obj); + return obj; +} +#define Py_XNewRef(obj) _Py_XNewRef(_PyObject_CAST(obj)) +#endif + + +// bpo-39573 added Py_SET_REFCNT() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_SET_REFCNT) +static inline void _Py_SET_REFCNT(PyObject *ob, Py_ssize_t refcnt) +{ + ob->ob_refcnt = refcnt; +} +#define Py_SET_REFCNT(ob, refcnt) _Py_SET_REFCNT(_PyObject_CAST(ob), refcnt) +#endif + + +// Py_SETREF() and Py_XSETREF() were added to Python 3.5.2. +// It is excluded from the limited C API. +#if (PY_VERSION_HEX < 0x03050200 && !defined(Py_SETREF)) && !defined(Py_LIMITED_API) +#define Py_SETREF(dst, src) \ + do { \ + PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \ + PyObject *_tmp_dst = (*_tmp_dst_ptr); \ + *_tmp_dst_ptr = _PyObject_CAST(src); \ + Py_DECREF(_tmp_dst); \ + } while (0) + +#define Py_XSETREF(dst, src) \ + do { \ + PyObject **_tmp_dst_ptr = _Py_CAST(PyObject**, &(dst)); \ + PyObject *_tmp_dst = (*_tmp_dst_ptr); \ + *_tmp_dst_ptr = _PyObject_CAST(src); \ + Py_XDECREF(_tmp_dst); \ + } while (0) +#endif + + +// bpo-43753 added Py_Is(), Py_IsNone(), Py_IsTrue() and Py_IsFalse() +// to Python 3.10.0b1. +#if PY_VERSION_HEX < 0x030A00B1 && !defined(Py_Is) +# define Py_Is(x, y) ((x) == (y)) +#endif +#if PY_VERSION_HEX < 0x030A00B1 && !defined(Py_IsNone) +# define Py_IsNone(x) Py_Is(x, Py_None) +#endif +#if (PY_VERSION_HEX < 0x030A00B1 || defined(PYPY_VERSION)) && !defined(Py_IsTrue) +# define Py_IsTrue(x) Py_Is(x, Py_True) +#endif +#if (PY_VERSION_HEX < 0x030A00B1 || defined(PYPY_VERSION)) && !defined(Py_IsFalse) +# define Py_IsFalse(x) Py_Is(x, Py_False) +#endif + + +// bpo-39573 added Py_SET_TYPE() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_SET_TYPE) +static inline void _Py_SET_TYPE(PyObject *ob, PyTypeObject *type) +{ + ob->ob_type = type; +} +#define Py_SET_TYPE(ob, type) _Py_SET_TYPE(_PyObject_CAST(ob), type) +#endif + + +// bpo-39573 added Py_SET_SIZE() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_SET_SIZE) +static inline void _Py_SET_SIZE(PyVarObject *ob, Py_ssize_t size) +{ + ob->ob_size = size; +} +#define Py_SET_SIZE(ob, size) _Py_SET_SIZE((PyVarObject*)(ob), size) +#endif + + +// bpo-40421 added PyFrame_GetCode() to Python 3.9.0b1 +#if PY_VERSION_HEX < 0x030900B1 || defined(PYPY_VERSION) +static inline PyCodeObject* PyFrame_GetCode(PyFrameObject *frame) +{ + assert(frame != _Py_NULL); + assert(frame->f_code != _Py_NULL); + return _Py_CAST(PyCodeObject*, Py_NewRef(frame->f_code)); +} +#endif + +static inline PyCodeObject* _PyFrame_GetCodeBorrow(PyFrameObject *frame) +{ + PyCodeObject *code = PyFrame_GetCode(frame); + Py_DECREF(code); + return code; +} + + +// bpo-40421 added PyFrame_GetBack() to Python 3.9.0b1 +#if PY_VERSION_HEX < 0x030900B1 && !defined(PYPY_VERSION) +static inline PyFrameObject* PyFrame_GetBack(PyFrameObject *frame) +{ + assert(frame != _Py_NULL); + return _Py_CAST(PyFrameObject*, Py_XNewRef(frame->f_back)); +} +#endif + +#if !defined(PYPY_VERSION) +static inline PyFrameObject* _PyFrame_GetBackBorrow(PyFrameObject *frame) +{ + PyFrameObject *back = PyFrame_GetBack(frame); + Py_XDECREF(back); + return back; +} +#endif + + +// bpo-40421 added PyFrame_GetLocals() to Python 3.11.0a7 +#if PY_VERSION_HEX < 0x030B00A7 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetLocals(PyFrameObject *frame) +{ +#if PY_VERSION_HEX >= 0x030400B1 + if (PyFrame_FastToLocalsWithError(frame) < 0) { + return NULL; + } +#else + PyFrame_FastToLocals(frame); +#endif + return Py_NewRef(frame->f_locals); +} +#endif + + +// bpo-40421 added PyFrame_GetGlobals() to Python 3.11.0a7 +#if PY_VERSION_HEX < 0x030B00A7 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetGlobals(PyFrameObject *frame) +{ + return Py_NewRef(frame->f_globals); +} +#endif + + +// bpo-40421 added PyFrame_GetBuiltins() to Python 3.11.0a7 +#if PY_VERSION_HEX < 0x030B00A7 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetBuiltins(PyFrameObject *frame) +{ + return Py_NewRef(frame->f_builtins); +} +#endif + + +// bpo-40421 added PyFrame_GetLasti() to Python 3.11.0b1 +#if PY_VERSION_HEX < 0x030B00B1 && !defined(PYPY_VERSION) +static inline int PyFrame_GetLasti(PyFrameObject *frame) +{ +#if PY_VERSION_HEX >= 0x030A00A7 + // bpo-27129: Since Python 3.10.0a7, f_lasti is an instruction offset, + // not a bytes offset anymore. Python uses 16-bit "wordcode" (2 bytes) + // instructions. + if (frame->f_lasti < 0) { + return -1; + } + return frame->f_lasti * 2; +#else + return frame->f_lasti; +#endif +} +#endif + + +// gh-91248 added PyFrame_GetVar() to Python 3.12.0a2 +#if PY_VERSION_HEX < 0x030C00A2 && !defined(PYPY_VERSION) +static inline PyObject* PyFrame_GetVar(PyFrameObject *frame, PyObject *name) +{ + PyObject *locals, *value; + + locals = PyFrame_GetLocals(frame); + if (locals == NULL) { + return NULL; + } +#if PY_VERSION_HEX >= 0x03000000 + value = PyDict_GetItemWithError(locals, name); +#else + value = _PyDict_GetItemWithError(locals, name); +#endif + Py_DECREF(locals); + + if (value == NULL) { + if (PyErr_Occurred()) { + return NULL; + } +#if PY_VERSION_HEX >= 0x03000000 + PyErr_Format(PyExc_NameError, "variable %R does not exist", name); +#else + PyErr_SetString(PyExc_NameError, "variable does not exist"); +#endif + return NULL; + } + return Py_NewRef(value); +} +#endif + + +// gh-91248 added PyFrame_GetVarString() to Python 3.12.0a2 +#if PY_VERSION_HEX < 0x030C00A2 && !defined(PYPY_VERSION) +static inline PyObject* +PyFrame_GetVarString(PyFrameObject *frame, const char *name) +{ + PyObject *name_obj, *value; +#if PY_VERSION_HEX >= 0x03000000 + name_obj = PyUnicode_FromString(name); +#else + name_obj = PyString_FromString(name); +#endif + if (name_obj == NULL) { + return NULL; + } + value = PyFrame_GetVar(frame, name_obj); + Py_DECREF(name_obj); + return value; +} +#endif + + +// bpo-39947 added PyThreadState_GetInterpreter() to Python 3.9.0a5 +#if PY_VERSION_HEX < 0x030900A5 || (defined(PYPY_VERSION) && PY_VERSION_HEX < 0x030B0000) +static inline PyInterpreterState * +PyThreadState_GetInterpreter(PyThreadState *tstate) +{ + assert(tstate != _Py_NULL); + return tstate->interp; +} +#endif + + +// bpo-40429 added PyThreadState_GetFrame() to Python 3.9.0b1 +#if PY_VERSION_HEX < 0x030900B1 && !defined(PYPY_VERSION) +static inline PyFrameObject* PyThreadState_GetFrame(PyThreadState *tstate) +{ + assert(tstate != _Py_NULL); + return _Py_CAST(PyFrameObject *, Py_XNewRef(tstate->frame)); +} +#endif + +#if !defined(PYPY_VERSION) +static inline PyFrameObject* +_PyThreadState_GetFrameBorrow(PyThreadState *tstate) +{ + PyFrameObject *frame = PyThreadState_GetFrame(tstate); + Py_XDECREF(frame); + return frame; +} +#endif + + +// bpo-39947 added PyInterpreterState_Get() to Python 3.9.0a5 +#if PY_VERSION_HEX < 0x030900A5 || defined(PYPY_VERSION) +static inline PyInterpreterState* PyInterpreterState_Get(void) +{ + PyThreadState *tstate; + PyInterpreterState *interp; + + tstate = PyThreadState_GET(); + if (tstate == _Py_NULL) { + Py_FatalError("GIL released (tstate is NULL)"); + } + interp = tstate->interp; + if (interp == _Py_NULL) { + Py_FatalError("no current interpreter"); + } + return interp; +} +#endif + + +// bpo-39947 added PyInterpreterState_Get() to Python 3.9.0a6 +#if 0x030700A1 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030900A6 && !defined(PYPY_VERSION) +static inline uint64_t PyThreadState_GetID(PyThreadState *tstate) +{ + assert(tstate != _Py_NULL); + return tstate->id; +} +#endif + +// bpo-43760 added PyThreadState_EnterTracing() to Python 3.11.0a2 +#if PY_VERSION_HEX < 0x030B00A2 && !defined(PYPY_VERSION) +static inline void PyThreadState_EnterTracing(PyThreadState *tstate) +{ + tstate->tracing++; +#if PY_VERSION_HEX >= 0x030A00A1 + tstate->cframe->use_tracing = 0; +#else + tstate->use_tracing = 0; +#endif +} +#endif + +// bpo-43760 added PyThreadState_LeaveTracing() to Python 3.11.0a2 +#if PY_VERSION_HEX < 0x030B00A2 && !defined(PYPY_VERSION) +static inline void PyThreadState_LeaveTracing(PyThreadState *tstate) +{ + int use_tracing = (tstate->c_tracefunc != _Py_NULL + || tstate->c_profilefunc != _Py_NULL); + tstate->tracing--; +#if PY_VERSION_HEX >= 0x030A00A1 + tstate->cframe->use_tracing = use_tracing; +#else + tstate->use_tracing = use_tracing; +#endif +} +#endif + + +// bpo-37194 added PyObject_CallNoArgs() to Python 3.9.0a1 +// PyObject_CallNoArgs() added to PyPy 3.9.16-v7.3.11 +#if !defined(PyObject_CallNoArgs) && PY_VERSION_HEX < 0x030900A1 +static inline PyObject* PyObject_CallNoArgs(PyObject *func) +{ + return PyObject_CallFunctionObjArgs(func, NULL); +} +#endif + + +// bpo-39245 made PyObject_CallOneArg() public (previously called +// _PyObject_CallOneArg) in Python 3.9.0a4 +// PyObject_CallOneArg() added to PyPy 3.9.16-v7.3.11 +#if !defined(PyObject_CallOneArg) && PY_VERSION_HEX < 0x030900A4 +static inline PyObject* PyObject_CallOneArg(PyObject *func, PyObject *arg) +{ + return PyObject_CallFunctionObjArgs(func, arg, NULL); +} +#endif + + +// bpo-1635741 added PyModule_AddObjectRef() to Python 3.10.0a3 +#if PY_VERSION_HEX < 0x030A00A3 +static inline int +PyModule_AddObjectRef(PyObject *module, const char *name, PyObject *value) +{ + int res; + + if (!value && !PyErr_Occurred()) { + // PyModule_AddObject() raises TypeError in this case + PyErr_SetString(PyExc_SystemError, + "PyModule_AddObjectRef() must be called " + "with an exception raised if value is NULL"); + return -1; + } + + Py_XINCREF(value); + res = PyModule_AddObject(module, name, value); + if (res < 0) { + Py_XDECREF(value); + } + return res; +} +#endif + + +// bpo-40024 added PyModule_AddType() to Python 3.9.0a5 +#if PY_VERSION_HEX < 0x030900A5 +static inline int PyModule_AddType(PyObject *module, PyTypeObject *type) +{ + const char *name, *dot; + + if (PyType_Ready(type) < 0) { + return -1; + } + + // inline _PyType_Name() + name = type->tp_name; + assert(name != _Py_NULL); + dot = strrchr(name, '.'); + if (dot != _Py_NULL) { + name = dot + 1; + } + + return PyModule_AddObjectRef(module, name, _PyObject_CAST(type)); +} +#endif + + +// bpo-40241 added PyObject_GC_IsTracked() to Python 3.9.0a6. +// bpo-4688 added _PyObject_GC_IS_TRACKED() to Python 2.7.0a2. +#if PY_VERSION_HEX < 0x030900A6 && !defined(PYPY_VERSION) +static inline int PyObject_GC_IsTracked(PyObject* obj) +{ + return (PyObject_IS_GC(obj) && _PyObject_GC_IS_TRACKED(obj)); +} +#endif + +// bpo-40241 added PyObject_GC_IsFinalized() to Python 3.9.0a6. +// bpo-18112 added _PyGCHead_FINALIZED() to Python 3.4.0 final. +#if PY_VERSION_HEX < 0x030900A6 && PY_VERSION_HEX >= 0x030400F0 && !defined(PYPY_VERSION) +static inline int PyObject_GC_IsFinalized(PyObject *obj) +{ + PyGC_Head *gc = _Py_CAST(PyGC_Head*, obj) - 1; + return (PyObject_IS_GC(obj) && _PyGCHead_FINALIZED(gc)); +} +#endif + + +// bpo-39573 added Py_IS_TYPE() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 && !defined(Py_IS_TYPE) +static inline int _Py_IS_TYPE(PyObject *ob, PyTypeObject *type) { + return Py_TYPE(ob) == type; +} +#define Py_IS_TYPE(ob, type) _Py_IS_TYPE(_PyObject_CAST(ob), type) +#endif + + +// bpo-46906 added PyFloat_Pack2() and PyFloat_Unpack2() to Python 3.11a7. +// bpo-11734 added _PyFloat_Pack2() and _PyFloat_Unpack2() to Python 3.6.0b1. +// Python 3.11a2 moved _PyFloat_Pack2() and _PyFloat_Unpack2() to the internal +// C API: Python 3.11a2-3.11a6 versions are not supported. +#if 0x030600B1 <= PY_VERSION_HEX && PY_VERSION_HEX <= 0x030B00A1 && !defined(PYPY_VERSION) +static inline int PyFloat_Pack2(double x, char *p, int le) +{ return _PyFloat_Pack2(x, (unsigned char*)p, le); } + +static inline double PyFloat_Unpack2(const char *p, int le) +{ return _PyFloat_Unpack2((const unsigned char *)p, le); } +#endif + + +// bpo-46906 added PyFloat_Pack4(), PyFloat_Pack8(), PyFloat_Unpack4() and +// PyFloat_Unpack8() to Python 3.11a7. +// Python 3.11a2 moved _PyFloat_Pack4(), _PyFloat_Pack8(), _PyFloat_Unpack4() +// and _PyFloat_Unpack8() to the internal C API: Python 3.11a2-3.11a6 versions +// are not supported. +#if PY_VERSION_HEX <= 0x030B00A1 && !defined(PYPY_VERSION) +static inline int PyFloat_Pack4(double x, char *p, int le) +{ return _PyFloat_Pack4(x, (unsigned char*)p, le); } + +static inline int PyFloat_Pack8(double x, char *p, int le) +{ return _PyFloat_Pack8(x, (unsigned char*)p, le); } + +static inline double PyFloat_Unpack4(const char *p, int le) +{ return _PyFloat_Unpack4((const unsigned char *)p, le); } + +static inline double PyFloat_Unpack8(const char *p, int le) +{ return _PyFloat_Unpack8((const unsigned char *)p, le); } +#endif + + +// gh-92154 added PyCode_GetCode() to Python 3.11.0b1 +#if PY_VERSION_HEX < 0x030B00B1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetCode(PyCodeObject *code) +{ + return Py_NewRef(code->co_code); +} +#endif + + +// gh-95008 added PyCode_GetVarnames() to Python 3.11.0rc1 +#if PY_VERSION_HEX < 0x030B00C1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetVarnames(PyCodeObject *code) +{ + return Py_NewRef(code->co_varnames); +} +#endif + +// gh-95008 added PyCode_GetFreevars() to Python 3.11.0rc1 +#if PY_VERSION_HEX < 0x030B00C1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetFreevars(PyCodeObject *code) +{ + return Py_NewRef(code->co_freevars); +} +#endif + +// gh-95008 added PyCode_GetCellvars() to Python 3.11.0rc1 +#if PY_VERSION_HEX < 0x030B00C1 && !defined(PYPY_VERSION) +static inline PyObject* PyCode_GetCellvars(PyCodeObject *code) +{ + return Py_NewRef(code->co_cellvars); +} +#endif + + +// Py_UNUSED() was added to Python 3.4.0b2. +#if PY_VERSION_HEX < 0x030400B2 && !defined(Py_UNUSED) +# if defined(__GNUC__) || defined(__clang__) +# define Py_UNUSED(name) _unused_ ## name __attribute__((unused)) +# else +# define Py_UNUSED(name) _unused_ ## name +# endif +#endif + + +// gh-105922 added PyImport_AddModuleRef() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A0 +static inline PyObject* PyImport_AddModuleRef(const char *name) +{ + return Py_XNewRef(PyImport_AddModule(name)); +} +#endif + + +// gh-105927 added PyWeakref_GetRef() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D0000 +static inline int PyWeakref_GetRef(PyObject *ref, PyObject **pobj) +{ + PyObject *obj; + if (ref != NULL && !PyWeakref_Check(ref)) { + *pobj = NULL; + PyErr_SetString(PyExc_TypeError, "expected a weakref"); + return -1; + } + obj = PyWeakref_GetObject(ref); + if (obj == NULL) { + // SystemError if ref is NULL + *pobj = NULL; + return -1; + } + if (obj == Py_None) { + *pobj = NULL; + return 0; + } + *pobj = Py_NewRef(obj); + return 1; +} +#endif + + +// bpo-36974 added PY_VECTORCALL_ARGUMENTS_OFFSET to Python 3.8b1 +#ifndef PY_VECTORCALL_ARGUMENTS_OFFSET +# define PY_VECTORCALL_ARGUMENTS_OFFSET (_Py_CAST(size_t, 1) << (8 * sizeof(size_t) - 1)) +#endif + +// bpo-36974 added PyVectorcall_NARGS() to Python 3.8b1 +#if PY_VERSION_HEX < 0x030800B1 +static inline Py_ssize_t PyVectorcall_NARGS(size_t n) +{ + return n & ~PY_VECTORCALL_ARGUMENTS_OFFSET; +} +#endif + + +// gh-105922 added PyObject_Vectorcall() to Python 3.9.0a4 +#if PY_VERSION_HEX < 0x030900A4 +static inline PyObject* +PyObject_Vectorcall(PyObject *callable, PyObject *const *args, + size_t nargsf, PyObject *kwnames) +{ +#if PY_VERSION_HEX >= 0x030800B1 && !defined(PYPY_VERSION) + // bpo-36974 added _PyObject_Vectorcall() to Python 3.8.0b1 + return _PyObject_Vectorcall(callable, args, nargsf, kwnames); +#else + PyObject *posargs = NULL, *kwargs = NULL; + PyObject *res; + Py_ssize_t nposargs, nkwargs, i; + + if (nargsf != 0 && args == NULL) { + PyErr_BadInternalCall(); + goto error; + } + if (kwnames != NULL && !PyTuple_Check(kwnames)) { + PyErr_BadInternalCall(); + goto error; + } + + nposargs = (Py_ssize_t)PyVectorcall_NARGS(nargsf); + if (kwnames) { + nkwargs = PyTuple_GET_SIZE(kwnames); + } + else { + nkwargs = 0; + } + + posargs = PyTuple_New(nposargs); + if (posargs == NULL) { + goto error; + } + if (nposargs) { + for (i=0; i < nposargs; i++) { + PyTuple_SET_ITEM(posargs, i, Py_NewRef(*args)); + args++; + } + } + + if (nkwargs) { + kwargs = PyDict_New(); + if (kwargs == NULL) { + goto error; + } + + for (i = 0; i < nkwargs; i++) { + PyObject *key = PyTuple_GET_ITEM(kwnames, i); + PyObject *value = *args; + args++; + if (PyDict_SetItem(kwargs, key, value) < 0) { + goto error; + } + } + } + else { + kwargs = NULL; + } + + res = PyObject_Call(callable, posargs, kwargs); + Py_DECREF(posargs); + Py_XDECREF(kwargs); + return res; + +error: + Py_DECREF(posargs); + Py_XDECREF(kwargs); + return NULL; +#endif +} +#endif + + +// gh-106521 added PyObject_GetOptionalAttr() and +// PyObject_GetOptionalAttrString() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyObject_GetOptionalAttr(PyObject *obj, PyObject *attr_name, PyObject **result) +{ + // bpo-32571 added _PyObject_LookupAttr() to Python 3.7.0b1 +#if PY_VERSION_HEX >= 0x030700B1 && !defined(PYPY_VERSION) + return _PyObject_LookupAttr(obj, attr_name, result); +#else + *result = PyObject_GetAttr(obj, attr_name); + if (*result != NULL) { + return 1; + } + if (!PyErr_Occurred()) { + return 0; + } + if (PyErr_ExceptionMatches(PyExc_AttributeError)) { + PyErr_Clear(); + return 0; + } + return -1; +#endif +} + +static inline int +PyObject_GetOptionalAttrString(PyObject *obj, const char *attr_name, PyObject **result) +{ + PyObject *name_obj; + int rc; +#if PY_VERSION_HEX >= 0x03000000 + name_obj = PyUnicode_FromString(attr_name); +#else + name_obj = PyString_FromString(attr_name); +#endif + if (name_obj == NULL) { + *result = NULL; + return -1; + } + rc = PyObject_GetOptionalAttr(obj, name_obj, result); + Py_DECREF(name_obj); + return rc; +} +#endif + + +// gh-106307 added PyObject_GetOptionalAttr() and +// PyMapping_GetOptionalItemString() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyMapping_GetOptionalItem(PyObject *obj, PyObject *key, PyObject **result) +{ + *result = PyObject_GetItem(obj, key); + if (*result) { + return 1; + } + if (!PyErr_ExceptionMatches(PyExc_KeyError)) { + return -1; + } + PyErr_Clear(); + return 0; +} + +static inline int +PyMapping_GetOptionalItemString(PyObject *obj, const char *key, PyObject **result) +{ + PyObject *key_obj; + int rc; +#if PY_VERSION_HEX >= 0x03000000 + key_obj = PyUnicode_FromString(key); +#else + key_obj = PyString_FromString(key); +#endif + if (key_obj == NULL) { + *result = NULL; + return -1; + } + rc = PyMapping_GetOptionalItem(obj, key_obj, result); + Py_DECREF(key_obj); + return rc; +} +#endif + +// gh-108511 added PyMapping_HasKeyWithError() and +// PyMapping_HasKeyStringWithError() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyMapping_HasKeyWithError(PyObject *obj, PyObject *key) +{ + PyObject *res; + int rc = PyMapping_GetOptionalItem(obj, key, &res); + Py_XDECREF(res); + return rc; +} + +static inline int +PyMapping_HasKeyStringWithError(PyObject *obj, const char *key) +{ + PyObject *res; + int rc = PyMapping_GetOptionalItemString(obj, key, &res); + Py_XDECREF(res); + return rc; +} +#endif + + +// gh-108511 added PyObject_HasAttrWithError() and +// PyObject_HasAttrStringWithError() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyObject_HasAttrWithError(PyObject *obj, PyObject *attr) +{ + PyObject *res; + int rc = PyObject_GetOptionalAttr(obj, attr, &res); + Py_XDECREF(res); + return rc; +} + +static inline int +PyObject_HasAttrStringWithError(PyObject *obj, const char *attr) +{ + PyObject *res; + int rc = PyObject_GetOptionalAttrString(obj, attr, &res); + Py_XDECREF(res); + return rc; +} +#endif + + +// gh-106004 added PyDict_GetItemRef() and PyDict_GetItemStringRef() +// to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyDict_GetItemRef(PyObject *mp, PyObject *key, PyObject **result) +{ +#if PY_VERSION_HEX >= 0x03000000 + PyObject *item = PyDict_GetItemWithError(mp, key); +#else + PyObject *item = _PyDict_GetItemWithError(mp, key); +#endif + if (item != NULL) { + *result = Py_NewRef(item); + return 1; // found + } + if (!PyErr_Occurred()) { + *result = NULL; + return 0; // not found + } + *result = NULL; + return -1; +} + +static inline int +PyDict_GetItemStringRef(PyObject *mp, const char *key, PyObject **result) +{ + int res; +#if PY_VERSION_HEX >= 0x03000000 + PyObject *key_obj = PyUnicode_FromString(key); +#else + PyObject *key_obj = PyString_FromString(key); +#endif + if (key_obj == NULL) { + *result = NULL; + return -1; + } + res = PyDict_GetItemRef(mp, key_obj, result); + Py_DECREF(key_obj); + return res; +} +#endif + + +// gh-106307 added PyModule_Add() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyModule_Add(PyObject *mod, const char *name, PyObject *value) +{ + int res = PyModule_AddObjectRef(mod, name, value); + Py_XDECREF(value); + return res; +} +#endif + + +// gh-108014 added Py_IsFinalizing() to Python 3.13.0a1 +// bpo-1856 added _Py_Finalizing to Python 3.2.1b1. +// _Py_IsFinalizing() was added to PyPy 7.3.0. +#if (0x030201B1 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030D00A1) \ + && (!defined(PYPY_VERSION_NUM) || PYPY_VERSION_NUM >= 0x7030000) +static inline int Py_IsFinalizing(void) +{ +#if PY_VERSION_HEX >= 0x030700A1 + // _Py_IsFinalizing() was added to Python 3.7.0a1. + return _Py_IsFinalizing(); +#else + return (_Py_Finalizing != NULL); +#endif +} +#endif + + +// gh-108323 added PyDict_ContainsString() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int PyDict_ContainsString(PyObject *op, const char *key) +{ + PyObject *key_obj = PyUnicode_FromString(key); + if (key_obj == NULL) { + return -1; + } + int res = PyDict_Contains(op, key_obj); + Py_DECREF(key_obj); + return res; +} +#endif + + +// gh-108445 added PyLong_AsInt() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int PyLong_AsInt(PyObject *obj) +{ +#ifdef PYPY_VERSION + long value = PyLong_AsLong(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + if (value < (long)INT_MIN || (long)INT_MAX < value) { + PyErr_SetString(PyExc_OverflowError, + "Python int too large to convert to C int"); + return -1; + } + return (int)value; +#else + return _PyLong_AsInt(obj); +#endif +} +#endif + + +// gh-107073 added PyObject_VisitManagedDict() to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyObject_VisitManagedDict(PyObject *obj, visitproc visit, void *arg) +{ + PyObject **dict = _PyObject_GetDictPtr(obj); + if (dict == NULL || *dict == NULL) { + return -1; + } + Py_VISIT(*dict); + return 0; +} + +static inline void +PyObject_ClearManagedDict(PyObject *obj) +{ + PyObject **dict = _PyObject_GetDictPtr(obj); + if (dict == NULL || *dict == NULL) { + return; + } + Py_CLEAR(*dict); +} +#endif + +// gh-108867 added PyThreadState_GetUnchecked() to Python 3.13.0a1 +// Python 3.5.2 added _PyThreadState_UncheckedGet(). +#if PY_VERSION_HEX >= 0x03050200 && PY_VERSION_HEX < 0x030D00A1 +static inline PyThreadState* +PyThreadState_GetUnchecked(void) +{ + return _PyThreadState_UncheckedGet(); +} +#endif + +// gh-110289 added PyUnicode_EqualToUTF8() and PyUnicode_EqualToUTF8AndSize() +// to Python 3.13.0a1 +#if PY_VERSION_HEX < 0x030D00A1 +static inline int +PyUnicode_EqualToUTF8AndSize(PyObject *unicode, const char *str, Py_ssize_t str_len) +{ + Py_ssize_t len; + const void *utf8; + PyObject *exc_type, *exc_value, *exc_tb; + int res; + + // API cannot report errors so save/restore the exception + PyErr_Fetch(&exc_type, &exc_value, &exc_tb); + + // Python 3.3.0a1 added PyUnicode_AsUTF8AndSize() +#if PY_VERSION_HEX >= 0x030300A1 + if (PyUnicode_IS_ASCII(unicode)) { + utf8 = PyUnicode_DATA(unicode); + len = PyUnicode_GET_LENGTH(unicode); + } + else { + utf8 = PyUnicode_AsUTF8AndSize(unicode, &len); + if (utf8 == NULL) { + // Memory allocation failure. The API cannot report error, + // so ignore the exception and return 0. + res = 0; + goto done; + } + } + + if (len != str_len) { + res = 0; + goto done; + } + res = (memcmp(utf8, str, (size_t)len) == 0); +#else + PyObject *bytes = PyUnicode_AsUTF8String(unicode); + if (bytes == NULL) { + // Memory allocation failure. The API cannot report error, + // so ignore the exception and return 0. + res = 0; + goto done; + } + +#if PY_VERSION_HEX >= 0x03000000 + len = PyBytes_GET_SIZE(bytes); + utf8 = PyBytes_AS_STRING(bytes); +#else + len = PyString_GET_SIZE(bytes); + utf8 = PyString_AS_STRING(bytes); +#endif + if (len != str_len) { + Py_DECREF(bytes); + res = 0; + goto done; + } + + res = (memcmp(utf8, str, (size_t)len) == 0); + Py_DECREF(bytes); +#endif + +done: + PyErr_Restore(exc_type, exc_value, exc_tb); + return res; +} + +static inline int +PyUnicode_EqualToUTF8(PyObject *unicode, const char *str) +{ + return PyUnicode_EqualToUTF8AndSize(unicode, str, (Py_ssize_t)strlen(str)); +} +#endif + + +// gh-111138 added PyList_Extend() and PyList_Clear() to Python 3.13.0a2 +#if PY_VERSION_HEX < 0x030D00A2 +static inline int +PyList_Extend(PyObject *list, PyObject *iterable) +{ + return PyList_SetSlice(list, PY_SSIZE_T_MAX, PY_SSIZE_T_MAX, iterable); +} + +static inline int +PyList_Clear(PyObject *list) +{ + return PyList_SetSlice(list, 0, PY_SSIZE_T_MAX, NULL); +} +#endif + +// gh-111262 added PyDict_Pop() and PyDict_PopString() to Python 3.13.0a2 +#if PY_VERSION_HEX < 0x030D00A2 +static inline int +PyDict_Pop(PyObject *dict, PyObject *key, PyObject **result) +{ + PyObject *value; + + if (!PyDict_Check(dict)) { + PyErr_BadInternalCall(); + if (result) { + *result = NULL; + } + return -1; + } + + // bpo-16991 added _PyDict_Pop() to Python 3.5.0b2. + // Python 3.6.0b3 changed _PyDict_Pop() first argument type to PyObject*. + // Python 3.13.0a1 removed _PyDict_Pop(). +#if defined(PYPY_VERSION) || PY_VERSION_HEX < 0x030500b2 || PY_VERSION_HEX >= 0x030D0000 + value = PyObject_CallMethod(dict, "pop", "O", key); +#elif PY_VERSION_HEX < 0x030600b3 + value = _PyDict_Pop(_Py_CAST(PyDictObject*, dict), key, NULL); +#else + value = _PyDict_Pop(dict, key, NULL); +#endif + if (value == NULL) { + if (result) { + *result = NULL; + } + if (PyErr_Occurred() && !PyErr_ExceptionMatches(PyExc_KeyError)) { + return -1; + } + PyErr_Clear(); + return 0; + } + if (result) { + *result = value; + } + else { + Py_DECREF(value); + } + return 1; +} + +static inline int +PyDict_PopString(PyObject *dict, const char *key, PyObject **result) +{ + PyObject *key_obj = PyUnicode_FromString(key); + if (key_obj == NULL) { + if (result != NULL) { + *result = NULL; + } + return -1; + } + + int res = PyDict_Pop(dict, key_obj, result); + Py_DECREF(key_obj); + return res; +} +#endif + + +#if PY_VERSION_HEX < 0x030200A4 +// Python 3.2.0a4 added Py_hash_t type +typedef Py_ssize_t Py_hash_t; +#endif + + +// gh-111545 added Py_HashPointer() to Python 3.13.0a3 +#if PY_VERSION_HEX < 0x030D00A3 +static inline Py_hash_t Py_HashPointer(const void *ptr) +{ +#if PY_VERSION_HEX >= 0x030900A4 && !defined(PYPY_VERSION) + return _Py_HashPointer(ptr); +#else + return _Py_HashPointer(_Py_CAST(void*, ptr)); +#endif +} +#endif + + +// Python 3.13a4 added a PyTime API. +// Use the private API added to Python 3.5. +#if PY_VERSION_HEX < 0x030D00A4 && PY_VERSION_HEX >= 0x03050000 +typedef _PyTime_t PyTime_t; +#define PyTime_MIN _PyTime_MIN +#define PyTime_MAX _PyTime_MAX + +static inline double PyTime_AsSecondsDouble(PyTime_t t) +{ return _PyTime_AsSecondsDouble(t); } + +static inline int PyTime_Monotonic(PyTime_t *result) +{ return _PyTime_GetMonotonicClockWithInfo(result, NULL); } + +static inline int PyTime_Time(PyTime_t *result) +{ return _PyTime_GetSystemClockWithInfo(result, NULL); } + +static inline int PyTime_PerfCounter(PyTime_t *result) +{ +#if PY_VERSION_HEX >= 0x03070000 && !defined(PYPY_VERSION) + return _PyTime_GetPerfCounterWithInfo(result, NULL); +#elif PY_VERSION_HEX >= 0x03070000 + // Call time.perf_counter_ns() and convert Python int object to PyTime_t. + // Cache time.perf_counter_ns() function for best performance. + static PyObject *func = NULL; + if (func == NULL) { + PyObject *mod = PyImport_ImportModule("time"); + if (mod == NULL) { + return -1; + } + + func = PyObject_GetAttrString(mod, "perf_counter_ns"); + Py_DECREF(mod); + if (func == NULL) { + return -1; + } + } + + PyObject *res = PyObject_CallNoArgs(func); + if (res == NULL) { + return -1; + } + long long value = PyLong_AsLongLong(res); + Py_DECREF(res); + + if (value == -1 && PyErr_Occurred()) { + return -1; + } + + Py_BUILD_ASSERT(sizeof(value) >= sizeof(PyTime_t)); + *result = (PyTime_t)value; + return 0; +#else + // Call time.perf_counter() and convert C double to PyTime_t. + // Cache time.perf_counter() function for best performance. + static PyObject *func = NULL; + if (func == NULL) { + PyObject *mod = PyImport_ImportModule("time"); + if (mod == NULL) { + return -1; + } + + func = PyObject_GetAttrString(mod, "perf_counter"); + Py_DECREF(mod); + if (func == NULL) { + return -1; + } + } + + PyObject *res = PyObject_CallNoArgs(func); + if (res == NULL) { + return -1; + } + double d = PyFloat_AsDouble(res); + Py_DECREF(res); + + if (d == -1.0 && PyErr_Occurred()) { + return -1; + } + + // Avoid floor() to avoid having to link to libm + *result = (PyTime_t)(d * 1e9); + return 0; +#endif +} + +#endif + +// gh-111389 added hash constants to Python 3.13.0a5. These constants were +// added first as private macros to Python 3.4.0b1 and PyPy 7.3.8. +#if (!defined(PyHASH_BITS) \ + && ((!defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x030400B1) \ + || (defined(PYPY_VERSION) && PY_VERSION_HEX >= 0x03070000 \ + && PYPY_VERSION_NUM >= 0x07030800))) +# define PyHASH_BITS _PyHASH_BITS +# define PyHASH_MODULUS _PyHASH_MODULUS +# define PyHASH_INF _PyHASH_INF +# define PyHASH_IMAG _PyHASH_IMAG +#endif + + +// gh-111545 added Py_GetConstant() and Py_GetConstantBorrowed() +// to Python 3.13.0a6 +#if PY_VERSION_HEX < 0x030D00A6 && !defined(Py_CONSTANT_NONE) + +#define Py_CONSTANT_NONE 0 +#define Py_CONSTANT_FALSE 1 +#define Py_CONSTANT_TRUE 2 +#define Py_CONSTANT_ELLIPSIS 3 +#define Py_CONSTANT_NOT_IMPLEMENTED 4 +#define Py_CONSTANT_ZERO 5 +#define Py_CONSTANT_ONE 6 +#define Py_CONSTANT_EMPTY_STR 7 +#define Py_CONSTANT_EMPTY_BYTES 8 +#define Py_CONSTANT_EMPTY_TUPLE 9 + +static inline PyObject* Py_GetConstant(unsigned int constant_id) +{ + static PyObject* constants[Py_CONSTANT_EMPTY_TUPLE + 1] = {NULL}; + + if (constants[Py_CONSTANT_NONE] == NULL) { + constants[Py_CONSTANT_NONE] = Py_None; + constants[Py_CONSTANT_FALSE] = Py_False; + constants[Py_CONSTANT_TRUE] = Py_True; + constants[Py_CONSTANT_ELLIPSIS] = Py_Ellipsis; + constants[Py_CONSTANT_NOT_IMPLEMENTED] = Py_NotImplemented; + + constants[Py_CONSTANT_ZERO] = PyLong_FromLong(0); + if (constants[Py_CONSTANT_ZERO] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_ONE] = PyLong_FromLong(1); + if (constants[Py_CONSTANT_ONE] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_STR] = PyUnicode_FromStringAndSize("", 0); + if (constants[Py_CONSTANT_EMPTY_STR] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_BYTES] = PyBytes_FromStringAndSize("", 0); + if (constants[Py_CONSTANT_EMPTY_BYTES] == NULL) { + goto fatal_error; + } + + constants[Py_CONSTANT_EMPTY_TUPLE] = PyTuple_New(0); + if (constants[Py_CONSTANT_EMPTY_TUPLE] == NULL) { + goto fatal_error; + } + // goto dance to avoid compiler warnings about Py_FatalError() + goto init_done; + +fatal_error: + // This case should never happen + Py_FatalError("Py_GetConstant() failed to get constants"); + } + +init_done: + if (constant_id <= Py_CONSTANT_EMPTY_TUPLE) { + return Py_NewRef(constants[constant_id]); + } + else { + PyErr_BadInternalCall(); + return NULL; + } +} + +static inline PyObject* Py_GetConstantBorrowed(unsigned int constant_id) +{ + PyObject *obj = Py_GetConstant(constant_id); + Py_XDECREF(obj); + return obj; +} +#endif + + +// gh-114329 added PyList_GetItemRef() to Python 3.13.0a4 +#if PY_VERSION_HEX < 0x030D00A4 +static inline PyObject * +PyList_GetItemRef(PyObject *op, Py_ssize_t index) +{ + PyObject *item = PyList_GetItem(op, index); + Py_XINCREF(item); + return item; +} +#endif + + +// gh-114329 added PyList_GetItemRef() to Python 3.13.0a4 +#if PY_VERSION_HEX < 0x030D00A4 +static inline int +PyDict_SetDefaultRef(PyObject *d, PyObject *key, PyObject *default_value, + PyObject **result) +{ + PyObject *value; + if (PyDict_GetItemRef(d, key, &value) < 0) { + // get error + if (result) { + *result = NULL; + } + return -1; + } + if (value != NULL) { + // present + if (result) { + *result = value; + } + else { + Py_DECREF(value); + } + return 1; + } + + // missing: set the item + if (PyDict_SetItem(d, key, default_value) < 0) { + // set error + if (result) { + *result = NULL; + } + return -1; + } + if (result) { + *result = Py_NewRef(default_value); + } + return 0; +} +#endif + +#if PY_VERSION_HEX < 0x030D00B3 +# define Py_BEGIN_CRITICAL_SECTION(op) { +# define Py_END_CRITICAL_SECTION() } +# define Py_BEGIN_CRITICAL_SECTION2(a, b) { +# define Py_END_CRITICAL_SECTION2() } +#endif + +#if PY_VERSION_HEX < 0x030E0000 && PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) +typedef struct PyUnicodeWriter PyUnicodeWriter; + +static inline void PyUnicodeWriter_Discard(PyUnicodeWriter *writer) +{ + _PyUnicodeWriter_Dealloc((_PyUnicodeWriter*)writer); + PyMem_Free(writer); +} + +static inline PyUnicodeWriter* PyUnicodeWriter_Create(Py_ssize_t length) +{ + if (length < 0) { + PyErr_SetString(PyExc_ValueError, + "length must be positive"); + return NULL; + } + + const size_t size = sizeof(_PyUnicodeWriter); + PyUnicodeWriter *pub_writer = (PyUnicodeWriter *)PyMem_Malloc(size); + if (pub_writer == _Py_NULL) { + PyErr_NoMemory(); + return _Py_NULL; + } + _PyUnicodeWriter *writer = (_PyUnicodeWriter *)pub_writer; + + _PyUnicodeWriter_Init(writer); + if (_PyUnicodeWriter_Prepare(writer, length, 127) < 0) { + PyUnicodeWriter_Discard(pub_writer); + return NULL; + } + writer->overallocate = 1; + return pub_writer; +} + +static inline PyObject* PyUnicodeWriter_Finish(PyUnicodeWriter *writer) +{ + PyObject *str = _PyUnicodeWriter_Finish((_PyUnicodeWriter*)writer); + assert(((_PyUnicodeWriter*)writer)->buffer == NULL); + PyMem_Free(writer); + return str; +} + +static inline int +PyUnicodeWriter_WriteChar(PyUnicodeWriter *writer, Py_UCS4 ch) +{ + if (ch > 0x10ffff) { + PyErr_SetString(PyExc_ValueError, + "character must be in range(0x110000)"); + return -1; + } + + return _PyUnicodeWriter_WriteChar((_PyUnicodeWriter*)writer, ch); +} + +static inline int +PyUnicodeWriter_WriteStr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Str(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +static inline int +PyUnicodeWriter_WriteRepr(PyUnicodeWriter *writer, PyObject *obj) +{ + PyObject *str = PyObject_Repr(obj); + if (str == NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} + +static inline int +PyUnicodeWriter_WriteUTF8(PyUnicodeWriter *writer, + const char *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)strlen(str); + } + + PyObject *str_obj = PyUnicode_FromStringAndSize(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteASCII(PyUnicodeWriter *writer, + const char *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)strlen(str); + } + + return _PyUnicodeWriter_WriteASCIIString((_PyUnicodeWriter*)writer, + str, size); +} + +static inline int +PyUnicodeWriter_WriteWideChar(PyUnicodeWriter *writer, + const wchar_t *str, Py_ssize_t size) +{ + if (size < 0) { + size = (Py_ssize_t)wcslen(str); + } + + PyObject *str_obj = PyUnicode_FromWideChar(str, size); + if (str_obj == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str_obj); + Py_DECREF(str_obj); + return res; +} + +static inline int +PyUnicodeWriter_WriteSubstring(PyUnicodeWriter *writer, PyObject *str, + Py_ssize_t start, Py_ssize_t end) +{ + if (!PyUnicode_Check(str)) { + PyErr_Format(PyExc_TypeError, "expect str, not %s", + Py_TYPE(str)->tp_name); + return -1; + } + if (start < 0 || start > end) { + PyErr_Format(PyExc_ValueError, "invalid start argument"); + return -1; + } + if (end > PyUnicode_GET_LENGTH(str)) { + PyErr_Format(PyExc_ValueError, "invalid end argument"); + return -1; + } + + return _PyUnicodeWriter_WriteSubstring((_PyUnicodeWriter*)writer, str, + start, end); +} + +static inline int +PyUnicodeWriter_Format(PyUnicodeWriter *writer, const char *format, ...) +{ + va_list vargs; + va_start(vargs, format); + PyObject *str = PyUnicode_FromFormatV(format, vargs); + va_end(vargs); + if (str == _Py_NULL) { + return -1; + } + + int res = _PyUnicodeWriter_WriteStr((_PyUnicodeWriter*)writer, str); + Py_DECREF(str); + return res; +} +#endif // PY_VERSION_HEX < 0x030E0000 + +// gh-116560 added PyLong_GetSign() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyLong_GetSign(PyObject *obj, int *sign) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expect int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + + *sign = _PyLong_Sign(obj); + return 0; +} +#endif + +// gh-126061 added PyLong_IsPositive/Negative/Zero() to Python in 3.14.0a2 +#if PY_VERSION_HEX < 0x030E00A2 +static inline int PyLong_IsPositive(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == 1; +} + +static inline int PyLong_IsNegative(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == -1; +} + +static inline int PyLong_IsZero(PyObject *obj) +{ + if (!PyLong_Check(obj)) { + PyErr_Format(PyExc_TypeError, "expected int, got %s", Py_TYPE(obj)->tp_name); + return -1; + } + return _PyLong_Sign(obj) == 0; +} +#endif + + +// gh-124502 added PyUnicode_Equal() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyUnicode_Equal(PyObject *str1, PyObject *str2) +{ + if (!PyUnicode_Check(str1)) { + PyErr_Format(PyExc_TypeError, "first argument must be str, not %s", + Py_TYPE(str1)->tp_name); + return -1; + } + if (!PyUnicode_Check(str2)) { + PyErr_Format(PyExc_TypeError, "second argument must be str, not %s", + Py_TYPE(str2)->tp_name); + return -1; + } + +#if PY_VERSION_HEX >= 0x030d0000 && !defined(PYPY_VERSION) + PyAPI_FUNC(int) _PyUnicode_Equal(PyObject *str1, PyObject *str2); + + return _PyUnicode_Equal(str1, str2); +#elif PY_VERSION_HEX >= 0x03060000 && !defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#elif PY_VERSION_HEX >= 0x03090000 && defined(PYPY_VERSION) + return _PyUnicode_EQ(str1, str2); +#else + return (PyUnicode_Compare(str1, str2) == 0); +#endif +} +#endif + + +// gh-121645 added PyBytes_Join() to Python 3.14.0a0 +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyBytes_Join(PyObject *sep, PyObject *iterable) +{ + return _PyBytes_Join(sep, iterable); +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline Py_hash_t Py_HashBuffer(const void *ptr, Py_ssize_t len) +{ +#if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) + PyAPI_FUNC(Py_hash_t) _Py_HashBytes(const void *src, Py_ssize_t len); + + return _Py_HashBytes(ptr, len); +#else + Py_hash_t hash; + PyObject *bytes = PyBytes_FromStringAndSize((const char*)ptr, len); + if (bytes == NULL) { + return -1; + } + hash = PyObject_Hash(bytes); + Py_DECREF(bytes); + return hash; +#endif +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline int PyIter_NextItem(PyObject *iter, PyObject **item) +{ + iternextfunc tp_iternext; + + assert(iter != NULL); + assert(item != NULL); + + tp_iternext = Py_TYPE(iter)->tp_iternext; + if (tp_iternext == NULL) { + *item = NULL; + PyErr_Format(PyExc_TypeError, "expected an iterator, got '%s'", + Py_TYPE(iter)->tp_name); + return -1; + } + + if ((*item = tp_iternext(iter))) { + return 1; + } + if (!PyErr_Occurred()) { + return 0; + } + if (PyErr_ExceptionMatches(PyExc_StopIteration)) { + PyErr_Clear(); + return 0; + } + return -1; +} +#endif + + +#if PY_VERSION_HEX < 0x030E00A0 +static inline PyObject* PyLong_FromInt32(int32_t value) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + return PyLong_FromLong(value); +} + +static inline PyObject* PyLong_FromInt64(int64_t value) +{ + Py_BUILD_ASSERT(sizeof(long long) >= 8); + return PyLong_FromLongLong(value); +} + +static inline PyObject* PyLong_FromUInt32(uint32_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long) >= 4); + return PyLong_FromUnsignedLong(value); +} + +static inline PyObject* PyLong_FromUInt64(uint64_t value) +{ + Py_BUILD_ASSERT(sizeof(unsigned long long) >= 8); + return PyLong_FromUnsignedLongLong(value); +} + +static inline int PyLong_AsInt32(PyObject *obj, int32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(int) == 4); + int value = PyLong_AsInt(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int32_t)value; + return 0; +} + +static inline int PyLong_AsInt64(PyObject *obj, int64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + long long value = PyLong_AsLongLong(obj); + if (value == -1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (int64_t)value; + return 0; +} + +static inline int PyLong_AsUInt32(PyObject *obj, uint32_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long) >= 4); + unsigned long value = PyLong_AsUnsignedLong(obj); + if (value == (unsigned long)-1 && PyErr_Occurred()) { + return -1; + } +#if SIZEOF_LONG > 4 + if ((unsigned long)UINT32_MAX < value) { + PyErr_SetString(PyExc_OverflowError, + "Python int too large to convert to C uint32_t"); + return -1; + } +#endif + *pvalue = (uint32_t)value; + return 0; +} + +static inline int PyLong_AsUInt64(PyObject *obj, uint64_t *pvalue) +{ + Py_BUILD_ASSERT(sizeof(long long) == 8); + unsigned long long value = PyLong_AsUnsignedLongLong(obj); + if (value == (unsigned long long)-1 && PyErr_Occurred()) { + return -1; + } + *pvalue = (uint64_t)value; + return 0; +} +#endif + + +// gh-102471 added import and export API for integers to 3.14.0a2. +#if PY_VERSION_HEX < 0x030E00A2 && PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) +// Helpers to access PyLongObject internals. +static inline void +_PyLong_SetSignAndDigitCount(PyLongObject *op, int sign, Py_ssize_t size) +{ +#if PY_VERSION_HEX >= 0x030C0000 + op->long_value.lv_tag = (uintptr_t)(1 - sign) | ((uintptr_t)(size) << 3); +#elif PY_VERSION_HEX >= 0x030900A4 + Py_SET_SIZE(op, sign * size); +#else + Py_SIZE(op) = sign * size; +#endif +} + +static inline Py_ssize_t +_PyLong_DigitCount(const PyLongObject *op) +{ +#if PY_VERSION_HEX >= 0x030C0000 + return (Py_ssize_t)(op->long_value.lv_tag >> 3); +#else + return _PyLong_Sign((PyObject*)op) < 0 ? -Py_SIZE(op) : Py_SIZE(op); +#endif +} + +static inline digit* +_PyLong_GetDigits(const PyLongObject *op) +{ +#if PY_VERSION_HEX >= 0x030C0000 + return (digit*)(op->long_value.ob_digit); +#else + return (digit*)(op->ob_digit); +#endif +} + +typedef struct PyLongLayout { + uint8_t bits_per_digit; + uint8_t digit_size; + int8_t digits_order; + int8_t digit_endianness; +} PyLongLayout; + +typedef struct PyLongExport { + int64_t value; + uint8_t negative; + Py_ssize_t ndigits; + const void *digits; + Py_uintptr_t _reserved; +} PyLongExport; + +typedef struct PyLongWriter PyLongWriter; + +static inline const PyLongLayout* +PyLong_GetNativeLayout(void) +{ + static const PyLongLayout PyLong_LAYOUT = { + PyLong_SHIFT, + sizeof(digit), + -1, // least significant first + PY_LITTLE_ENDIAN ? -1 : 1, + }; + + return &PyLong_LAYOUT; +} + +static inline int +PyLong_Export(PyObject *obj, PyLongExport *export_long) +{ + if (!PyLong_Check(obj)) { + memset(export_long, 0, sizeof(*export_long)); + PyErr_Format(PyExc_TypeError, "expected int, got %s", + Py_TYPE(obj)->tp_name); + return -1; + } + + // Fast-path: try to convert to a int64_t + PyLongObject *self = (PyLongObject*)obj; + int overflow; +#if SIZEOF_LONG == 8 + long value = PyLong_AsLongAndOverflow(obj, &overflow); +#else + // Windows has 32-bit long, so use 64-bit long long instead + long long value = PyLong_AsLongLongAndOverflow(obj, &overflow); +#endif + Py_BUILD_ASSERT(sizeof(value) == sizeof(int64_t)); + // the function cannot fail since obj is a PyLongObject + assert(!(value == -1 && PyErr_Occurred())); + + if (!overflow) { + export_long->value = value; + export_long->negative = 0; + export_long->ndigits = 0; + export_long->digits = 0; + export_long->_reserved = 0; + } + else { + export_long->value = 0; + export_long->negative = _PyLong_Sign(obj) < 0; + export_long->ndigits = _PyLong_DigitCount(self); + if (export_long->ndigits == 0) { + export_long->ndigits = 1; + } + export_long->digits = _PyLong_GetDigits(self); + export_long->_reserved = (Py_uintptr_t)Py_NewRef(obj); + } + return 0; +} + +static inline void +PyLong_FreeExport(PyLongExport *export_long) +{ + PyObject *obj = (PyObject*)export_long->_reserved; + + if (obj) { + export_long->_reserved = 0; + Py_DECREF(obj); + } +} + +static inline PyLongWriter* +PyLongWriter_Create(int negative, Py_ssize_t ndigits, void **digits) +{ + if (ndigits <= 0) { + PyErr_SetString(PyExc_ValueError, "ndigits must be positive"); + return NULL; + } + assert(digits != NULL); + + PyLongObject *obj = _PyLong_New(ndigits); + if (obj == NULL) { + return NULL; + } + _PyLong_SetSignAndDigitCount(obj, negative?-1:1, ndigits); + + *digits = _PyLong_GetDigits(obj); + return (PyLongWriter*)obj; +} + +static inline void +PyLongWriter_Discard(PyLongWriter *writer) +{ + PyLongObject *obj = (PyLongObject *)writer; + + assert(Py_REFCNT(obj) == 1); + Py_DECREF(obj); +} + +static inline PyObject* +PyLongWriter_Finish(PyLongWriter *writer) +{ + PyObject *obj = (PyObject *)writer; + PyLongObject *self = (PyLongObject*)obj; + Py_ssize_t j = _PyLong_DigitCount(self); + Py_ssize_t i = j; + int sign = _PyLong_Sign(obj); + + assert(Py_REFCNT(obj) == 1); + + // Normalize and get singleton if possible + while (i > 0 && _PyLong_GetDigits(self)[i-1] == 0) { + --i; + } + if (i != j) { + if (i == 0) { + sign = 0; + } + _PyLong_SetSignAndDigitCount(self, sign, i); + } + if (i <= 1) { + long val = sign * (long)(_PyLong_GetDigits(self)[0]); + Py_DECREF(obj); + return PyLong_FromLong(val); + } + + return obj; +} +#endif + + +#if PY_VERSION_HEX < 0x030C00A3 +# define Py_T_SHORT T_SHORT +# define Py_T_INT T_INT +# define Py_T_LONG T_LONG +# define Py_T_FLOAT T_FLOAT +# define Py_T_DOUBLE T_DOUBLE +# define Py_T_STRING T_STRING +# define _Py_T_OBJECT T_OBJECT +# define Py_T_CHAR T_CHAR +# define Py_T_BYTE T_BYTE +# define Py_T_UBYTE T_UBYTE +# define Py_T_USHORT T_USHORT +# define Py_T_UINT T_UINT +# define Py_T_ULONG T_ULONG +# define Py_T_STRING_INPLACE T_STRING_INPLACE +# define Py_T_BOOL T_BOOL +# define Py_T_OBJECT_EX T_OBJECT_EX +# define Py_T_LONGLONG T_LONGLONG +# define Py_T_ULONGLONG T_ULONGLONG +# define Py_T_PYSSIZET T_PYSSIZET + +# if PY_VERSION_HEX >= 0x03000000 && !defined(PYPY_VERSION) +# define _Py_T_NONE T_NONE +# endif + +# define Py_READONLY READONLY +# define Py_AUDIT_READ READ_RESTRICTED +# define _Py_WRITE_RESTRICTED PY_WRITE_RESTRICTED +#endif + + +// gh-127350 added Py_fopen() and Py_fclose() to Python 3.14a4 +#if PY_VERSION_HEX < 0x030E00A4 +static inline FILE* Py_fopen(PyObject *path, const char *mode) +{ +#if 0x030400A2 <= PY_VERSION_HEX && !defined(PYPY_VERSION) + PyAPI_FUNC(FILE*) _Py_fopen_obj(PyObject *path, const char *mode); + + return _Py_fopen_obj(path, mode); +#else + FILE *f; + PyObject *bytes; +#if PY_VERSION_HEX >= 0x03000000 + if (!PyUnicode_FSConverter(path, &bytes)) { + return NULL; + } +#else + if (!PyString_Check(path)) { + PyErr_SetString(PyExc_TypeError, "except str"); + return NULL; + } + bytes = Py_NewRef(path); +#endif + const char *path_bytes = PyBytes_AS_STRING(bytes); + + f = fopen(path_bytes, mode); + Py_DECREF(bytes); + + if (f == NULL) { + PyErr_SetFromErrnoWithFilenameObject(PyExc_OSError, path); + return NULL; + } + return f; +#endif +} + +static inline int Py_fclose(FILE *file) +{ + return fclose(file); +} +#endif + + +#if 0x03090000 <= PY_VERSION_HEX && PY_VERSION_HEX < 0x030E0000 && !defined(PYPY_VERSION) +static inline PyObject* +PyConfig_Get(const char *name) +{ + typedef enum { + _PyConfig_MEMBER_INT, + _PyConfig_MEMBER_UINT, + _PyConfig_MEMBER_ULONG, + _PyConfig_MEMBER_BOOL, + _PyConfig_MEMBER_WSTR, + _PyConfig_MEMBER_WSTR_OPT, + _PyConfig_MEMBER_WSTR_LIST, + } PyConfigMemberType; + + typedef struct { + const char *name; + size_t offset; + PyConfigMemberType type; + const char *sys_attr; + } PyConfigSpec; + +#define PYTHONCAPI_COMPAT_SPEC(MEMBER, TYPE, sys_attr) \ + {#MEMBER, offsetof(PyConfig, MEMBER), \ + _PyConfig_MEMBER_##TYPE, sys_attr} + + static const PyConfigSpec config_spec[] = { + PYTHONCAPI_COMPAT_SPEC(argv, WSTR_LIST, "argv"), + PYTHONCAPI_COMPAT_SPEC(base_exec_prefix, WSTR_OPT, "base_exec_prefix"), + PYTHONCAPI_COMPAT_SPEC(base_executable, WSTR_OPT, "_base_executable"), + PYTHONCAPI_COMPAT_SPEC(base_prefix, WSTR_OPT, "base_prefix"), + PYTHONCAPI_COMPAT_SPEC(bytes_warning, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(exec_prefix, WSTR_OPT, "exec_prefix"), + PYTHONCAPI_COMPAT_SPEC(executable, WSTR_OPT, "executable"), + PYTHONCAPI_COMPAT_SPEC(inspect, BOOL, _Py_NULL), +#if 0x030C0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(int_max_str_digits, UINT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(interactive, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(module_search_paths, WSTR_LIST, "path"), + PYTHONCAPI_COMPAT_SPEC(optimization_level, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(parser_debug, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(platlibdir, WSTR, "platlibdir"), + PYTHONCAPI_COMPAT_SPEC(prefix, WSTR_OPT, "prefix"), + PYTHONCAPI_COMPAT_SPEC(pycache_prefix, WSTR_OPT, "pycache_prefix"), + PYTHONCAPI_COMPAT_SPEC(quiet, BOOL, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(stdlib_dir, WSTR_OPT, "_stdlib_dir"), +#endif + PYTHONCAPI_COMPAT_SPEC(use_environment, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(verbose, UINT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(warnoptions, WSTR_LIST, "warnoptions"), + PYTHONCAPI_COMPAT_SPEC(write_bytecode, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(xoptions, WSTR_LIST, "_xoptions"), + PYTHONCAPI_COMPAT_SPEC(buffered_stdio, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(check_hash_pycs_mode, WSTR, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(code_debug_ranges, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(configure_c_stdio, BOOL, _Py_NULL), +#if 0x030D0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(cpu_count, INT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(dev_mode, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(dump_refs, BOOL, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(dump_refs_file, WSTR_OPT, _Py_NULL), +#endif +#ifdef Py_GIL_DISABLED + PYTHONCAPI_COMPAT_SPEC(enable_gil, INT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(faulthandler, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(filesystem_encoding, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(filesystem_errors, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(hash_seed, ULONG, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(home, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(import_time, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(install_signal_handlers, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(isolated, BOOL, _Py_NULL), +#ifdef MS_WINDOWS + PYTHONCAPI_COMPAT_SPEC(legacy_windows_stdio, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(malloc_stats, BOOL, _Py_NULL), +#if 0x030A0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(orig_argv, WSTR_LIST, "orig_argv"), +#endif + PYTHONCAPI_COMPAT_SPEC(parse_argv, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(pathconfig_warnings, BOOL, _Py_NULL), +#if 0x030C0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(perf_profiling, UINT, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(program_name, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_command, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_filename, WSTR_OPT, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(run_module, WSTR_OPT, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(safe_path, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(show_ref_count, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(site_import, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(skip_source_first_line, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(stdio_encoding, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(stdio_errors, WSTR, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(tracemalloc, UINT, _Py_NULL), +#if 0x030B0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(use_frozen_modules, BOOL, _Py_NULL), +#endif + PYTHONCAPI_COMPAT_SPEC(use_hash_seed, BOOL, _Py_NULL), + PYTHONCAPI_COMPAT_SPEC(user_site_directory, BOOL, _Py_NULL), +#if 0x030A0000 <= PY_VERSION_HEX + PYTHONCAPI_COMPAT_SPEC(warn_default_encoding, BOOL, _Py_NULL), +#endif + }; + +#undef PYTHONCAPI_COMPAT_SPEC + + const PyConfigSpec *spec; + int found = 0; + for (size_t i=0; i < sizeof(config_spec) / sizeof(config_spec[0]); i++) { + spec = &config_spec[i]; + if (strcmp(spec->name, name) == 0) { + found = 1; + break; + } + } + if (found) { + if (spec->sys_attr != NULL) { + PyObject *value = PySys_GetObject(spec->sys_attr); + if (value == NULL) { + PyErr_Format(PyExc_RuntimeError, "lost sys.%s", spec->sys_attr); + return NULL; + } + return Py_NewRef(value); + } + + PyAPI_FUNC(const PyConfig*) _Py_GetConfig(void); + + const PyConfig *config = _Py_GetConfig(); + void *member = (char *)config + spec->offset; + switch (spec->type) { + case _PyConfig_MEMBER_INT: + case _PyConfig_MEMBER_UINT: + { + int value = *(int *)member; + return PyLong_FromLong(value); + } + case _PyConfig_MEMBER_BOOL: + { + int value = *(int *)member; + return PyBool_FromLong(value != 0); + } + case _PyConfig_MEMBER_ULONG: + { + unsigned long value = *(unsigned long *)member; + return PyLong_FromUnsignedLong(value); + } + case _PyConfig_MEMBER_WSTR: + case _PyConfig_MEMBER_WSTR_OPT: + { + wchar_t *wstr = *(wchar_t **)member; + if (wstr != NULL) { + return PyUnicode_FromWideChar(wstr, -1); + } + else { + return Py_NewRef(Py_None); + } + } + case _PyConfig_MEMBER_WSTR_LIST: + { + const PyWideStringList *list = (const PyWideStringList *)member; + PyObject *tuple = PyTuple_New(list->length); + if (tuple == NULL) { + return NULL; + } + + for (Py_ssize_t i = 0; i < list->length; i++) { + PyObject *item = PyUnicode_FromWideChar(list->items[i], -1); + if (item == NULL) { + Py_DECREF(tuple); + return NULL; + } + PyTuple_SET_ITEM(tuple, i, item); + } + return tuple; + } + default: + Py_UNREACHABLE(); + } + } + + PyErr_Format(PyExc_ValueError, "unknown config option name: %s", name); + return NULL; +} + +static inline int +PyConfig_GetInt(const char *name, int *value) +{ + PyObject *obj = PyConfig_Get(name); + if (obj == NULL) { + return -1; + } + + if (!PyLong_Check(obj)) { + Py_DECREF(obj); + PyErr_Format(PyExc_TypeError, "config option %s is not an int", name); + return -1; + } + + int as_int = PyLong_AsInt(obj); + Py_DECREF(obj); + if (as_int == -1 && PyErr_Occurred()) { + PyErr_Format(PyExc_OverflowError, + "config option %s value does not fit into a C int", name); + return -1; + } + + *value = as_int; + return 0; +} +#endif // PY_VERSION_HEX > 0x03090000 && !defined(PYPY_VERSION) + + +#if PY_VERSION_HEX < 0x030F0000 +static inline PyObject* +PySys_GetAttrString(const char *name) +{ +#if PY_VERSION_HEX >= 0x03000000 + PyObject *value = Py_XNewRef(PySys_GetObject(name)); +#else + PyObject *value = Py_XNewRef(PySys_GetObject((char*)name)); +#endif + if (value != NULL) { + return value; + } + if (!PyErr_Occurred()) { + PyErr_Format(PyExc_RuntimeError, "lost sys.%s", name); + } + return NULL; +} + +static inline PyObject* +PySys_GetAttr(PyObject *name) +{ +#if PY_VERSION_HEX >= 0x03000000 + const char *name_str = PyUnicode_AsUTF8(name); +#else + const char *name_str = PyString_AsString(name); +#endif + if (name_str == NULL) { + return NULL; + } + + return PySys_GetAttrString(name_str); +} + +static inline int +PySys_GetOptionalAttrString(const char *name, PyObject **value) +{ +#if PY_VERSION_HEX >= 0x03000000 + *value = Py_XNewRef(PySys_GetObject(name)); +#else + *value = Py_XNewRef(PySys_GetObject((char*)name)); +#endif + if (*value != NULL) { + return 1; + } + return 0; +} + +static inline int +PySys_GetOptionalAttr(PyObject *name, PyObject **value) +{ +#if PY_VERSION_HEX >= 0x03000000 + const char *name_str = PyUnicode_AsUTF8(name); +#else + const char *name_str = PyString_AsString(name); +#endif + if (name_str == NULL) { + *value = NULL; + return -1; + } + + return PySys_GetOptionalAttrString(name_str, value); +} +#endif // PY_VERSION_HEX < 0x030F00A1 + + +#ifdef __cplusplus +} +#endif +#endif // PYTHONCAPI_COMPAT diff --git a/src/propcache/_propcachelib/state.h b/src/propcache/_propcachelib/state.h new file mode 100644 index 0000000..2f799f6 --- /dev/null +++ b/src/propcache/_propcachelib/state.h @@ -0,0 +1,94 @@ +#ifndef __STATE_H__ +#define __STATE_H__ + +#include +#ifdef __cplusplus +extern "C" { +#endif + +/****************** Module State ****************/ + +typedef struct _mod_state { + PyTypeObject* UnderCachedPropertyType; + PyTypeObject* CachedPropertyType; +} mod_state; + + +// Multidict's library was a really good template so +// no need to revient the wheel here... + +static inline mod_state* get_mod_state(PyObject* mod){ + mod_state* state = (mod_state*)PyModule_GetState(mod); + assert(state != NULL); + return state; +}; + +static inline mod_state * +get_mod_state_by_cls(PyTypeObject *cls) +{ + mod_state *state = (mod_state *)PyType_GetModuleState(cls); + assert(state != NULL); + return state; +} + +#if PY_VERSION_HEX < 0x030b0000 +PyObject * +PyType_GetModuleByDef(PyTypeObject *tp, PyModuleDef *def) +{ + PyModuleDef *mod_def; + if (!PyType_HasFeature(tp, Py_TPFLAGS_HEAPTYPE)) { + goto err; + } + PyObject *mod = NULL; + + mod = PyType_GetModule(tp); + if (mod == NULL) { + PyErr_Clear(); + } else { + mod_def = PyModule_GetDef(mod); + if (mod_def == def) { + return mod; + } + } + + PyObject *mro = tp->tp_mro; + assert(mro != NULL); + assert(PyTuple_Check(mro)); + assert(PyTuple_GET_SIZE(mro) >= 1); + assert(PyTuple_GET_ITEM(mro, 0) == (PyObject *)tp); + + Py_ssize_t n = PyTuple_GET_SIZE(mro); + for (Py_ssize_t i = 1; i < n; i++) { + PyObject *super = PyTuple_GET_ITEM(mro, i); + if (!PyType_HasFeature((PyTypeObject *)super, Py_TPFLAGS_HEAPTYPE)) { + continue; + } + mod = PyType_GetModule((PyTypeObject *)super); + if (mod == NULL) { + PyErr_Clear(); + } else { + mod_def = PyModule_GetDef(mod); + if (mod_def == def) { + return mod; + } + } + } + +err: + PyErr_Format( + PyExc_TypeError, + "PyType_GetModuleByDef: No superclass of '%s' has the given module", + tp->tp_name); + return NULL; +} +#endif + + +static PyModuleDef propcache_module; + + +#ifdef __cplusplus +} +#endif + +#endif // __STATE_H__ \ No newline at end of file From bc73761b2b7e8d4163e59b71f36fa609d689d07a Mon Sep 17 00:00:00 2001 From: Vizonex <114684698+Vizonex@users.noreply.github.com> Date: Tue, 24 Jun 2025 23:56:02 -0500 Subject: [PATCH 2/4] upload tools --- tools/upgrade_pythoncapi.py | 748 ++++++++++++++++++++++++++++++++++++ 1 file changed, 748 insertions(+) create mode 100644 tools/upgrade_pythoncapi.py diff --git a/tools/upgrade_pythoncapi.py b/tools/upgrade_pythoncapi.py new file mode 100644 index 0000000..23c1418 --- /dev/null +++ b/tools/upgrade_pythoncapi.py @@ -0,0 +1,748 @@ +#!/usr/bin/env python3 +import argparse +import os +import re +import urllib.request +import sys + + +MIN_PYTHON = (2, 7) + + +PYTHONCAPI_COMPAT_URL = ('https://raw.githubusercontent.com/python/' + 'pythoncapi-compat/main/pythoncapi_compat.h') +PYTHONCAPI_COMPAT_H = 'pythoncapi_compat.h' +INCLUDE_PYTHONCAPI_COMPAT = f'#include "{PYTHONCAPI_COMPAT_H}"' +INCLUDE_PYTHONCAPI_COMPAT2 = f'#include <{PYTHONCAPI_COMPAT_H}>' + +C_FILE_EXT = ( + # C language + ".c", ".h", + # C++ language + ".cc", ".cpp", ".cxx", ".hpp", +) +IGNORE_DIRS = (".git", ".tox") + + +# Match spaces but not newline characters. +# Similar to \s but exclude newline characters and only look for ASCII spaces +SPACE_REGEX = r'[ \t\f\v]' +# Match the end of a line: newline characters of a single line +NEWLINE_REGEX = r'(?:\n|\r|\r\n)' +# Match the indentation at the beginning of a line +INDENTATION_REGEX = fr'^{SPACE_REGEX}*' + + +# Match a C identifier: 'identifier', 'var_3', 'NameCamelCase', '_var' +# Use \b to only match a full word: match "a_b", but not just "b" in "a_b". +ID_REGEX = r'\b[a-zA-Z_][a-zA-Z0-9_]*\b' +# Match 'array[3]' +SUBEXPR_REGEX = fr'{ID_REGEX}(?:\[[^]]+\])*' +# Match a C expression like "frame", "frame.attr", "obj->attr" or "*obj". +# Don't match functions calls like "func()". +EXPR_REGEX = (fr"\*?" # "*" prefix + fr"{SUBEXPR_REGEX}" # "var" + fr"(?:(?:->|\.){SUBEXPR_REGEX})*") # "->attr" or ".attr" + +# # Match 'PyObject *var' and 'struct MyStruct* var' +TYPE_PTR_REGEX = fr'{ID_REGEX} *\*' + +# Match '(PyObject*)' and nothing +OPT_CAST_REGEX = fr'(?:\({TYPE_PTR_REGEX} *\){SPACE_REGEX}*)?' + + +def same_indentation(group): + # the regex must have re.MULTILINE flag + return fr'{SPACE_REGEX}*(?:{NEWLINE_REGEX}{group})?' + + +def get_member_regex_str(member): + # Match "var->member". + return fr'\b({EXPR_REGEX}) *-> *{member}\b' + + +def get_member_regex(member): + # Match "var->member" (get). + # Don't match "var->member = value" (set). + # Don't match "Py_CLEAR(var->member)". + # Only "Py_CLEAR(" exact string is excluded. + regex = (r'(?member = expr;". + regex = assign_regex_str(get_member_regex_str(member), r'([^=].*)') + return re.compile(regex) + + +def call_assign_regex(name): + # Match "Py_TYPE(expr) = expr;". + # Don't match "assert(Py_TYPE(expr) == expr);". + # Tolerate spaces + regex = fr'{name} *\( *(.+) *\) *= *([^=].*) *;' + return re.compile(regex) + + +def is_c_filename(filename): + return filename.endswith(C_FILE_EXT) + + +class Operation: + NAME = "" + REPLACE = () + NEED_PYTHONCAPI_COMPAT = False + + def __init__(self, patcher): + self.patcher = patcher + + def patch(self, content): + old_content = content + for regex, replace in self.REPLACE: + content = regex.sub(replace, content) + if content != old_content and self.NEED_PYTHONCAPI_COMPAT: + content = self.patcher.add_pythoncapi_compat(content) + return content + + +class Py_TYPE(Operation): + NAME = "Py_TYPE" + REPLACE = ( + (get_member_regex('ob_type'), r'Py_TYPE(\1)'), + ) + # Py_TYPE() was added to Python 2.6. + + +class Py_SIZE(Operation): + NAME = "Py_SIZE" + REPLACE = ( + (get_member_regex('ob_size'), r'Py_SIZE(\1)'), + ) + # Py_SIZE() was added to Python 2.6. + + +class Py_REFCNT(Operation): + NAME = "Py_REFCNT" + REPLACE = ( + (get_member_regex('ob_refcnt'), r'Py_REFCNT(\1)'), + ) + # Py_REFCNT() was added to Python 2.6. + + +class Py_SET_TYPE(Operation): + NAME = "Py_SET_TYPE" + REPLACE = ( + (call_assign_regex('Py_TYPE'), r'Py_SET_TYPE(\1, \2);'), + (set_member_regex('ob_type'), r'Py_SET_TYPE(\1, \2);'), + ) + # Need Py_SET_TYPE(): new in Python 3.9. + NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + + +class Py_SET_SIZE(Operation): + NAME = "Py_SET_SIZE" + REPLACE = ( + (call_assign_regex('Py_SIZE'), r'Py_SET_SIZE(\1, \2);'), + (set_member_regex('ob_size'), r'Py_SET_SIZE(\1, \2);'), + ) + # Need Py_SET_SIZE(): new in Python 3.9. + NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + + +class Py_SET_REFCNT(Operation): + NAME = "Py_SET_REFCNT" + REPLACE = ( + (call_assign_regex('Py_REFCNT'), r'Py_SET_REFCNT(\1, \2);'), + (set_member_regex('ob_refcnt'), r'Py_SET_REFCNT(\1, \2);'), + ) + # Need Py_SET_REFCNT(): new in Python 3.9. + NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + + +class PyObject_NEW(Operation): + NAME = "PyObject_NEW" + # In Python 3.9, the PyObject_NEW() macro becomes an alias to the + # PyObject_New() macro, and the PyObject_NEW_VAR() macro becomes an alias + # to the PyObject_NewVar() macro. + REPLACE = ( + (re.compile(r"\bPyObject_NEW\b( *\()"), r'PyObject_New\1'), + (re.compile(r"\bPyObject_NEW_VAR\b( *\()"), r'PyObject_NewVar\1'), + ) + + +class PyMem_MALLOC(Operation): + NAME = "PyMem_MALLOC" + # In Python 3.9, the PyObject_NEW() macro becomes an alias to the + # PyObject_New() macro, and the PyObject_NEW_VAR() macro becomes an alias + # to the PyObject_NewVar() macro. + + REPLACE = ( + (re.compile(r"\bPyMem_MALLOC\b( *\()"), r'PyMem_Malloc\1'), + (re.compile(r"\bPyMem_REALLOC\b( *\()"), r'PyMem_Realloc\1'), + (re.compile(r"\bPyMem_FREE\b( *\()"), r'PyMem_Free\1'), + (re.compile(r"\bPyMem_Del\b( *\()"), r'PyMem_Free\1'), + (re.compile(r"\bPyMem_DEL\b( *\()"), r'PyMem_Free\1'), + ) + + +class PyObject_MALLOC(Operation): + NAME = "PyObject_MALLOC" + # In Python 3.9, the PyObject_NEW() macro becomes an alias to the + # PyObject_New() macro, and the PyObject_NEW_VAR() macro becomes an alias + # to the PyObject_NewVar() macro. + + REPLACE = ( + (re.compile(r"\bPyObject_MALLOC\b( *\()"), r'PyObject_Malloc\1'), + (re.compile(r"\bPyObject_REALLOC\b( *\()"), r'PyObject_Realloc\1'), + (re.compile(r"\bPyObject_FREE\b( *\()"), r'PyObject_Free\1'), + (re.compile(r"\bPyObject_Del\b( *\()"), r'PyObject_Free\1'), + (re.compile(r"\bPyObject_DEL\b( *\()"), r'PyObject_Free\1'), + ) + + +class PyFrame_GetBack(Operation): + NAME = "PyFrame_GetBack" + REPLACE = ( + (get_member_regex('f_back'), r'_PyFrame_GetBackBorrow(\1)'), + ) + # Need _PyFrame_GetBackBorrow() (PyFrame_GetBack() is new in Python 3.9) + NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + + +class PyFrame_GetCode(Operation): + NAME = "PyFrame_GetCode" + + REPLACE = ( + (get_member_regex('f_code'), r'_PyFrame_GetCodeBorrow(\1)'), + ) + # Need _PyFrame_GetCodeBorrow() (PyFrame_GetCode() is new in Python 3.9) + NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + + +class PyThreadState_GetInterpreter(Operation): + NAME = "PyThreadState_GetInterpreter" + REPLACE = ( + (get_member_regex('interp'), r'PyThreadState_GetInterpreter(\1)'), + ) + # Need PyThreadState_GetInterpreter() (new in Python 3.9) + NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + + +class PyThreadState_GetFrame(Operation): + NAME = "PyThreadState_GetFrame" + REPLACE = ( + (get_member_regex('frame'), r'_PyThreadState_GetFrameBorrow(\1)'), + ) + # Need _PyThreadState_GetFrameBorrow() + # (PyThreadState_GetFrame() is new in Python 3.9) + NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + + +class Py_NewRef(Operation): + NAME = "Py_NewRef" + REPLACE = ( + # "Py_INCREF(x); return x;" => "return Py_NewRef(x);" + # "Py_XINCREF(x); return x;" => "return Py_XNewRef(x);" + # The two statements must be at the same indentation, otherwise the + # regex does not match. + (re.compile(fr'({INDENTATION_REGEX})' + + fr'Py_(X?)INCREF\(({EXPR_REGEX})\)\s*;' + + same_indentation(r'\1') + + fr'return {OPT_CAST_REGEX}\3;', + re.MULTILINE), + r'\1return Py_\2NewRef(\3);'), + + # Same regex than the previous one, + # but the two statements are on the same line. + (re.compile(fr'Py_(X?)INCREF\(({EXPR_REGEX})\)\s*;' + + fr'{SPACE_REGEX}*' + + fr'return {OPT_CAST_REGEX}\2;', + re.MULTILINE), + r'return Py_\1NewRef(\2);'), + + # "Py_INCREF(x); y = x;" must be replaced before + # "y = x; Py_INCREF(y);", to not miss consecutive + # "Py_INCREF; assign; Py_INCREF; assign; ..." (see unit tests). + + # "Py_INCREF(x); y = x;" => "y = Py_NewRef(x)" + # "Py_XINCREF(x); y = x;" => "y = Py_XNewRef(x)" + # The two statements must have the same indentation, otherwise the + # regex does not match. + (re.compile(fr'({INDENTATION_REGEX})' + + fr'Py_(X?)INCREF\(({EXPR_REGEX})\);' + + same_indentation(r'\1') + + assign_regex_str(fr'({EXPR_REGEX})', + fr'{OPT_CAST_REGEX}\3'), + re.MULTILINE), + r'\1\4 = Py_\2NewRef(\3);'), + + # Same regex than the previous one, + # but the two statements are on the same line. + (re.compile(fr'Py_(X?)INCREF\(({EXPR_REGEX})\);' + + fr'{SPACE_REGEX}*' + + assign_regex_str(fr'({EXPR_REGEX})', + fr'{OPT_CAST_REGEX}\2')), + r'\3 = Py_\1NewRef(\2);'), + + # "y = x; Py_INCREF(x);" => "y = Py_NewRef(x);" + # "y = x; Py_INCREF(y);" => "y = Py_NewRef(x);" + # "y = x; Py_XINCREF(x);" => "y = Py_XNewRef(x);" + # "y = x; Py_XINCREF(y);" => "y = Py_XNewRef(x);" + # "y = (PyObject*)x; Py_XINCREF(y);" => "y = Py_XNewRef(x);" + # The two statements must have the same indentation, otherwise the + # regex does not match. + (re.compile(fr'({INDENTATION_REGEX})' + + assign_regex_str(fr'({EXPR_REGEX})', + fr'{OPT_CAST_REGEX}({EXPR_REGEX})') + + same_indentation(r'\1') + + r'Py_(X?)INCREF\((?:\2|\3)\);', + re.MULTILINE), + r'\1\2 = Py_\4NewRef(\3);'), + + # Same regex than the previous one, + # but the two statements are on the same line. + (re.compile(assign_regex_str(fr'({EXPR_REGEX})', + fr'{OPT_CAST_REGEX}({EXPR_REGEX})') + + fr'{SPACE_REGEX}*' + + r'Py_(X?)INCREF\((?:\1|\2)\);'), + r'\1 = Py_\3NewRef(\2);'), + + # "PyObject *var = x; Py_INCREF(x);" => "PyObject *var = Py_NewRef(x);" + # The two statements must have the same indentation, otherwise the + # regex does not match. + (re.compile(fr'({INDENTATION_REGEX})' + # "type* var = expr;" + + assign_regex_str(fr'({TYPE_PTR_REGEX} *)({EXPR_REGEX})', + fr'({OPT_CAST_REGEX})({EXPR_REGEX})') + + same_indentation(r'\1') + # "Py_INCREF(var);" + + r'Py_(X?)INCREF\((?:\3|\5)\);', + re.MULTILINE), + r'\1\2\3 = \4Py_\6NewRef(\5);'), + + # "Py_INCREF(x); PyObject *var = x;" => "PyObject *var = Py_NewRef(x);" + # The two statements must have the same indentation, otherwise the + # regex does not match. + (re.compile(fr'({INDENTATION_REGEX})' + # "Py_INCREF(var);" + + fr'Py_(X?)INCREF\(({EXPR_REGEX})\);' + + same_indentation(r'\1') + # "type* var = expr;" + + assign_regex_str(fr'({TYPE_PTR_REGEX} *{EXPR_REGEX})', + fr'({OPT_CAST_REGEX})\3'), + re.MULTILINE), + r'\1\4 = \5Py_\2NewRef(\3);'), + ) + # Need Py_NewRef(): new in Python 3.10 + NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 10)) + + +class Py_CLEAR(Operation): + NAME = "Py_CLEAR" + REPLACE = ( + # "Py_XDECREF(x); x = NULL;" => "Py_CLEAR(x)"; + # The two statements must have the same indentation, otherwise the + # regex does not match. + (re.compile(fr'({INDENTATION_REGEX})' + + fr'Py_XDECREF\(({EXPR_REGEX})\) *;' + + same_indentation(r'\1') + + assign_regex_str(r'\2', r'NULL'), + re.MULTILINE), + r'\1Py_CLEAR(\2);'), + + # "Py_XDECREF(x); x = NULL;" => "Py_CLEAR(x)"; + (re.compile(fr'Py_XDECREF\(({EXPR_REGEX})\) *;' + + fr'{SPACE_REGEX}*' + + assign_regex_str(r'\1', r'NULL')), + r'Py_CLEAR(\1);'), + ) + + +SETREF_VALUE = fr'{OPT_CAST_REGEX}(?:{EXPR_REGEX}|Py_X?NewRef\({EXPR_REGEX}\))' + + +class Py_SETREF(Operation): + NAME = "Py_SETREF" + REPLACE = ( + # "Py_INCREF(y); Py_CLEAR(x); x = y;" => "Py_XSETREF(x, y)"; + # Statements must have the same indentation, otherwise the regex does + # not match. + (re.compile(fr'({INDENTATION_REGEX})' + + fr'Py_(X?)INCREF\(({EXPR_REGEX})\) *;' + + same_indentation(r'\1') + + fr'Py_CLEAR\(({EXPR_REGEX})\) *;' + + same_indentation(r'\1') + + assign_regex_str(r'\4', r'\3'), + re.MULTILINE), + r'\1Py_XSETREF(\4, Py_\2NewRef(\3));'), + + # "Py_CLEAR(x); x = y;" => "Py_XSETREF(x, y)"; + # Statements must have the same indentation, otherwise the regex does + # not match. + (re.compile(fr'({INDENTATION_REGEX})' + + fr'Py_CLEAR\(({EXPR_REGEX})\) *;' + + same_indentation(r'\1') + + assign_regex_str(r'\2', + fr'({SETREF_VALUE})'), + re.MULTILINE), + r'\1Py_XSETREF(\2, \3);'), + + # "Py_INCREF(y); Py_DECREF(x); x = y;" => "Py_SETREF(x, y)"; + # Statements must have the same indentation, otherwise the regex does + # not match. + (re.compile(fr'({INDENTATION_REGEX})' + + fr'Py_(X?)INCREF\(({EXPR_REGEX})\) *;' + + same_indentation(r'\1') + + fr'Py_(X?)DECREF\(({EXPR_REGEX})\) *;' + + same_indentation(r'\1') + + assign_regex_str(r'\5', r'\3'), + re.MULTILINE), + r'\1Py_\4SETREF(\5, Py_\2NewRef(\3));'), + + # "Py_DECREF(x); x = y;" => "Py_SETREF(x, y)"; + # "Py_DECREF(x); x = Py_NewRef(y);" => "Py_SETREF(x, Py_NewRef(y))"; + # Statements must have the same indentation, otherwise the regex does + # not match. + (re.compile(fr'({INDENTATION_REGEX})' + + fr'Py_(X?)DECREF\(({EXPR_REGEX})\) *;' + + same_indentation(r'\1') + + assign_regex_str(r'\3', + fr'({SETREF_VALUE})'), + re.MULTILINE), + r'\1Py_\2SETREF(\3, \4);'), + + # "old = var; var = new; Py_DECREF(old);" => "Py_SETREF(var, new);" + # "PyObject *old = var; var = new; Py_DECREF(old);" => "Py_SETREF(var, new);" + # Statements must have the same indentation, otherwise the regex does + # not match. + (re.compile(fr'({INDENTATION_REGEX})' + + fr'(?:{ID_REGEX} *\* *)?({ID_REGEX}) *= *{OPT_CAST_REGEX}({EXPR_REGEX}) *;' + + same_indentation(r'\1') + + assign_regex_str(r'\3', + fr'({SETREF_VALUE})') + + same_indentation(r'\1') + + fr'Py_(X?)DECREF\(\2\) *;', + re.MULTILINE), + r'\1Py_\5SETREF(\3, \4);'), + ) + # Need Py_NewRef(): new in Python 3.5 + NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 5)) + + +class Py_Is(Operation): + NAME = "Py_Is" + + def replace2(regs): + x = regs.group(1) + y = regs.group(2) + if y == 'NULL': + return regs.group(0) + return f'{x} = _Py_StealRef({y});' + + REPLACE = [] + expr = fr'({EXPR_REGEX})' + for name in ('None', 'True', 'False'): + REPLACE.extend(( + (re.compile(fr'{expr} == Py_{name}\b'), + fr'Py_Is{name}(\1)'), + (re.compile(fr'{expr} != Py_{name}\b'), + fr'!Py_Is{name}(\1)'), + )) + + # Need Py_IsNone(), Py_IsTrue(), Py_IsFalse(): new in Python 3.10 + NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 10)) + + +OPERATIONS = ( + Py_SET_TYPE, + Py_SET_SIZE, + Py_SET_REFCNT, + # Py_SET_xxx must be run before Py_xxx + Py_TYPE, + Py_SIZE, + Py_REFCNT, + + Py_Is, + + PyObject_NEW, + PyMem_MALLOC, + PyObject_MALLOC, + + PyFrame_GetBack, + PyFrame_GetCode, + + PyThreadState_GetInterpreter, + PyThreadState_GetFrame, + + # Code style: excluded from "all" + Py_NewRef, + Py_CLEAR, + Py_SETREF, +) + +EXCLUDE_FROM_ALL = ( + Py_NewRef, + Py_CLEAR, + Py_SETREF, +) + + +def all_operations(): + return set(operation_class.NAME for operation_class in OPERATIONS + if operation_class not in EXCLUDE_FROM_ALL) + + +class Patcher: + def __init__(self, args=None): + self.exitcode = 0 + self.pythoncapi_compat_added = 0 + self.want_pythoncapi_compat = False + self.operations = None + self.applied_operations = set() + + # Set temporariliy by patch() + self._has_pythoncapi_compat = None + self._applied_operations = None + + self._parse_options(args) + + def log(self, msg=''): + print(msg, file=sys.stderr, flush=True) + + def warning(self, msg): + self.log(f"WARNING: {msg}") + + def _get_operations(self, parser): + args_names = self.args.operations.split(',') + + wanted = set() + for name in args_names: + name = name.strip() + if not name: + continue + + if name == "all": + wanted |= all_operations() + elif name.startswith("-"): + name = name[1:] + wanted.discard(name) + else: + wanted.add(name) + + operations = [] + for operation_class in OPERATIONS: + name = operation_class.NAME + if name not in wanted: + continue + wanted.discard(name) + operation = operation_class(self) + operations.append(operation) + + if wanted: + print(f"invalid operations: {','.join(wanted)}") + print() + self.usage(parser) + sys.exit(1) + + return operations + + def add_line(self, content, line): + line = line + '\n' + # FIXME: tolerate trailing spaces + if line not in content: + # FIXME: add macro after the first header comment + # FIXME: add macro after includes + # FIXME: add macro after: #define PY_SSIZE_T_CLEAN + return line + '\n' + content + else: + return content + + def add_pythoncapi_compat(self, content): + if self._has_pythoncapi_compat: + return content + content = self.add_line(content, INCLUDE_PYTHONCAPI_COMPAT) + self._has_pythoncapi_compat = True + self.pythoncapi_compat_added += 1 + return content + + def _patch(self, content): + try: + has = (self.args.no_compat + or INCLUDE_PYTHONCAPI_COMPAT in content + or INCLUDE_PYTHONCAPI_COMPAT2 in content) + self._has_pythoncapi_compat = has + self._applied_operations = [] + for operation in self.operations: + new_content = operation.patch(content) + if new_content != content: + self._applied_operations.append(operation.NAME) + content = new_content + applied_operations = self._applied_operations + finally: + self._has_pythoncapi_compat = None + self._applied_operations = None + return (content, applied_operations) + + def patch(self, content): + return self._patch(content)[0] + + def patch_file(self, filename): + if os.path.basename(filename) == PYTHONCAPI_COMPAT_H: + self.log(f"Skip {filename}") + return + + encoding = "utf-8" + errors = "surrogateescape" + + with open(filename, encoding=encoding, errors=errors) as fp: + old_contents = fp.read() + + new_contents, operations = self._patch(old_contents) + + if self.args.to_stdout: + print(new_contents, end="") + return (new_contents != old_contents) + + # Don't rewrite if the filename for in-place replacement, + # to avoid changing the file modification time. + if new_contents == old_contents: + return False + + if not self.args.no_backup: + old_filename = filename + ".old" + # If old_filename already exists, replace it + os.replace(filename, old_filename) + + with open(filename, "w", encoding=encoding, errors=errors) as fp: + fp.write(new_contents) + + self.applied_operations |= set(operations) + operations = ', '.join(operations) + self.log(f"Patched file: {filename} ({operations})") + return True + + def _walk_dir(self, path): + empty = True + + for dirpath, dirnames, filenames in os.walk(path): + # Don't walk into .tox + for ignore_name in IGNORE_DIRS: + try: + dirnames.remove(ignore_name) + except ValueError: + pass + for filename in filenames: + if is_c_filename(filename): + yield os.path.join(dirpath, filename) + empty = False + + if empty: + self.warning(f"Directory {path} doesn't contain any C file") + self.exitcode = 1 + + def walk(self, paths): + for path in paths: + if os.path.isdir(path): + for filename in self._walk_dir(path): + yield filename + elif os.path.exists(path): + yield path + else: + self.warning(f"Path {path} does not exist") + self.exitcode = 1 + + def get_latest_header(self, base_dir): + target = os.path.join(base_dir, PYTHONCAPI_COMPAT_H) + self.log(f"Download the file from {PYTHONCAPI_COMPAT_URL} to {target}.") + urllib.request.urlretrieve(PYTHONCAPI_COMPAT_URL, target) + + @staticmethod + def usage(parser): + parser.print_help() + print() + print("Operations:") + print() + for operation in sorted(OPERATIONS, + key=lambda operation: operation.NAME.lower()): + print(f"- {operation.NAME}") + print() + print("If a directory is passed, search for .c and .h files " + "in subdirectories.") + + def _parse_dir_path(self, path): + if os.path.isdir(path): + return path + else: + raise argparse.ArgumentTypeError(f"{path} is not a valid path") + + def _parse_options(self, args): + parser = argparse.ArgumentParser( + description="Upgrade C extension modules to newer Python C API") + parser.add_argument( + '-o', '--operations', action="store", + default="all", + help='Space separated list of operation names to apply') + parser.add_argument( + '-q', '--quiet', action="store_true", + help='Quiet mode') + parser.add_argument( + '-c', '--to-stdout', action="store_true", + help='Write output into stdout instead of modifying files ' + 'in-place (imply quiet mode)') + parser.add_argument( + '-B', '--no-backup', action="store_true", + help="Don't create .old backup files") + parser.add_argument( + '-C', '--no-compat', action="store_true", + help=f"Don't add: {INCLUDE_PYTHONCAPI_COMPAT}") + parser.add_argument( + '-d', '--download', metavar='PATH', + help=f'Download latest pythoncapi_compat.h file to designated PATH', + type=self._parse_dir_path) + parser.add_argument( + metavar='file_or_directory', dest="paths", nargs='*') + + args = parser.parse_args(args) + if not args.paths and not args.download: + self.usage(parser) + sys.exit(1) + + if args.to_stdout: + args.quiet = True + + self.args = args + self.operations = self._get_operations(parser) + + def main(self): + if self.args.paths: + for filename in self.walk(self.args.paths): + self.patch_file(filename) + + if self.applied_operations: + nops = len(self.applied_operations) + ops = ', '.join(sorted(self.applied_operations)) + self.log() + self.log(f"Applied operations ({nops}): {ops}") + + if self.args.download: + path = self.args.download + self.get_latest_header(path) + + if self.pythoncapi_compat_added and not self.args.quiet: + self.log() + self.log(f"{INCLUDE_PYTHONCAPI_COMPAT} added: you may have " + f"to copy {PYTHONCAPI_COMPAT_H} to your project") + self.log("Run 'python upgrade_pythoncapi.py --download '") + + sys.exit(self.exitcode) + + +if __name__ == "__main__": + Patcher().main() From 755a983a03c5281f304158df6676ea806751eb29 Mon Sep 17 00:00:00 2001 From: Vizonex <114684698+Vizonex@users.noreply.github.com> Date: Tue, 24 Jun 2025 23:59:15 -0500 Subject: [PATCH 3/4] Create setup.py --- setup.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..f7b6481 --- /dev/null +++ b/setup.py @@ -0,0 +1,11 @@ +from setuptools import setup, Extension +# XXX: No idea where this would go in a config file so I am putting it here... +setup( + ext_modules=[ + Extension( + "propcache._propcache_c", + ["src/propcache/_propcache_c.c"], + include_dirs=["src/propcache"], + ) + ] +) From 07436b75cb98d0cb3fe94f80670a9492dd6b4e89 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 25 Jun 2025 05:18:37 +0000 Subject: [PATCH 4/4] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- setup.py | 3 +- src/propcache/_propcache_c.c | 16 +- src/propcache/_propcache_c.pyi | 10 +- src/propcache/_propcachelib/pc.h | 24 +- .../_propcachelib/propcache_objects.h | 4 +- src/propcache/_propcachelib/state.h | 2 +- tools/upgrade_pythoncapi.py | 539 ++++++++++-------- 7 files changed, 326 insertions(+), 272 deletions(-) diff --git a/setup.py b/setup.py index f7b6481..ea8f0fe 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ -from setuptools import setup, Extension +from setuptools import Extension, setup + # XXX: No idea where this would go in a config file so I am putting it here... setup( ext_modules=[ diff --git a/src/propcache/_propcache_c.c b/src/propcache/_propcache_c.c index 56b2b48..ae9d91f 100644 --- a/src/propcache/_propcache_c.c +++ b/src/propcache/_propcache_c.c @@ -1,4 +1,4 @@ -/* _propcache_c.c +/* _propcache_c.c * * A replacement of the cython _helper_c.pyx with a few more optimized * parts for better care and speed... @@ -43,14 +43,14 @@ under_cached_property_set__doc__(UnderCachedPropertyObject* self, PyObject* new }; -static PyObject* +static PyObject* under_cached_property__get__(UnderCachedPropertyObject* self, PyObject* inst, void* Py_UNUSED(owner)){ return uc_prop__get__(self, inst); } /* under_cached_property is immutable */ -static PyObject* +static PyObject* under_cached_property__set__(UnderCachedPropertyObject* self, void* Py_UNUSED(inst), void* Py_UNUSED(owner)){ PyErr_SetString(PyExc_AttributeError, "cached property is read-only"); return NULL; @@ -112,7 +112,7 @@ static PyMemberDef under_cached_property_members[] = { Py_READONLY, }, {NULL} /* Sentinel */ -}; +}; static PyMethodDef under_cached_property_methods[] = { {"__class_getitem__", (PyCFunction)Py_GenericAlias, METH_O | METH_CLASS,NULL}, @@ -147,7 +147,7 @@ static PyType_Spec UnderCachedPropertySpec = { /*************************** CachedPropertyObject **************************/ /* An optimized version of functool's cached-property */ -/* For more info on how this object works it's recommended to see +/* For more info on how this object works it's recommended to see * Either functool's version or _propcache/pc.h */ @@ -170,7 +170,7 @@ cached_property__set_name__(CachedPropertyObject* self, PyObject *const *args, P return c_prop__set_name__(self, args[0]); } -static PyObject* +static PyObject* cached_property__get__(CachedPropertyObject* self, PyObject* inst, void* Py_UNUSED(owner)){ return c_prop__get__(self, inst); }; @@ -346,11 +346,11 @@ static PyModuleDef propcache_c_module = { .m_slots = module_slots, .m_traverse = module_traverse, .m_clear = module_clear, - .m_free = (freefunc)module_free, + .m_free = (freefunc)module_free, }; PyMODINIT_FUNC PyInit__propcache_c(void) { return PyModuleDef_Init(&propcache_c_module); -} \ No newline at end of file +} diff --git a/src/propcache/_propcache_c.pyi b/src/propcache/_propcache_c.pyi index 841bdf9..0073ab9 100644 --- a/src/propcache/_propcache_c.pyi +++ b/src/propcache/_propcache_c.pyi @@ -19,11 +19,11 @@ class _CacheImpl(Protocol[_Cache]): class cached_property(Generic[_T]): func: Callable[[Any], _T] name: str - def __init__(self, func:Callable[[Any], _T]) -> None: ... + def __init__(self, func: Callable[[Any], _T]) -> None: ... @classmethod def __class_getitem__(cls, *args, **kwargs): ... - def __get__(self, instance:_T, owner:Type[Any]) -> _T: ... - def __set_name__(self, name:str, owner:Type[Any]): ... + def __get__(self, instance: _T, owner: Type[Any]) -> _T: ... + def __set_name__(self, name: str, owner: Type[Any]): ... class under_cached_property(Generic[_T]): name: str @@ -36,5 +36,7 @@ class under_cached_property(Generic[_T]): def __get__(self, inst: None, owner: type[object] | None = None) -> Self: ... @overload def __get__(self, inst: _CacheImpl[Any], owner: type[object] | None = None) -> _T: ... # type: ignore[misc] - def __get__(self, inst: _CacheImpl[Any] | None, owner: type[object] | None = None): ... + def __get__( + self, inst: _CacheImpl[Any] | None, owner: type[object] | None = None + ): ... def __set__(self, inst: _CacheImpl[Any] | None, value: Any): ... diff --git a/src/propcache/_propcachelib/pc.h b/src/propcache/_propcachelib/pc.h index 37c4420..d2d4814 100644 --- a/src/propcache/_propcachelib/pc.h +++ b/src/propcache/_propcachelib/pc.h @@ -13,8 +13,8 @@ extern "C" { /************* under_cached_property / cached_property methods ********************/ -/* OTHER NOTES: - * - Made sense to not use -1 to express failure and rather handle everything in +/* OTHER NOTES: + * - Made sense to not use -1 to express failure and rather handle everything in * True/False Fashion don't see the day this gets requested to be wrapped as a c-api * module but incase requested this is why true/false is utilized */ @@ -36,7 +36,7 @@ __propcache_set_func_doc(PyObject* func, PyObject* doc){ } // cached / under_cached use the same caching mechanism -// so combining the two under an inlined funciton made +// so combining the two under an inlined funciton made // perfect sense... static inline PyObject* @@ -54,7 +54,7 @@ __propcache_get_value(PyObject* name, PyObject* func, PyObject* inst, const char } else { Py_INCREF(val); } - // TODO: Try Looking into returning val with Py_NewRef + // TODO: Try Looking into returning val with Py_NewRef return val; } @@ -70,7 +70,7 @@ uc_prop_init(UnderCachedPropertyObject* self, PyObject* wrapped){ if (!PyCallable_Check(wrapped)){ PyErr_Format( PyExc_TypeError, - "wrapped method named %R must be callable", + "wrapped method named %R must be callable", name ); return 0; @@ -84,7 +84,7 @@ uc_prop_init(UnderCachedPropertyObject* self, PyObject* wrapped){ -static inline PyObject* +static inline PyObject* uc_prop__get__(UnderCachedPropertyObject* self, PyObject* inst){ if (inst == NULL || Py_IsNone(inst)){ return (PyObject*)self; @@ -99,7 +99,7 @@ uc_prop__get__(UnderCachedPropertyObject* self, PyObject* inst){ static inline void c_prop_init(CachedPropertyObject* self, PyObject* func){ - /* In the old cython version we used None This time + /* In the old cython version we used None This time We can now utilize NULL to speedup checking in __set_name__ */ self->func = func; self->name = NULL; @@ -122,12 +122,12 @@ c_prop__set_name__(CachedPropertyObject* self, PyObject* name){ PyErr_Format( PyExc_TypeError, "Cannot assign the same cached_property to two different names %R and %R", - self->name, + self->name, name ); return 0; } - + default : { /* Error but likely for a different reason */ return 0; @@ -136,7 +136,7 @@ c_prop__set_name__(CachedPropertyObject* self, PyObject* name){ } } -static inline PyObject* +static inline PyObject* c_prop__get__(CachedPropertyObject* self, PyObject* inst){ if (inst == NULL || (Py_IsNone(inst))){ return (PyObject*)self; @@ -149,7 +149,7 @@ c_prop__get__(CachedPropertyObject* self, PyObject* inst){ ); return NULL; } - /* Incase object was using __slots__ instead of __dict__ + /* Incase object was using __slots__ instead of __dict__ * PyObject_GetAttrString will catch it and throw an error */ return __propcache_get_value(self->name, self->func, inst, "__dict__"); } @@ -160,4 +160,4 @@ c_prop__get__(CachedPropertyObject* self, PyObject* inst){ #endif -#endif // __PC_H__ \ No newline at end of file +#endif // __PC_H__ diff --git a/src/propcache/_propcachelib/propcache_objects.h b/src/propcache/_propcachelib/propcache_objects.h index 2d64e65..6279db5 100644 --- a/src/propcache/_propcachelib/propcache_objects.h +++ b/src/propcache/_propcachelib/propcache_objects.h @@ -21,7 +21,7 @@ typedef struct _under_cached_property_object { PyObject* name; PyObject* doc; // TODO: Maybe implement __isabstract__ for @abc.abstractmethod() - // PyObject* isabstract; + // PyObject* isabstract; } UnderCachedPropertyObject; @@ -39,4 +39,4 @@ typedef struct _cached_property { }; #endif -#endif // __PROPCACHE_OBJECTS_H__ \ No newline at end of file +#endif // __PROPCACHE_OBJECTS_H__ diff --git a/src/propcache/_propcachelib/state.h b/src/propcache/_propcachelib/state.h index 2f799f6..2458a5d 100644 --- a/src/propcache/_propcachelib/state.h +++ b/src/propcache/_propcachelib/state.h @@ -91,4 +91,4 @@ static PyModuleDef propcache_module; } #endif -#endif // __STATE_H__ \ No newline at end of file +#endif // __STATE_H__ diff --git a/tools/upgrade_pythoncapi.py b/tools/upgrade_pythoncapi.py index 23c1418..a02fd92 100644 --- a/tools/upgrade_pythoncapi.py +++ b/tools/upgrade_pythoncapi.py @@ -2,63 +2,70 @@ import argparse import os import re -import urllib.request import sys - +import urllib.request MIN_PYTHON = (2, 7) -PYTHONCAPI_COMPAT_URL = ('https://raw.githubusercontent.com/python/' - 'pythoncapi-compat/main/pythoncapi_compat.h') -PYTHONCAPI_COMPAT_H = 'pythoncapi_compat.h' +PYTHONCAPI_COMPAT_URL = ( + "https://raw.githubusercontent.com/python/" + "pythoncapi-compat/main/pythoncapi_compat.h" +) +PYTHONCAPI_COMPAT_H = "pythoncapi_compat.h" INCLUDE_PYTHONCAPI_COMPAT = f'#include "{PYTHONCAPI_COMPAT_H}"' -INCLUDE_PYTHONCAPI_COMPAT2 = f'#include <{PYTHONCAPI_COMPAT_H}>' +INCLUDE_PYTHONCAPI_COMPAT2 = f"#include <{PYTHONCAPI_COMPAT_H}>" C_FILE_EXT = ( # C language - ".c", ".h", + ".c", + ".h", # C++ language - ".cc", ".cpp", ".cxx", ".hpp", + ".cc", + ".cpp", + ".cxx", + ".hpp", ) IGNORE_DIRS = (".git", ".tox") # Match spaces but not newline characters. # Similar to \s but exclude newline characters and only look for ASCII spaces -SPACE_REGEX = r'[ \t\f\v]' +SPACE_REGEX = r"[ \t\f\v]" # Match the end of a line: newline characters of a single line -NEWLINE_REGEX = r'(?:\n|\r|\r\n)' +NEWLINE_REGEX = r"(?:\n|\r|\r\n)" # Match the indentation at the beginning of a line -INDENTATION_REGEX = fr'^{SPACE_REGEX}*' +INDENTATION_REGEX = rf"^{SPACE_REGEX}*" # Match a C identifier: 'identifier', 'var_3', 'NameCamelCase', '_var' # Use \b to only match a full word: match "a_b", but not just "b" in "a_b". -ID_REGEX = r'\b[a-zA-Z_][a-zA-Z0-9_]*\b' +ID_REGEX = r"\b[a-zA-Z_][a-zA-Z0-9_]*\b" # Match 'array[3]' -SUBEXPR_REGEX = fr'{ID_REGEX}(?:\[[^]]+\])*' +SUBEXPR_REGEX = rf"{ID_REGEX}(?:\[[^]]+\])*" # Match a C expression like "frame", "frame.attr", "obj->attr" or "*obj". # Don't match functions calls like "func()". -EXPR_REGEX = (fr"\*?" # "*" prefix - fr"{SUBEXPR_REGEX}" # "var" - fr"(?:(?:->|\.){SUBEXPR_REGEX})*") # "->attr" or ".attr" +EXPR_REGEX = ( + rf"\*?" # "*" prefix + rf"{SUBEXPR_REGEX}" # "var" + rf"(?:(?:->|\.){SUBEXPR_REGEX})*" +) # "->attr" or ".attr" # # Match 'PyObject *var' and 'struct MyStruct* var' -TYPE_PTR_REGEX = fr'{ID_REGEX} *\*' +TYPE_PTR_REGEX = rf"{ID_REGEX} *\*" # Match '(PyObject*)' and nothing -OPT_CAST_REGEX = fr'(?:\({TYPE_PTR_REGEX} *\){SPACE_REGEX}*)?' +OPT_CAST_REGEX = rf"(?:\({TYPE_PTR_REGEX} *\){SPACE_REGEX}*)?" def same_indentation(group): # the regex must have re.MULTILINE flag - return fr'{SPACE_REGEX}*(?:{NEWLINE_REGEX}{group})?' + return rf"{SPACE_REGEX}*(?:{NEWLINE_REGEX}{group})?" def get_member_regex_str(member): # Match "var->member". - return fr'\b({EXPR_REGEX}) *-> *{member}\b' + return rf"\b({EXPR_REGEX}) *-> *{member}\b" def get_member_regex(member): @@ -66,20 +73,18 @@ def get_member_regex(member): # Don't match "var->member = value" (set). # Don't match "Py_CLEAR(var->member)". # Only "Py_CLEAR(" exact string is excluded. - regex = (r'(?member = expr;". - regex = assign_regex_str(get_member_regex_str(member), r'([^=].*)') + regex = assign_regex_str(get_member_regex_str(member), r"([^=].*)") return re.compile(regex) @@ -87,7 +92,7 @@ def call_assign_regex(name): # Match "Py_TYPE(expr) = expr;". # Don't match "assert(Py_TYPE(expr) == expr);". # Tolerate spaces - regex = fr'{name} *\( *(.+) *\) *= *([^=].*) *;' + regex = rf"{name} *\( *(.+) *\) *= *([^=].*) *;" return re.compile(regex) @@ -114,56 +119,50 @@ def patch(self, content): class Py_TYPE(Operation): NAME = "Py_TYPE" - REPLACE = ( - (get_member_regex('ob_type'), r'Py_TYPE(\1)'), - ) + REPLACE = ((get_member_regex("ob_type"), r"Py_TYPE(\1)"),) # Py_TYPE() was added to Python 2.6. class Py_SIZE(Operation): NAME = "Py_SIZE" - REPLACE = ( - (get_member_regex('ob_size'), r'Py_SIZE(\1)'), - ) + REPLACE = ((get_member_regex("ob_size"), r"Py_SIZE(\1)"),) # Py_SIZE() was added to Python 2.6. class Py_REFCNT(Operation): NAME = "Py_REFCNT" - REPLACE = ( - (get_member_regex('ob_refcnt'), r'Py_REFCNT(\1)'), - ) + REPLACE = ((get_member_regex("ob_refcnt"), r"Py_REFCNT(\1)"),) # Py_REFCNT() was added to Python 2.6. class Py_SET_TYPE(Operation): NAME = "Py_SET_TYPE" REPLACE = ( - (call_assign_regex('Py_TYPE'), r'Py_SET_TYPE(\1, \2);'), - (set_member_regex('ob_type'), r'Py_SET_TYPE(\1, \2);'), + (call_assign_regex("Py_TYPE"), r"Py_SET_TYPE(\1, \2);"), + (set_member_regex("ob_type"), r"Py_SET_TYPE(\1, \2);"), ) # Need Py_SET_TYPE(): new in Python 3.9. - NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + NEED_PYTHONCAPI_COMPAT = MIN_PYTHON < (3, 9) class Py_SET_SIZE(Operation): NAME = "Py_SET_SIZE" REPLACE = ( - (call_assign_regex('Py_SIZE'), r'Py_SET_SIZE(\1, \2);'), - (set_member_regex('ob_size'), r'Py_SET_SIZE(\1, \2);'), + (call_assign_regex("Py_SIZE"), r"Py_SET_SIZE(\1, \2);"), + (set_member_regex("ob_size"), r"Py_SET_SIZE(\1, \2);"), ) # Need Py_SET_SIZE(): new in Python 3.9. - NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + NEED_PYTHONCAPI_COMPAT = MIN_PYTHON < (3, 9) class Py_SET_REFCNT(Operation): NAME = "Py_SET_REFCNT" REPLACE = ( - (call_assign_regex('Py_REFCNT'), r'Py_SET_REFCNT(\1, \2);'), - (set_member_regex('ob_refcnt'), r'Py_SET_REFCNT(\1, \2);'), + (call_assign_regex("Py_REFCNT"), r"Py_SET_REFCNT(\1, \2);"), + (set_member_regex("ob_refcnt"), r"Py_SET_REFCNT(\1, \2);"), ) # Need Py_SET_REFCNT(): new in Python 3.9. - NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + NEED_PYTHONCAPI_COMPAT = MIN_PYTHON < (3, 9) class PyObject_NEW(Operation): @@ -172,8 +171,8 @@ class PyObject_NEW(Operation): # PyObject_New() macro, and the PyObject_NEW_VAR() macro becomes an alias # to the PyObject_NewVar() macro. REPLACE = ( - (re.compile(r"\bPyObject_NEW\b( *\()"), r'PyObject_New\1'), - (re.compile(r"\bPyObject_NEW_VAR\b( *\()"), r'PyObject_NewVar\1'), + (re.compile(r"\bPyObject_NEW\b( *\()"), r"PyObject_New\1"), + (re.compile(r"\bPyObject_NEW_VAR\b( *\()"), r"PyObject_NewVar\1"), ) @@ -184,11 +183,11 @@ class PyMem_MALLOC(Operation): # to the PyObject_NewVar() macro. REPLACE = ( - (re.compile(r"\bPyMem_MALLOC\b( *\()"), r'PyMem_Malloc\1'), - (re.compile(r"\bPyMem_REALLOC\b( *\()"), r'PyMem_Realloc\1'), - (re.compile(r"\bPyMem_FREE\b( *\()"), r'PyMem_Free\1'), - (re.compile(r"\bPyMem_Del\b( *\()"), r'PyMem_Free\1'), - (re.compile(r"\bPyMem_DEL\b( *\()"), r'PyMem_Free\1'), + (re.compile(r"\bPyMem_MALLOC\b( *\()"), r"PyMem_Malloc\1"), + (re.compile(r"\bPyMem_REALLOC\b( *\()"), r"PyMem_Realloc\1"), + (re.compile(r"\bPyMem_FREE\b( *\()"), r"PyMem_Free\1"), + (re.compile(r"\bPyMem_Del\b( *\()"), r"PyMem_Free\1"), + (re.compile(r"\bPyMem_DEL\b( *\()"), r"PyMem_Free\1"), ) @@ -199,50 +198,42 @@ class PyObject_MALLOC(Operation): # to the PyObject_NewVar() macro. REPLACE = ( - (re.compile(r"\bPyObject_MALLOC\b( *\()"), r'PyObject_Malloc\1'), - (re.compile(r"\bPyObject_REALLOC\b( *\()"), r'PyObject_Realloc\1'), - (re.compile(r"\bPyObject_FREE\b( *\()"), r'PyObject_Free\1'), - (re.compile(r"\bPyObject_Del\b( *\()"), r'PyObject_Free\1'), - (re.compile(r"\bPyObject_DEL\b( *\()"), r'PyObject_Free\1'), + (re.compile(r"\bPyObject_MALLOC\b( *\()"), r"PyObject_Malloc\1"), + (re.compile(r"\bPyObject_REALLOC\b( *\()"), r"PyObject_Realloc\1"), + (re.compile(r"\bPyObject_FREE\b( *\()"), r"PyObject_Free\1"), + (re.compile(r"\bPyObject_Del\b( *\()"), r"PyObject_Free\1"), + (re.compile(r"\bPyObject_DEL\b( *\()"), r"PyObject_Free\1"), ) class PyFrame_GetBack(Operation): NAME = "PyFrame_GetBack" - REPLACE = ( - (get_member_regex('f_back'), r'_PyFrame_GetBackBorrow(\1)'), - ) + REPLACE = ((get_member_regex("f_back"), r"_PyFrame_GetBackBorrow(\1)"),) # Need _PyFrame_GetBackBorrow() (PyFrame_GetBack() is new in Python 3.9) - NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + NEED_PYTHONCAPI_COMPAT = MIN_PYTHON < (3, 9) class PyFrame_GetCode(Operation): NAME = "PyFrame_GetCode" - REPLACE = ( - (get_member_regex('f_code'), r'_PyFrame_GetCodeBorrow(\1)'), - ) + REPLACE = ((get_member_regex("f_code"), r"_PyFrame_GetCodeBorrow(\1)"),) # Need _PyFrame_GetCodeBorrow() (PyFrame_GetCode() is new in Python 3.9) - NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + NEED_PYTHONCAPI_COMPAT = MIN_PYTHON < (3, 9) class PyThreadState_GetInterpreter(Operation): NAME = "PyThreadState_GetInterpreter" - REPLACE = ( - (get_member_regex('interp'), r'PyThreadState_GetInterpreter(\1)'), - ) + REPLACE = ((get_member_regex("interp"), r"PyThreadState_GetInterpreter(\1)"),) # Need PyThreadState_GetInterpreter() (new in Python 3.9) - NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + NEED_PYTHONCAPI_COMPAT = MIN_PYTHON < (3, 9) class PyThreadState_GetFrame(Operation): NAME = "PyThreadState_GetFrame" - REPLACE = ( - (get_member_regex('frame'), r'_PyThreadState_GetFrameBorrow(\1)'), - ) + REPLACE = ((get_member_regex("frame"), r"_PyThreadState_GetFrameBorrow(\1)"),) # Need _PyThreadState_GetFrameBorrow() # (PyThreadState_GetFrame() is new in Python 3.9) - NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 9)) + NEED_PYTHONCAPI_COMPAT = MIN_PYTHON < (3, 9) class Py_NewRef(Operation): @@ -252,45 +243,54 @@ class Py_NewRef(Operation): # "Py_XINCREF(x); return x;" => "return Py_XNewRef(x);" # The two statements must be at the same indentation, otherwise the # regex does not match. - (re.compile(fr'({INDENTATION_REGEX})' - + fr'Py_(X?)INCREF\(({EXPR_REGEX})\)\s*;' - + same_indentation(r'\1') - + fr'return {OPT_CAST_REGEX}\3;', - re.MULTILINE), - r'\1return Py_\2NewRef(\3);'), - + ( + re.compile( + rf"({INDENTATION_REGEX})" + + rf"Py_(X?)INCREF\(({EXPR_REGEX})\)\s*;" + + same_indentation(r"\1") + + rf"return {OPT_CAST_REGEX}\3;", + re.MULTILINE, + ), + r"\1return Py_\2NewRef(\3);", + ), # Same regex than the previous one, # but the two statements are on the same line. - (re.compile(fr'Py_(X?)INCREF\(({EXPR_REGEX})\)\s*;' - + fr'{SPACE_REGEX}*' - + fr'return {OPT_CAST_REGEX}\2;', - re.MULTILINE), - r'return Py_\1NewRef(\2);'), - + ( + re.compile( + rf"Py_(X?)INCREF\(({EXPR_REGEX})\)\s*;" + + rf"{SPACE_REGEX}*" + + rf"return {OPT_CAST_REGEX}\2;", + re.MULTILINE, + ), + r"return Py_\1NewRef(\2);", + ), # "Py_INCREF(x); y = x;" must be replaced before # "y = x; Py_INCREF(y);", to not miss consecutive # "Py_INCREF; assign; Py_INCREF; assign; ..." (see unit tests). - # "Py_INCREF(x); y = x;" => "y = Py_NewRef(x)" # "Py_XINCREF(x); y = x;" => "y = Py_XNewRef(x)" # The two statements must have the same indentation, otherwise the # regex does not match. - (re.compile(fr'({INDENTATION_REGEX})' - + fr'Py_(X?)INCREF\(({EXPR_REGEX})\);' - + same_indentation(r'\1') - + assign_regex_str(fr'({EXPR_REGEX})', - fr'{OPT_CAST_REGEX}\3'), - re.MULTILINE), - r'\1\4 = Py_\2NewRef(\3);'), - + ( + re.compile( + rf"({INDENTATION_REGEX})" + + rf"Py_(X?)INCREF\(({EXPR_REGEX})\);" + + same_indentation(r"\1") + + assign_regex_str(rf"({EXPR_REGEX})", rf"{OPT_CAST_REGEX}\3"), + re.MULTILINE, + ), + r"\1\4 = Py_\2NewRef(\3);", + ), # Same regex than the previous one, # but the two statements are on the same line. - (re.compile(fr'Py_(X?)INCREF\(({EXPR_REGEX})\);' - + fr'{SPACE_REGEX}*' - + assign_regex_str(fr'({EXPR_REGEX})', - fr'{OPT_CAST_REGEX}\2')), - r'\3 = Py_\1NewRef(\2);'), - + ( + re.compile( + rf"Py_(X?)INCREF\(({EXPR_REGEX})\);" + + rf"{SPACE_REGEX}*" + + assign_regex_str(rf"({EXPR_REGEX})", rf"{OPT_CAST_REGEX}\2") + ), + r"\3 = Py_\1NewRef(\2);", + ), # "y = x; Py_INCREF(x);" => "y = Py_NewRef(x);" # "y = x; Py_INCREF(y);" => "y = Py_NewRef(x);" # "y = x; Py_XINCREF(x);" => "y = Py_XNewRef(x);" @@ -298,50 +298,65 @@ class Py_NewRef(Operation): # "y = (PyObject*)x; Py_XINCREF(y);" => "y = Py_XNewRef(x);" # The two statements must have the same indentation, otherwise the # regex does not match. - (re.compile(fr'({INDENTATION_REGEX})' - + assign_regex_str(fr'({EXPR_REGEX})', - fr'{OPT_CAST_REGEX}({EXPR_REGEX})') - + same_indentation(r'\1') - + r'Py_(X?)INCREF\((?:\2|\3)\);', - re.MULTILINE), - r'\1\2 = Py_\4NewRef(\3);'), - + ( + re.compile( + rf"({INDENTATION_REGEX})" + + assign_regex_str( + rf"({EXPR_REGEX})", rf"{OPT_CAST_REGEX}({EXPR_REGEX})" + ) + + same_indentation(r"\1") + + r"Py_(X?)INCREF\((?:\2|\3)\);", + re.MULTILINE, + ), + r"\1\2 = Py_\4NewRef(\3);", + ), # Same regex than the previous one, # but the two statements are on the same line. - (re.compile(assign_regex_str(fr'({EXPR_REGEX})', - fr'{OPT_CAST_REGEX}({EXPR_REGEX})') - + fr'{SPACE_REGEX}*' - + r'Py_(X?)INCREF\((?:\1|\2)\);'), - r'\1 = Py_\3NewRef(\2);'), - + ( + re.compile( + assign_regex_str(rf"({EXPR_REGEX})", rf"{OPT_CAST_REGEX}({EXPR_REGEX})") + + rf"{SPACE_REGEX}*" + + r"Py_(X?)INCREF\((?:\1|\2)\);" + ), + r"\1 = Py_\3NewRef(\2);", + ), # "PyObject *var = x; Py_INCREF(x);" => "PyObject *var = Py_NewRef(x);" # The two statements must have the same indentation, otherwise the # regex does not match. - (re.compile(fr'({INDENTATION_REGEX})' - # "type* var = expr;" - + assign_regex_str(fr'({TYPE_PTR_REGEX} *)({EXPR_REGEX})', - fr'({OPT_CAST_REGEX})({EXPR_REGEX})') - + same_indentation(r'\1') - # "Py_INCREF(var);" - + r'Py_(X?)INCREF\((?:\3|\5)\);', - re.MULTILINE), - r'\1\2\3 = \4Py_\6NewRef(\5);'), - + ( + re.compile( + rf"({INDENTATION_REGEX})" + # "type* var = expr;" + + assign_regex_str( + rf"({TYPE_PTR_REGEX} *)({EXPR_REGEX})", + rf"({OPT_CAST_REGEX})({EXPR_REGEX})", + ) + + same_indentation(r"\1") + # "Py_INCREF(var);" + + r"Py_(X?)INCREF\((?:\3|\5)\);", + re.MULTILINE, + ), + r"\1\2\3 = \4Py_\6NewRef(\5);", + ), # "Py_INCREF(x); PyObject *var = x;" => "PyObject *var = Py_NewRef(x);" # The two statements must have the same indentation, otherwise the # regex does not match. - (re.compile(fr'({INDENTATION_REGEX})' - # "Py_INCREF(var);" - + fr'Py_(X?)INCREF\(({EXPR_REGEX})\);' - + same_indentation(r'\1') - # "type* var = expr;" - + assign_regex_str(fr'({TYPE_PTR_REGEX} *{EXPR_REGEX})', - fr'({OPT_CAST_REGEX})\3'), - re.MULTILINE), - r'\1\4 = \5Py_\2NewRef(\3);'), + ( + re.compile( + rf"({INDENTATION_REGEX})" + # "Py_INCREF(var);" + + rf"Py_(X?)INCREF\(({EXPR_REGEX})\);" + same_indentation(r"\1") + # "type* var = expr;" + + assign_regex_str( + rf"({TYPE_PTR_REGEX} *{EXPR_REGEX})", rf"({OPT_CAST_REGEX})\3" + ), + re.MULTILINE, + ), + r"\1\4 = \5Py_\2NewRef(\3);", + ), ) # Need Py_NewRef(): new in Python 3.10 - NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 10)) + NEED_PYTHONCAPI_COMPAT = MIN_PYTHON < (3, 10) class Py_CLEAR(Operation): @@ -350,22 +365,29 @@ class Py_CLEAR(Operation): # "Py_XDECREF(x); x = NULL;" => "Py_CLEAR(x)"; # The two statements must have the same indentation, otherwise the # regex does not match. - (re.compile(fr'({INDENTATION_REGEX})' - + fr'Py_XDECREF\(({EXPR_REGEX})\) *;' - + same_indentation(r'\1') - + assign_regex_str(r'\2', r'NULL'), - re.MULTILINE), - r'\1Py_CLEAR(\2);'), - + ( + re.compile( + rf"({INDENTATION_REGEX})" + + rf"Py_XDECREF\(({EXPR_REGEX})\) *;" + + same_indentation(r"\1") + + assign_regex_str(r"\2", r"NULL"), + re.MULTILINE, + ), + r"\1Py_CLEAR(\2);", + ), # "Py_XDECREF(x); x = NULL;" => "Py_CLEAR(x)"; - (re.compile(fr'Py_XDECREF\(({EXPR_REGEX})\) *;' - + fr'{SPACE_REGEX}*' - + assign_regex_str(r'\1', r'NULL')), - r'Py_CLEAR(\1);'), + ( + re.compile( + rf"Py_XDECREF\(({EXPR_REGEX})\) *;" + + rf"{SPACE_REGEX}*" + + assign_regex_str(r"\1", r"NULL") + ), + r"Py_CLEAR(\1);", + ), ) -SETREF_VALUE = fr'{OPT_CAST_REGEX}(?:{EXPR_REGEX}|Py_X?NewRef\({EXPR_REGEX}\))' +SETREF_VALUE = rf"{OPT_CAST_REGEX}(?:{EXPR_REGEX}|Py_X?NewRef\({EXPR_REGEX}\))" class Py_SETREF(Operation): @@ -374,66 +396,79 @@ class Py_SETREF(Operation): # "Py_INCREF(y); Py_CLEAR(x); x = y;" => "Py_XSETREF(x, y)"; # Statements must have the same indentation, otherwise the regex does # not match. - (re.compile(fr'({INDENTATION_REGEX})' - + fr'Py_(X?)INCREF\(({EXPR_REGEX})\) *;' - + same_indentation(r'\1') - + fr'Py_CLEAR\(({EXPR_REGEX})\) *;' - + same_indentation(r'\1') - + assign_regex_str(r'\4', r'\3'), - re.MULTILINE), - r'\1Py_XSETREF(\4, Py_\2NewRef(\3));'), - + ( + re.compile( + rf"({INDENTATION_REGEX})" + + rf"Py_(X?)INCREF\(({EXPR_REGEX})\) *;" + + same_indentation(r"\1") + + rf"Py_CLEAR\(({EXPR_REGEX})\) *;" + + same_indentation(r"\1") + + assign_regex_str(r"\4", r"\3"), + re.MULTILINE, + ), + r"\1Py_XSETREF(\4, Py_\2NewRef(\3));", + ), # "Py_CLEAR(x); x = y;" => "Py_XSETREF(x, y)"; # Statements must have the same indentation, otherwise the regex does # not match. - (re.compile(fr'({INDENTATION_REGEX})' - + fr'Py_CLEAR\(({EXPR_REGEX})\) *;' - + same_indentation(r'\1') - + assign_regex_str(r'\2', - fr'({SETREF_VALUE})'), - re.MULTILINE), - r'\1Py_XSETREF(\2, \3);'), - + ( + re.compile( + rf"({INDENTATION_REGEX})" + + rf"Py_CLEAR\(({EXPR_REGEX})\) *;" + + same_indentation(r"\1") + + assign_regex_str(r"\2", rf"({SETREF_VALUE})"), + re.MULTILINE, + ), + r"\1Py_XSETREF(\2, \3);", + ), # "Py_INCREF(y); Py_DECREF(x); x = y;" => "Py_SETREF(x, y)"; # Statements must have the same indentation, otherwise the regex does # not match. - (re.compile(fr'({INDENTATION_REGEX})' - + fr'Py_(X?)INCREF\(({EXPR_REGEX})\) *;' - + same_indentation(r'\1') - + fr'Py_(X?)DECREF\(({EXPR_REGEX})\) *;' - + same_indentation(r'\1') - + assign_regex_str(r'\5', r'\3'), - re.MULTILINE), - r'\1Py_\4SETREF(\5, Py_\2NewRef(\3));'), - + ( + re.compile( + rf"({INDENTATION_REGEX})" + + rf"Py_(X?)INCREF\(({EXPR_REGEX})\) *;" + + same_indentation(r"\1") + + rf"Py_(X?)DECREF\(({EXPR_REGEX})\) *;" + + same_indentation(r"\1") + + assign_regex_str(r"\5", r"\3"), + re.MULTILINE, + ), + r"\1Py_\4SETREF(\5, Py_\2NewRef(\3));", + ), # "Py_DECREF(x); x = y;" => "Py_SETREF(x, y)"; # "Py_DECREF(x); x = Py_NewRef(y);" => "Py_SETREF(x, Py_NewRef(y))"; # Statements must have the same indentation, otherwise the regex does # not match. - (re.compile(fr'({INDENTATION_REGEX})' - + fr'Py_(X?)DECREF\(({EXPR_REGEX})\) *;' - + same_indentation(r'\1') - + assign_regex_str(r'\3', - fr'({SETREF_VALUE})'), - re.MULTILINE), - r'\1Py_\2SETREF(\3, \4);'), - + ( + re.compile( + rf"({INDENTATION_REGEX})" + + rf"Py_(X?)DECREF\(({EXPR_REGEX})\) *;" + + same_indentation(r"\1") + + assign_regex_str(r"\3", rf"({SETREF_VALUE})"), + re.MULTILINE, + ), + r"\1Py_\2SETREF(\3, \4);", + ), # "old = var; var = new; Py_DECREF(old);" => "Py_SETREF(var, new);" # "PyObject *old = var; var = new; Py_DECREF(old);" => "Py_SETREF(var, new);" # Statements must have the same indentation, otherwise the regex does # not match. - (re.compile(fr'({INDENTATION_REGEX})' - + fr'(?:{ID_REGEX} *\* *)?({ID_REGEX}) *= *{OPT_CAST_REGEX}({EXPR_REGEX}) *;' - + same_indentation(r'\1') - + assign_regex_str(r'\3', - fr'({SETREF_VALUE})') - + same_indentation(r'\1') - + fr'Py_(X?)DECREF\(\2\) *;', - re.MULTILINE), - r'\1Py_\5SETREF(\3, \4);'), + ( + re.compile( + rf"({INDENTATION_REGEX})" + + rf"(?:{ID_REGEX} *\* *)?({ID_REGEX}) *= *{OPT_CAST_REGEX}({EXPR_REGEX}) *;" + + same_indentation(r"\1") + + assign_regex_str(r"\3", rf"({SETREF_VALUE})") + + same_indentation(r"\1") + + rf"Py_(X?)DECREF\(\2\) *;", + re.MULTILINE, + ), + r"\1Py_\5SETREF(\3, \4);", + ), ) # Need Py_NewRef(): new in Python 3.5 - NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 5)) + NEED_PYTHONCAPI_COMPAT = MIN_PYTHON < (3, 5) class Py_Is(Operation): @@ -442,22 +477,22 @@ class Py_Is(Operation): def replace2(regs): x = regs.group(1) y = regs.group(2) - if y == 'NULL': + if y == "NULL": return regs.group(0) - return f'{x} = _Py_StealRef({y});' + return f"{x} = _Py_StealRef({y});" REPLACE = [] - expr = fr'({EXPR_REGEX})' - for name in ('None', 'True', 'False'): - REPLACE.extend(( - (re.compile(fr'{expr} == Py_{name}\b'), - fr'Py_Is{name}(\1)'), - (re.compile(fr'{expr} != Py_{name}\b'), - fr'!Py_Is{name}(\1)'), - )) + expr = rf"({EXPR_REGEX})" + for name in ("None", "True", "False"): + REPLACE.extend( + ( + (re.compile(rf"{expr} == Py_{name}\b"), rf"Py_Is{name}(\1)"), + (re.compile(rf"{expr} != Py_{name}\b"), rf"!Py_Is{name}(\1)"), + ) + ) # Need Py_IsNone(), Py_IsTrue(), Py_IsFalse(): new in Python 3.10 - NEED_PYTHONCAPI_COMPAT = (MIN_PYTHON < (3, 10)) + NEED_PYTHONCAPI_COMPAT = MIN_PYTHON < (3, 10) OPERATIONS = ( @@ -468,19 +503,14 @@ def replace2(regs): Py_TYPE, Py_SIZE, Py_REFCNT, - Py_Is, - PyObject_NEW, PyMem_MALLOC, PyObject_MALLOC, - PyFrame_GetBack, PyFrame_GetCode, - PyThreadState_GetInterpreter, PyThreadState_GetFrame, - # Code style: excluded from "all" Py_NewRef, Py_CLEAR, @@ -495,8 +525,11 @@ def replace2(regs): def all_operations(): - return set(operation_class.NAME for operation_class in OPERATIONS - if operation_class not in EXCLUDE_FROM_ALL) + return { + operation_class.NAME + for operation_class in OPERATIONS + if operation_class not in EXCLUDE_FROM_ALL + } class Patcher: @@ -513,14 +546,14 @@ def __init__(self, args=None): self._parse_options(args) - def log(self, msg=''): + def log(self, msg=""): print(msg, file=sys.stderr, flush=True) def warning(self, msg): self.log(f"WARNING: {msg}") def _get_operations(self, parser): - args_names = self.args.operations.split(',') + args_names = self.args.operations.split(",") wanted = set() for name in args_names: @@ -554,13 +587,13 @@ def _get_operations(self, parser): return operations def add_line(self, content, line): - line = line + '\n' + line = line + "\n" # FIXME: tolerate trailing spaces if line not in content: # FIXME: add macro after the first header comment # FIXME: add macro after includes # FIXME: add macro after: #define PY_SSIZE_T_CLEAN - return line + '\n' + content + return line + "\n" + content else: return content @@ -574,9 +607,11 @@ def add_pythoncapi_compat(self, content): def _patch(self, content): try: - has = (self.args.no_compat - or INCLUDE_PYTHONCAPI_COMPAT in content - or INCLUDE_PYTHONCAPI_COMPAT2 in content) + has = ( + self.args.no_compat + or INCLUDE_PYTHONCAPI_COMPAT in content + or INCLUDE_PYTHONCAPI_COMPAT2 in content + ) self._has_pythoncapi_compat = has self._applied_operations = [] for operation in self.operations: @@ -608,7 +643,7 @@ def patch_file(self, filename): if self.args.to_stdout: print(new_contents, end="") - return (new_contents != old_contents) + return new_contents != old_contents # Don't rewrite if the filename for in-place replacement, # to avoid changing the file modification time. @@ -624,7 +659,7 @@ def patch_file(self, filename): fp.write(new_contents) self.applied_operations |= set(operations) - operations = ', '.join(operations) + operations = ", ".join(operations) self.log(f"Patched file: {filename} ({operations})") return True @@ -650,8 +685,7 @@ def _walk_dir(self, path): def walk(self, paths): for path in paths: if os.path.isdir(path): - for filename in self._walk_dir(path): - yield filename + yield from self._walk_dir(path) elif os.path.exists(path): yield path else: @@ -669,12 +703,14 @@ def usage(parser): print() print("Operations:") print() - for operation in sorted(OPERATIONS, - key=lambda operation: operation.NAME.lower()): + for operation in sorted( + OPERATIONS, key=lambda operation: operation.NAME.lower() + ): print(f"- {operation.NAME}") print() - print("If a directory is passed, search for .c and .h files " - "in subdirectories.") + print( + "If a directory is passed, search for .c and .h files " "in subdirectories." + ) def _parse_dir_path(self, path): if os.path.isdir(path): @@ -684,30 +720,43 @@ def _parse_dir_path(self, path): def _parse_options(self, args): parser = argparse.ArgumentParser( - description="Upgrade C extension modules to newer Python C API") + description="Upgrade C extension modules to newer Python C API" + ) parser.add_argument( - '-o', '--operations', action="store", + "-o", + "--operations", + action="store", default="all", - help='Space separated list of operation names to apply') - parser.add_argument( - '-q', '--quiet', action="store_true", - help='Quiet mode') - parser.add_argument( - '-c', '--to-stdout', action="store_true", - help='Write output into stdout instead of modifying files ' - 'in-place (imply quiet mode)') + help="Space separated list of operation names to apply", + ) + parser.add_argument("-q", "--quiet", action="store_true", help="Quiet mode") parser.add_argument( - '-B', '--no-backup', action="store_true", - help="Don't create .old backup files") + "-c", + "--to-stdout", + action="store_true", + help="Write output into stdout instead of modifying files " + "in-place (imply quiet mode)", + ) parser.add_argument( - '-C', '--no-compat', action="store_true", - help=f"Don't add: {INCLUDE_PYTHONCAPI_COMPAT}") + "-B", + "--no-backup", + action="store_true", + help="Don't create .old backup files", + ) parser.add_argument( - '-d', '--download', metavar='PATH', - help=f'Download latest pythoncapi_compat.h file to designated PATH', - type=self._parse_dir_path) + "-C", + "--no-compat", + action="store_true", + help=f"Don't add: {INCLUDE_PYTHONCAPI_COMPAT}", + ) parser.add_argument( - metavar='file_or_directory', dest="paths", nargs='*') + "-d", + "--download", + metavar="PATH", + help=f"Download latest pythoncapi_compat.h file to designated PATH", + type=self._parse_dir_path, + ) + parser.add_argument(metavar="file_or_directory", dest="paths", nargs="*") args = parser.parse_args(args) if not args.paths and not args.download: @@ -727,7 +776,7 @@ def main(self): if self.applied_operations: nops = len(self.applied_operations) - ops = ', '.join(sorted(self.applied_operations)) + ops = ", ".join(sorted(self.applied_operations)) self.log() self.log(f"Applied operations ({nops}): {ops}") @@ -737,8 +786,10 @@ def main(self): if self.pythoncapi_compat_added and not self.args.quiet: self.log() - self.log(f"{INCLUDE_PYTHONCAPI_COMPAT} added: you may have " - f"to copy {PYTHONCAPI_COMPAT_H} to your project") + self.log( + f"{INCLUDE_PYTHONCAPI_COMPAT} added: you may have " + f"to copy {PYTHONCAPI_COMPAT_H} to your project" + ) self.log("Run 'python upgrade_pythoncapi.py --download '") sys.exit(self.exitcode)