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/Lib/test/test_capi/test_tuple.py b/Lib/test/test_capi/test_tuple.py
index d6669d7802c5b8..a14b158bbf8348 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,36 @@ def test_tuple_pack(self):
# CRASHES pack(1, NULL)
# CRASHES pack(2, [1])
+ def test_tuple_from_pair(self):
+ # 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()
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/Objects/tupleobject.c b/Objects/tupleobject.c
index 169ac69701da11..d91328786f41bf 100644
--- a/Objects/tupleobject.c
+++ b/Objects/tupleobject.c
@@ -202,6 +202,58 @@ PyTuple_Pack(Py_ssize_t n, ...)
return (PyObject *)result;
}
+static PyTupleObject *
+tuple_alloc_2(void)
+{
+ Py_ssize_t size = 2;
+ Py_ssize_t index = size - 1;
+ 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 */
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
+