diff --git a/docs/api.rst b/docs/api.rst index 13a9d1b..f13d4b2 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -465,7 +465,9 @@ Python 3.13 See `Py_HashPointer() documentation `__. - Not available on the limited C API. +.. c:function:: Py_hash_t PyObject_GenericHash(PyObject *obj) + + See `PyObject_GenericHash() documentation `__. .. c:type:: PyTime_t @@ -533,7 +535,6 @@ Not supported: * ``PyLong_AsNativeBytes()`` * ``PyLong_FromNativeBytes()`` * ``PyLong_FromUnsignedNativeBytes()`` -* ``PyObject_GenericHash()``. * ``PySys_Audit()``. * ``PySys_AuditTuple()``. diff --git a/docs/changelog.rst b/docs/changelog.rst index db7fc9f..7e3f002 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,8 @@ Changelog ========= -* 2026-10-10: Add ``PyUnicodeWriter_WriteUCS4()`` function. +* 2026-10-10: Add ``PyObject_GenericHash()`` and + ``PyUnicodeWriter_WriteUCS4()`` functions. * 2026-10-08: Add support for the limited C API 3.2. * 2026-10-08: Add functions: diff --git a/pythoncapi_compat.h b/pythoncapi_compat.h index 0981dea..8ad5cc7 100644 --- a/pythoncapi_compat.h +++ b/pythoncapi_compat.h @@ -1236,14 +1236,39 @@ PyDict_PopString(PyObject *dict, const char *key, PyObject **result) // gh-111545 added Py_HashPointer() to Python 3.13 -#if PY_VERSION_HEX < _Py_PACK_VERSION(3, 13) && !defined(Py_LIMITED_API) +#if PY_VERSION_HEX < _Py_PACK_VERSION(3, 13) || defined(Py_LIMITED_API) static inline Py_hash_t Py_HashPointer(const void *ptr) { -#if PY_VERSION_HEX >= _Py_PACK_VERSION(3, 9) && !defined(PYPY_VERSION) +#if PY_VERSION_HEX >= _Py_PACK_VERSION(3, 9) && !defined(PYPY_VERSION) && !defined(Py_LIMITED_API) return _Py_HashPointer(ptr); -#else +#elif !defined(Py_LIMITED_API) + // PyPy _Py_HashPointer() argument type is "void*", not "const void*" return _Py_HashPointer(_Py_CAST(void*, ptr)); +#else + // Limited C API implementation + uintptr_t x = (uintptr_t)ptr; + Py_BUILD_ASSERT(sizeof(x) == sizeof(ptr)); + + // Bottom 3 or 4 bits are likely to be 0; rotate x by 4 to the right + // to avoid excessive hash collisions for dicts and sets. + x = (x >> 4) | (x << (8 * sizeof(uintptr_t) - 4)); + + if (x == (uintptr_t)-1) { + x = (uintptr_t)-2; + } + + Py_BUILD_ASSERT(sizeof(x) == sizeof(Py_hash_t)); + return (Py_hash_t)x; +#endif +} #endif + + +#if PY_VERSION_HEX < _Py_PACK_VERSION(3, 13) || defined(Py_LIMITED_API) +Py_hash_t +PyObject_GenericHash(PyObject *obj) +{ + return Py_HashPointer(obj); } #endif diff --git a/tests/test_pythoncapi_compat.py b/tests/test_pythoncapi_compat.py index be62b0b..aca6d67 100644 --- a/tests/test_pythoncapi_compat.py +++ b/tests/test_pythoncapi_compat.py @@ -173,7 +173,7 @@ def run_tests(module_name, std): except AttributeError: pass else: - if attr in ("PY_VERSION_HEX", "PYPY_VERSION_NUM"): + if attr in ('PY_VERSION_HEX', 'Py_LIMITED_API', 'PYPY_VERSION_NUM'): value = "0x%x" % value print("%s: %s" % (attr, value)) empty_line = True diff --git a/tests/test_pythoncapi_compat_cext.c b/tests/test_pythoncapi_compat_cext.c index 9e77f79..6727136 100644 --- a/tests/test_pythoncapi_compat_cext.c +++ b/tests/test_pythoncapi_compat_cext.c @@ -1762,6 +1762,22 @@ test_hash(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args)) Py_DECREF(abc); } + // Test PyObject_GenericHash() + { + PyObject *object_type = PyImport_ImportModuleAttrString("builtins", "object"); + if (object_type == NULL) { + return NULL; + } + PyObject *object = PyObject_CallNoArgs(object_type); + Py_DECREF(object_type); + if (object == NULL) { + return NULL; + } + + assert(PyObject_GenericHash(object) == Py_HashPointer(object)); + Py_DECREF(object); + } + Py_RETURN_NONE; }