From 85e418661e5667edb5fd80368a68fe7b55b7067d Mon Sep 17 00:00:00 2001 From: Chidiebere Ekennia <54110155+Davien21@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:28:44 +0000 Subject: [PATCH 1/2] gh-160688: Add tests for PyObject_CopyData() and PyBuffer_FillContiguousStrides() --- Lib/test/test_capi/test_buffer.py | 94 ++++++++++++++++++++++++ Modules/Setup.stdlib.in | 2 +- Modules/_testlimitedcapi.c | 3 + Modules/_testlimitedcapi/buffer.c | 86 ++++++++++++++++++++++ Modules/_testlimitedcapi/parts.h | 1 + PCbuild/_testlimitedcapi.vcxproj | 1 + PCbuild/_testlimitedcapi.vcxproj.filters | 1 + 7 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 Lib/test/test_capi/test_buffer.py create mode 100644 Modules/_testlimitedcapi/buffer.c diff --git a/Lib/test/test_capi/test_buffer.py b/Lib/test/test_capi/test_buffer.py new file mode 100644 index 000000000000000..665a22ca0a3e514 --- /dev/null +++ b/Lib/test/test_capi/test_buffer.py @@ -0,0 +1,94 @@ +import unittest +from test.support import import_helper + +_testlimitedcapi = import_helper.import_module('_testlimitedcapi') +_testbuffer = import_helper.import_module('_testbuffer') +from _testbuffer import ndarray, ND_WRITABLE, ND_FORTRAN + + +class CAPITest(unittest.TestCase): + def test_object_copydata(self): + # Test PyObject_CopyData() + copydata = _testlimitedcapi.object_copydata + + dest = bytearray(b'xyz') + self.assertEqual(copydata(dest, b'abc'), 0) + self.assertEqual(dest, b'abc') + + dest = bytearray(5) + self.assertEqual(copydata(dest, memoryview(b'abc')), 0) + self.assertEqual(dest, b'abc\0\0') + + dest = bytearray() + self.assertEqual(copydata(dest, b''), 0) + self.assertEqual(dest, b'') + + # Non-contiguous source + dest = bytearray(4) + self.assertEqual(copydata(dest, memoryview(b'abcdefgh')[::2]), 0) + self.assertEqual(dest, b'aceg') + + # Non-contiguous destination + base = bytearray(b'........') + self.assertEqual(copydata(memoryview(base)[::2], b'wxyz'), 0) + self.assertEqual(base, b'w.x.y.z.') + + # Destination too small + dest = bytearray(b'xy') + self.assertRaises(BufferError, copydata, dest, b'abc') + self.assertEqual(dest, b'xy') + + # Read-only destination + self.assertRaises(BufferError, copydata, b'xyz', b'abc') + + self.assertRaises(TypeError, copydata, bytearray(3), 'abc') + self.assertRaises(TypeError, copydata, [], b'') + self.assertRaises(TypeError, copydata, 42, b'') + + # CRASHES copydata(NULL, b'abc') + # CRASHES copydata(bytearray(3), NULL) + + def test_object_copydata_multidim(self): + # Test PyObject_CopyData() with multi-dimensional buffers + copydata = _testlimitedcapi.object_copydata + items = list(range(12)) + c_src = ndarray(items, shape=[3, 4], format='B') + f_src = ndarray(items, shape=[3, 4], format='B', flags=ND_FORTRAN) + + for src in c_src, f_src: + for flags in ND_WRITABLE, ND_WRITABLE|ND_FORTRAN: + with self.subTest(src=src.flags, dest=flags): + dest = ndarray([0]*12, shape=[3, 4], format='B', + flags=flags) + self.assertEqual(copydata(dest, src), 0) + self.assertEqual(dest.tolist(), src.tolist()) + + # Multi-byte items + src = ndarray([1, -2, 3, -4, 5, -6], shape=[2, 3], format='i') + dest = ndarray([0]*6, shape=[2, 3], format='i', + flags=ND_WRITABLE|ND_FORTRAN) + self.assertEqual(copydata(dest, src), 0) + self.assertEqual(dest.tolist(), [[1, -2, 3], [-4, 5, -6]]) + + # Non-contiguous source + src = c_src[::2, ::2] + dest = ndarray([0]*4, shape=[2, 2], format='B', flags=ND_WRITABLE) + self.assertEqual(copydata(dest, src), 0) + self.assertEqual(dest.tolist(), [[0, 2], [8, 10]]) + + def test_buffer_fillcontiguousstrides(self): + # Test PyBuffer_FillContiguousStrides() + fill = _testlimitedcapi.buffer_fillcontiguousstrides + + self.assertEqual(fill((2, 3, 4), 8, 'C'), (96, 32, 8)) + self.assertEqual(fill((2, 3, 4), 8, 'F'), (8, 16, 48)) + self.assertEqual(fill((5,), 2, 'C'), (2,)) + self.assertEqual(fill((5,), 2, 'F'), (2,)) + self.assertEqual(fill((), 4, 'C'), ()) + self.assertEqual(fill((), 4, 'F'), ()) + self.assertEqual(fill((2, 0, 3), 4, 'C'), (0, 12, 4)) + self.assertEqual(fill((2, 0, 3), 4, 'F'), (4, 8, 0)) + + +if __name__ == "__main__": + unittest.main() diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index d8cd8de10dfe084..a410b0b2bff3b87 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -174,7 +174,7 @@ @MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c @MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c _testinternalcapi/complex.c _testinternalcapi/interpreter.c _testinternalcapi/tokenizer.c _testinternalcapi/tuple.c _testinternalcapi/typecache.c @MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/modsupport.c _testcapi/monitoring.c _testcapi/config.c _testcapi/import.c _testcapi/frame.c _testcapi/type.c _testcapi/function.c _testcapi/module.c _testcapi/weakref.c _testcapi/marshal.c _testcapi/fpcast.c -@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/capsule.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slice.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c _testlimitedcapi/type.c _testlimitedcapi/hash.c _testlimitedcapi/build.c +@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/buffer.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/capsule.c _testlimitedcapi/codec.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/import.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/slice.c _testlimitedcapi/slots.c _testlimitedcapi/sys.c _testlimitedcapi/threadstate.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c _testlimitedcapi/weakref.c _testlimitedcapi/run.c _testlimitedcapi/type.c _testlimitedcapi/hash.c _testlimitedcapi/build.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c @MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c diff --git a/Modules/_testlimitedcapi.c b/Modules/_testlimitedcapi.c index 1aee3bc9bf3c1df..43ef2e3d06ae1b8 100644 --- a/Modules/_testlimitedcapi.c +++ b/Modules/_testlimitedcapi.c @@ -13,6 +13,9 @@ module_exec(PyObject *mod) if (_PyTestLimitedCAPI_Init_Abstract(mod) < 0) { return -1; } + if (_PyTestLimitedCAPI_Init_Buffer(mod) < 0) { + return -1; + } if (_PyTestLimitedCAPI_Init_ByteArray(mod) < 0) { return -1; } diff --git a/Modules/_testlimitedcapi/buffer.c b/Modules/_testlimitedcapi/buffer.c new file mode 100644 index 000000000000000..3d19feea1974201 --- /dev/null +++ b/Modules/_testlimitedcapi/buffer.c @@ -0,0 +1,86 @@ +#include "pyconfig.h" // Py_GIL_DISABLED +#ifdef Py_GIL_DISABLED +# define Py_TARGET_ABI3T 0x030f0000 +#else + // Need limited C API 3.11 for the buffer protocol +# define Py_LIMITED_API 0x030b0000 +#endif + +#include "parts.h" +#include "util.h" + + +static PyObject * +object_copydata(PyObject *Py_UNUSED(module), PyObject *args) +{ + PyObject *dest, *src; + if (!PyArg_ParseTuple(args, "OO", &dest, &src)) { + return NULL; + } + RETURN_INT(PyObject_CopyData(dest, src)); +} + +static PyObject * +buffer_fillcontiguousstrides(PyObject *Py_UNUSED(module), PyObject *args) +{ + PyObject *shape_obj; + int itemsize, order; + if (!PyArg_ParseTuple(args, "O!iC", &PyTuple_Type, &shape_obj, + &itemsize, &order)) + { + return NULL; + } + Py_ssize_t size = PyTuple_Size(shape_obj); + if (size < 0) { + return NULL; + } + int nd = (int)size; + Py_ssize_t *shape = PyMem_Malloc(size * sizeof(Py_ssize_t)); + Py_ssize_t *strides = PyMem_Malloc(size * sizeof(Py_ssize_t)); + if (shape == NULL || strides == NULL) { + PyErr_NoMemory(); + goto error; + } + for (int k = 0; k < nd; k++) { + shape[k] = PyLong_AsSsize_t(PyTuple_GetItem(shape_obj, k)); + if (shape[k] == -1 && PyErr_Occurred()) { + goto error; + } + } + + PyBuffer_FillContiguousStrides(nd, shape, strides, itemsize, (char)order); + + PyObject *result = PyTuple_New(size); + if (result == NULL) { + goto error; + } + for (int k = 0; k < nd; k++) { + PyObject *item = PyLong_FromSsize_t(strides[k]); + if (item == NULL) { + Py_DECREF(result); + goto error; + } + PyTuple_SetItem(result, k, item); + } + PyMem_Free(shape); + PyMem_Free(strides); + return result; + +error: + PyMem_Free(shape); + PyMem_Free(strides); + return NULL; +} + + +static PyMethodDef test_methods[] = { + {"object_copydata", object_copydata, METH_VARARGS}, + {"buffer_fillcontiguousstrides", buffer_fillcontiguousstrides, METH_VARARGS}, + {NULL}, +}; + +int +_PyTestLimitedCAPI_Init_Buffer(PyObject *m) +{ + return PyModule_AddFunctions(m, test_methods); +} diff --git a/Modules/_testlimitedcapi/parts.h b/Modules/_testlimitedcapi/parts.h index 1e9d140899cecfd..9d34ce087f749d7 100644 --- a/Modules/_testlimitedcapi/parts.h +++ b/Modules/_testlimitedcapi/parts.h @@ -25,6 +25,7 @@ #endif int _PyTestLimitedCAPI_Init_Abstract(PyObject *module); +int _PyTestLimitedCAPI_Init_Buffer(PyObject *module); int _PyTestLimitedCAPI_Init_ByteArray(PyObject *module); int _PyTestLimitedCAPI_Init_Bytes(PyObject *module); int _PyTestLimitedCAPI_Init_Capsule(PyObject *module); diff --git a/PCbuild/_testlimitedcapi.vcxproj b/PCbuild/_testlimitedcapi.vcxproj index 1a40cd0d2271f5f..e85d69bb2d780b9 100644 --- a/PCbuild/_testlimitedcapi.vcxproj +++ b/PCbuild/_testlimitedcapi.vcxproj @@ -95,6 +95,7 @@ + diff --git a/PCbuild/_testlimitedcapi.vcxproj.filters b/PCbuild/_testlimitedcapi.vcxproj.filters index 1b13da0ebca7ba2..1bd170ad2381705 100644 --- a/PCbuild/_testlimitedcapi.vcxproj.filters +++ b/PCbuild/_testlimitedcapi.vcxproj.filters @@ -10,6 +10,7 @@ + From 557f4320e3112cf5d3a8c5d05141ec69b08b65d0 Mon Sep 17 00:00:00 2001 From: Chidiebere Ekennia <54110155+Davien21@users.noreply.github.com> Date: Sun, 1 Nov 2026 14:44:23 +0000 Subject: [PATCH 2/2] Add NEWS entry --- .../next/Tests/2026-11-01-11-50-00.gh-issue-160688.Xp4kQw.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Tests/2026-11-01-11-50-00.gh-issue-160688.Xp4kQw.rst diff --git a/Misc/NEWS.d/next/Tests/2026-11-01-11-50-00.gh-issue-160688.Xp4kQw.rst b/Misc/NEWS.d/next/Tests/2026-11-01-11-50-00.gh-issue-160688.Xp4kQw.rst new file mode 100644 index 000000000000000..e535e38cbcf0a18 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-11-01-11-50-00.gh-issue-160688.Xp4kQw.rst @@ -0,0 +1,2 @@ +Add tests for the C API functions :c:func:`PyObject_CopyData` and +:c:func:`PyBuffer_FillContiguousStrides`.