From f2d3ab87ac04b11ec63b8d33c7704b0497894ae9 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 27 Feb 2026 23:01:39 +0500 Subject: [PATCH 1/6] Add initial version of _PyTuple_FromPair --- Include/internal/pycore_tuple.h | 3 ++ Objects/tupleobject.c | 53 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/Include/internal/pycore_tuple.h b/Include/internal/pycore_tuple.h index 46db02593ad106..23ef7230419e3b 100644 --- a/Include/internal/pycore_tuple.h +++ b/Include/internal/pycore_tuple.h @@ -26,6 +26,9 @@ extern PyStatus _PyTuple_InitGlobalObjects(PyInterpreterState *); PyAPI_FUNC(PyObject *)_PyTuple_FromStackRefStealOnSuccess(const union _PyStackRef *, Py_ssize_t); PyAPI_FUNC(PyObject *)_PyTuple_FromArraySteal(PyObject *const *, Py_ssize_t); +PyAPI_FUNC(PyObject *)_PyTuple_FromPair(PyObject *, PyObject *); +PyAPI_FUNC(PyObject *)_PyTuple_FromPairSteal(PyObject *, PyObject *); + typedef struct { PyObject_HEAD Py_ssize_t it_index; diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 169ac69701da11..672c092ff929a6 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -202,6 +202,59 @@ PyTuple_Pack(Py_ssize_t n, ...) return (PyObject *)result; } +static PyTupleObject * +tuple_alloc_2() +{ + Py_ssize_t size = 2; + Py_ssize_t index = size - 1; + assert(index < PyTuple_MAXSAVESIZE); + PyTupleObject *result = _Py_FREELIST_POP(PyTupleObject, tuples[index]); + if (result == NULL) { + result = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size); + } + if (result != NULL) { + _PyTuple_RESET_HASH_CACHE(result); + } + return result; +} + +PyObject * +_PyTuple_FromPair(PyObject *one, PyObject *two) +{ + assert (one != NULL); + assert (two != NULL); + + PyTupleObject *op = tuple_alloc_2(); + if (op == NULL) { + return NULL; + } + op->ob_item[0] = Py_NewRef(one); + op->ob_item[1] = Py_NewRef(two); + if (maybe_tracked(one) || maybe_tracked(two)) { + _PyObject_GC_TRACK(op); + } + return (PyObject *)op; +} + +PyObject * +_PyTuple_FromPairSteal(PyObject *one, PyObject *two) +{ + assert (one != NULL); + assert (two != NULL); + + PyTupleObject *op = tuple_alloc_2(); + if (op == NULL) { + Py_DECREF(one); + Py_DECREF(two); + return NULL; + } + op->ob_item[0] = one; + op->ob_item[1] = two; + if (maybe_tracked(one) || maybe_tracked(two)) { + _PyObject_GC_TRACK(op); + } + return (PyObject *)op; +} /* Methods */ From 84597690a7b678ae88a1d038a3b4d7e3f8bb2600 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Fri, 27 Feb 2026 23:03:21 +0500 Subject: [PATCH 2/6] Add initial tests for _PyTuple_FromPair --- Lib/test/test_capi/test_tuple.py | 57 +++++++++++++++++++++++ Modules/Setup.stdlib.in | 2 +- Modules/_testinternalcapi.c | 3 ++ Modules/_testinternalcapi/parts.h | 1 + Modules/_testinternalcapi/tuple.c | 43 +++++++++++++++++ PCbuild/_testinternalcapi.vcxproj | 1 + PCbuild/_testinternalcapi.vcxproj.filters | 3 ++ 7 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 Modules/_testinternalcapi/tuple.c diff --git a/Lib/test/test_capi/test_tuple.py b/Lib/test/test_capi/test_tuple.py index d6669d7802c5b8..4fe72c1e2e6ca4 100644 --- a/Lib/test/test_capi/test_tuple.py +++ b/Lib/test/test_capi/test_tuple.py @@ -1,9 +1,11 @@ import unittest import gc +from sys import getrefcount from test.support import import_helper _testcapi = import_helper.import_module('_testcapi') _testlimitedcapi = import_helper.import_module('_testlimitedcapi') +_testinternalcapi = import_helper.import_module('_testinternalcapi') NULL = None PY_SSIZE_T_MIN = _testcapi.PY_SSIZE_T_MIN @@ -118,6 +120,61 @@ def test_tuple_pack(self): # CRASHES pack(1, NULL) # CRASHES pack(2, [1]) + def test_tuple_from_pair(self): + # Test _PyTuple_FromPair() + tuple_from_pair = _testinternalcapi._tuple_from_pair + + self.assertEqual(tuple_from_pair(1, 2), (1, 2)) + self.assertEqual(tuple_from_pair(None, None), (None, None)) + self.assertEqual(tuple_from_pair(True, False), (True, False)) + + # user class supports gc + class Temp: + pass + temp = Temp() + temp_rc = getrefcount(temp) + self.assertEqual(tuple_from_pair(temp, temp), (temp, temp)) + self.assertEqual(getrefcount(temp), temp_rc) + + self.assertRaises(TypeError, tuple_from_pair, 1, 2, 3) + self.assertRaises(TypeError, tuple_from_pair, 1) + self.assertRaises(TypeError, tuple_from_pair) + + self.assertFalse(gc.is_tracked(tuple_from_pair(1, 2))) + self.assertFalse(gc.is_tracked(tuple_from_pair(None, None))) + self.assertFalse(gc.is_tracked(tuple_from_pair(True, False))) + self.assertTrue(gc.is_tracked(tuple_from_pair(temp, (1, 2)))) + self.assertTrue(gc.is_tracked(tuple_from_pair(temp, 1))) + self.assertTrue(gc.is_tracked(tuple_from_pair([], {}))) + + def test_tuple_from_pair_steal(self): + # Test _PyTuple_FromPairSteal() + tuple_from_pair = _testinternalcapi._tuple_from_pair_steal + + self.assertEqual(tuple_from_pair(1, 2), (1, 2)) + self.assertEqual(tuple_from_pair(None, None), (None, None)) + self.assertEqual(tuple_from_pair(True, False), (True, False)) + + # user class supports gc + class Temp: + pass + temp = Temp() + temp_rc = getrefcount(temp) + self.assertEqual(tuple_from_pair(temp, temp), (temp, temp)) + self.assertEqual(getrefcount(temp), temp_rc) + + self.assertRaises(TypeError, tuple_from_pair, 1, 2, 3) + self.assertRaises(TypeError, tuple_from_pair, 1) + self.assertRaises(TypeError, tuple_from_pair) + + self.assertFalse(gc.is_tracked(tuple_from_pair(1, 2))) + self.assertFalse(gc.is_tracked(tuple_from_pair(None, None))) + self.assertFalse(gc.is_tracked(tuple_from_pair(True, False))) + self.assertTrue(gc.is_tracked(tuple_from_pair(temp, (1, 2)))) + self.assertTrue(gc.is_tracked(tuple_from_pair(temp, 1))) + self.assertTrue(gc.is_tracked(tuple_from_pair([], {}))) + + def test_tuple_size(self): # Test PyTuple_Size() size = _testlimitedcapi.tuple_size diff --git a/Modules/Setup.stdlib.in b/Modules/Setup.stdlib.in index 1dd0512832adf7..97d8fafa0c975c 100644 --- a/Modules/Setup.stdlib.in +++ b/Modules/Setup.stdlib.in @@ -174,7 +174,7 @@ @MODULE_XXSUBTYPE_TRUE@xxsubtype xxsubtype.c @MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c @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 +@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/tuple.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 @MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.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/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c _testlimitedcapi/version.c _testlimitedcapi/file.c @MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c index 22cfa3f58a9d83..b17faa05f69ec3 100644 --- a/Modules/_testinternalcapi.c +++ b/Modules/_testinternalcapi.c @@ -2981,6 +2981,9 @@ module_exec(PyObject *module) if (_PyTestInternalCapi_Init_CriticalSection(module) < 0) { return 1; } + if (_PyTestInternalCapi_Init_Tuple(module) < 0) { + return 1; + } Py_ssize_t sizeof_gc_head = 0; #ifndef Py_GIL_DISABLED diff --git a/Modules/_testinternalcapi/parts.h b/Modules/_testinternalcapi/parts.h index 03557d5bf5957f..81f536c3babb18 100644 --- a/Modules/_testinternalcapi/parts.h +++ b/Modules/_testinternalcapi/parts.h @@ -15,5 +15,6 @@ int _PyTestInternalCapi_Init_PyTime(PyObject *module); int _PyTestInternalCapi_Init_Set(PyObject *module); int _PyTestInternalCapi_Init_Complex(PyObject *module); int _PyTestInternalCapi_Init_CriticalSection(PyObject *module); +int _PyTestInternalCapi_Init_Tuple(PyObject *module); #endif // Py_TESTINTERNALCAPI_PARTS_H diff --git a/Modules/_testinternalcapi/tuple.c b/Modules/_testinternalcapi/tuple.c new file mode 100644 index 00000000000000..ee95dee2b21bcc --- /dev/null +++ b/Modules/_testinternalcapi/tuple.c @@ -0,0 +1,43 @@ +#include "parts.h" + +#include "pycore_tuple.h" + + +static PyObject * +_tuple_from_pair(PyObject *Py_UNUSED(module), PyObject *args) +{ + PyObject *one, *two; + if (!PyArg_ParseTuple(args, "OO", &one, &two)) { + return NULL; + } + + return _PyTuple_FromPair(one, two); +} + +static PyObject * +_tuple_from_pair_steal(PyObject *Py_UNUSED(module), PyObject *args) +{ + PyObject *one, *two; + if (!PyArg_ParseTuple(args, "OO", &one, &two)) { + return NULL; + } + + return _PyTuple_FromPairSteal(Py_NewRef(one), Py_NewRef(two)); +} + + +static PyMethodDef test_methods[] = { + {"_tuple_from_pair", _tuple_from_pair, METH_VARARGS}, + {"_tuple_from_pair_steal", _tuple_from_pair_steal, METH_VARARGS}, + {NULL}, +}; + +int +_PyTestInternalCapi_Init_Tuple(PyObject *m) +{ + if (PyModule_AddFunctions(m, test_methods) < 0) { + return -1; + } + + return 0; +} diff --git a/PCbuild/_testinternalcapi.vcxproj b/PCbuild/_testinternalcapi.vcxproj index 3818e6d3f7bbd2..f3e423fa04668e 100644 --- a/PCbuild/_testinternalcapi.vcxproj +++ b/PCbuild/_testinternalcapi.vcxproj @@ -100,6 +100,7 @@ + diff --git a/PCbuild/_testinternalcapi.vcxproj.filters b/PCbuild/_testinternalcapi.vcxproj.filters index 012d709bd1ce5d..7ab242c2c230b6 100644 --- a/PCbuild/_testinternalcapi.vcxproj.filters +++ b/PCbuild/_testinternalcapi.vcxproj.filters @@ -27,6 +27,9 @@ Source Files + + Source Files + From b51ffc61e9d495d03ab62c5aaf982d09668810cc Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Sat, 28 Feb 2026 00:23:19 +0500 Subject: [PATCH 3/6] Fix warnings --- Objects/tupleobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index 672c092ff929a6..e2f8cec19dd4a2 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -203,7 +203,7 @@ PyTuple_Pack(Py_ssize_t n, ...) } static PyTupleObject * -tuple_alloc_2() +tuple_alloc_2(void) { Py_ssize_t size = 2; Py_ssize_t index = size - 1; From b6c872786409e691b2a35084ab27d6d81576362c Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Sat, 28 Feb 2026 11:15:29 +0500 Subject: [PATCH 4/6] Simplify tests --- Lib/test/test_capi/test_tuple.py | 81 +++++++++++--------------------- 1 file changed, 28 insertions(+), 53 deletions(-) diff --git a/Lib/test/test_capi/test_tuple.py b/Lib/test/test_capi/test_tuple.py index 4fe72c1e2e6ca4..a14b158bbf8348 100644 --- a/Lib/test/test_capi/test_tuple.py +++ b/Lib/test/test_capi/test_tuple.py @@ -121,59 +121,34 @@ def test_tuple_pack(self): # CRASHES pack(2, [1]) def test_tuple_from_pair(self): - # Test _PyTuple_FromPair() - tuple_from_pair = _testinternalcapi._tuple_from_pair - - self.assertEqual(tuple_from_pair(1, 2), (1, 2)) - self.assertEqual(tuple_from_pair(None, None), (None, None)) - self.assertEqual(tuple_from_pair(True, False), (True, False)) - - # user class supports gc - class Temp: - pass - temp = Temp() - temp_rc = getrefcount(temp) - self.assertEqual(tuple_from_pair(temp, temp), (temp, temp)) - self.assertEqual(getrefcount(temp), temp_rc) - - self.assertRaises(TypeError, tuple_from_pair, 1, 2, 3) - self.assertRaises(TypeError, tuple_from_pair, 1) - self.assertRaises(TypeError, tuple_from_pair) - - self.assertFalse(gc.is_tracked(tuple_from_pair(1, 2))) - self.assertFalse(gc.is_tracked(tuple_from_pair(None, None))) - self.assertFalse(gc.is_tracked(tuple_from_pair(True, False))) - self.assertTrue(gc.is_tracked(tuple_from_pair(temp, (1, 2)))) - self.assertTrue(gc.is_tracked(tuple_from_pair(temp, 1))) - self.assertTrue(gc.is_tracked(tuple_from_pair([], {}))) - - def test_tuple_from_pair_steal(self): - # Test _PyTuple_FromPairSteal() - tuple_from_pair = _testinternalcapi._tuple_from_pair_steal - - self.assertEqual(tuple_from_pair(1, 2), (1, 2)) - self.assertEqual(tuple_from_pair(None, None), (None, None)) - self.assertEqual(tuple_from_pair(True, False), (True, False)) - - # user class supports gc - class Temp: - pass - temp = Temp() - temp_rc = getrefcount(temp) - self.assertEqual(tuple_from_pair(temp, temp), (temp, temp)) - self.assertEqual(getrefcount(temp), temp_rc) - - self.assertRaises(TypeError, tuple_from_pair, 1, 2, 3) - self.assertRaises(TypeError, tuple_from_pair, 1) - self.assertRaises(TypeError, tuple_from_pair) - - self.assertFalse(gc.is_tracked(tuple_from_pair(1, 2))) - self.assertFalse(gc.is_tracked(tuple_from_pair(None, None))) - self.assertFalse(gc.is_tracked(tuple_from_pair(True, False))) - self.assertTrue(gc.is_tracked(tuple_from_pair(temp, (1, 2)))) - self.assertTrue(gc.is_tracked(tuple_from_pair(temp, 1))) - self.assertTrue(gc.is_tracked(tuple_from_pair([], {}))) - + # Test _PyTuple_FromPair, _PyTuple_FromPairSteal + ctors = (("_PyTuple_FromPair", _testinternalcapi._tuple_from_pair), + ("_PyTuple_FromPairSteal", _testinternalcapi._tuple_from_pair_steal)) + + for name, ctor in ctors: + with self.subTest(name): + self.assertEqual(ctor(1, 2), (1, 2)) + self.assertEqual(ctor(None, None), (None, None)) + self.assertEqual(ctor(True, False), (True, False)) + + # user class supports gc + class Temp: + pass + temp = Temp() + temp_rc = getrefcount(temp) + self.assertEqual(ctor(temp, temp), (temp, temp)) + self.assertEqual(getrefcount(temp), temp_rc) + + self.assertRaises(TypeError, ctor, 1, 2, 3) + self.assertRaises(TypeError, ctor, 1) + self.assertRaises(TypeError, ctor) + + self.assertFalse(gc.is_tracked(ctor(1, 2))) + self.assertFalse(gc.is_tracked(ctor(None, None))) + self.assertFalse(gc.is_tracked(ctor(True, False))) + self.assertTrue(gc.is_tracked(ctor(temp, (1, 2)))) + self.assertTrue(gc.is_tracked(ctor(temp, 1))) + self.assertTrue(gc.is_tracked(ctor([], {}))) def test_tuple_size(self): # Test PyTuple_Size() From 940840046c95236d12b87eca535afc2d184a7705 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Sat, 28 Feb 2026 11:15:57 +0500 Subject: [PATCH 5/6] Remove unnecessary assert --- Objects/tupleobject.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Objects/tupleobject.c b/Objects/tupleobject.c index e2f8cec19dd4a2..d91328786f41bf 100644 --- a/Objects/tupleobject.c +++ b/Objects/tupleobject.c @@ -207,7 +207,6 @@ tuple_alloc_2(void) { Py_ssize_t size = 2; Py_ssize_t index = size - 1; - assert(index < PyTuple_MAXSAVESIZE); PyTupleObject *result = _Py_FREELIST_POP(PyTupleObject, tuples[index]); if (result == NULL) { result = PyObject_GC_NewVar(PyTupleObject, &PyTuple_Type, size); From 586261332d03ad6473968022d2d46fcdd8f28785 Mon Sep 17 00:00:00 2001 From: Sergey Miryanov Date: Sun, 1 Mar 2026 11:54:25 +0500 Subject: [PATCH 6/6] Update Lib/test/test_capi/test_tuple.py Co-authored-by: Pieter Eendebak --- Lib/test/test_capi/test_tuple.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_capi/test_tuple.py b/Lib/test/test_capi/test_tuple.py index a14b158bbf8348..5b951d19997d02 100644 --- a/Lib/test/test_capi/test_tuple.py +++ b/Lib/test/test_capi/test_tuple.py @@ -127,6 +127,7 @@ def test_tuple_from_pair(self): for name, ctor in ctors: with self.subTest(name): + self.assertEqual(type(ctor(1, 2)), tuple) self.assertEqual(ctor(1, 2), (1, 2)) self.assertEqual(ctor(None, None), (None, None)) self.assertEqual(ctor(True, False), (True, False))