Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Doc/c-api/tuple.rst
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ Tuple Objects
``*p`` is destroyed. On failure, returns ``-1`` and sets ``*p`` to ``NULL``, and
raises :exc:`MemoryError` or :exc:`SystemError`.

While tuple objects are usually immutable in Python, this special C API
allows mutating a tuple object in-place. The returned tuple object can still
be mutated using :c:func:`PyTuple_SetItem`; except if *newsize* is zero in
which case it returns the immutable empty tuple.


.. _struct-sequence-objects:

Expand Down
85 changes: 60 additions & 25 deletions Lib/test/test_capi/test_tuple.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import unittest
import gc
from sys import getrefcount
import sys
import unittest
from test.support import import_helper

_testcapi = import_helper.import_module('_testcapi')
Expand Down Expand Up @@ -131,9 +131,9 @@ def check_tuple_from_pair(self, from_pair):
class Temp:
pass
temp = Temp()
temp_rc = getrefcount(temp)
temp_rc = sys.getrefcount(temp)
self.assertEqual(from_pair(temp, temp), (temp, temp))
self.assertEqual(getrefcount(temp), temp_rc)
self.assertEqual(sys.getrefcount(temp), temp_rc)

self._not_tracked(from_pair(1, 2))
self._not_tracked(from_pair(None, None))
Expand Down Expand Up @@ -299,29 +299,64 @@ def test_tuple_set_item(self):

def test__tuple_resize(self):
# Test _PyTuple_Resize()
resize = _testcapi._tuple_resize
_resize = _testcapi._tuple_resize
checknull = _testcapi._check_tuple_item_is_NULL

a = ()
b = resize(a, 0, False)
self.assertEqual(len(a), 0)
self.assertEqual(len(b), 0)
b = resize(a, 2, False)
self.assertEqual(len(a), 0)
self.assertEqual(len(b), 2)
self.assertTrue(checknull(b, 0))
self.assertTrue(checknull(b, 1))

a = ([1], [2], [3])
b = resize(a, 3)
self.assertEqual(b, a)
b = resize(a, 2)
self.assertEqual(b, a[:2])
b = resize(a, 5)
self.assertEqual(len(b), 5)
self.assertEqual(b[:3], a)
self.assertTrue(checknull(b, 3))
self.assertTrue(checknull(b, 4))
def assert_is_fresh_copy(result, refcnt, is_new_obj):
self.assertEqual(refcnt, 1)
self.assertTrue(is_new_obj)
self.assertFalse(sys._is_immortal(result))

def resize(tup, new_size, new=True, compute_hash=False):
if isinstance(tup, tuple):
old_size = len(tup)
else:
old_size = 0
result, refcnt, is_new_obj = _resize(tup, new_size, new,
compute_hash=compute_hash)
if new_size == old_size:
# Return the same object unchanged
self.assertFalse(is_new_obj)
elif old_size == 0:
assert_is_fresh_copy(result, refcnt, is_new_obj)
elif new_size == 0:
# check that the result is the empty bytes string singleton
self.assertEqual(result, ())
self.assertTrue(sys._is_immortal(result))
self.assertTrue(is_new_obj)
elif (not new) or compute_hash:
# Make sure that the result is a fresh object.
assert_is_fresh_copy(result, refcnt, is_new_obj)
else:
# An in-place resize can return the same memory address, or
# not. 'is_new_obj' cannot be tested.
self.assertEqual(refcnt, 1)
self.assertFalse(sys._is_immortal(result))

return result

for compute_hash in True, False:
with self.subTest(compute_hash=compute_hash):
a = ()
b = resize(a, 0, False, compute_hash)
self.assertEqual(len(a), 0)
self.assertEqual(len(b), 0)
b = resize(a, 2, False, compute_hash)
self.assertEqual(len(a), 0)
self.assertEqual(len(b), 2)
self.assertTrue(checknull(b, 0))
self.assertTrue(checknull(b, 1))

a = tuple(object() for _ in range(3))
b = resize(a, 3, True, compute_hash)
self.assertEqual(b, a)
b = resize(a, 2, True, compute_hash)
self.assertEqual(b, a[:2])
b = resize(a, 5, True, compute_hash)
self.assertEqual(len(b), 5)
self.assertEqual(b[:3], a)
self.assertTrue(checknull(b, 3))
self.assertTrue(checknull(b, 4))

a = ()
self.assertRaises(MemoryError, resize, a, PY_SSIZE_T_MAX)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:c:func:`_PyTuple_Resize` now returns a copy if the hash value was already
computed. Patch by Victor Stinner.
25 changes: 21 additions & 4 deletions Modules/_testcapi/tuple.c
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,17 @@ tuple_set_item(PyObject *Py_UNUSED(module), PyObject *args)
}

static PyObject *
_tuple_resize(PyObject *Py_UNUSED(module), PyObject *args)
_tuple_resize(PyObject *Py_UNUSED(module), PyObject *args, PyObject *kwargs)
{
static char *kwlist[] = {"obj", "newsize", "new", "compute_hash", NULL};
PyObject *tup;
Py_ssize_t newsize;
int new = 1;
if (!PyArg_ParseTuple(args, "On|p", &tup, &newsize, &new)) {
int compute_hash = 0;

if (!PyArg_ParseTupleAndKeywords(args, kwargs,
"On|pi", kwlist,
&tup, &newsize, &new, &compute_hash)) {
return NULL;
}
if (new) {
Expand All @@ -84,12 +89,24 @@ _tuple_resize(PyObject *Py_UNUSED(module), PyObject *args)
NULLABLE(tup);
Py_XINCREF(tup);
}

if (compute_hash) {
if (PyObject_Hash(tup) == -1) {
Py_DECREF(tup);
return NULL;
}
}

PyObject *old_tup = tup;
int r = _PyTuple_Resize(&tup, newsize);
if (r == -1) {
assert(tup == NULL);
return NULL;
}
return tup;
assert(tup != NULL);

Py_ssize_t refcnt = Py_REFCNT(tup);
return Py_BuildValue("Nnp", tup, refcnt, tup != old_tup);
}

static PyObject *
Expand Down Expand Up @@ -135,7 +152,7 @@ static PyMethodDef test_methods[] = {
{"tuple_get_size", tuple_get_size, METH_O},
{"tuple_get_item", tuple_get_item, METH_VARARGS},
{"tuple_set_item", tuple_set_item, METH_VARARGS},
{"_tuple_resize", _tuple_resize, METH_VARARGS},
{"_tuple_resize", _PyCFunction_CAST(_tuple_resize), METH_VARARGS | METH_KEYWORDS},
{"_check_tuple_item_is_NULL", _check_tuple_item_is_NULL, METH_VARARGS},
{"tuple_fromarray", tuple_fromarray, METH_VARARGS},
{NULL},
Expand Down
128 changes: 83 additions & 45 deletions Objects/tupleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1015,49 +1015,12 @@ PyTypeObject PyTuple_Type = {
._tp_iteritem = tuple_iteritem,
};

/* The following function breaks the notion that tuples are immutable:
it changes the size of a tuple. We get away with this only if there
is only one module referencing the object. You can also think of it
as creating a new tuple object and destroying the old one, only more
efficiently. In any case, don't use this if the tuple may already be
known to some other part of the code. */

int
_PyTuple_Resize(PyObject **pv, Py_ssize_t newsize)
static int
tuple_resize_inplace(PyObject **pv, Py_ssize_t newsize)
{
PyTupleObject *v;
PyTupleObject *sv;
Py_ssize_t i;
Py_ssize_t oldsize;
PyTupleObject *v = (PyTupleObject *) *pv;

v = (PyTupleObject *) *pv;
if (v == NULL || !Py_IS_TYPE(v, &PyTuple_Type) || newsize < 0 ||
(Py_SIZE(v) != 0 && !_PyObject_IsUniquelyReferenced(*pv))) {
*pv = 0;
Py_XDECREF(v);
PyErr_BadInternalCall();
return -1;
}

oldsize = Py_SIZE(v);
if (oldsize == newsize) {
return 0;
}
if (newsize == 0) {
Py_DECREF(v);
*pv = tuple_get_empty();
return 0;
}
if (oldsize == 0) {
#ifdef Py_DEBUG
assert(v == &_Py_SINGLETON(tuple_empty));
#endif
/* The empty tuple is statically allocated so we never
resize it in-place. */
Py_DECREF(v);
*pv = PyTuple_New(newsize);
return *pv == NULL ? -1 : 0;
}
/* Check for overflow */
if ((size_t)newsize > ((size_t)PY_SSIZE_T_MAX - (sizeof(PyTupleObject) -
sizeof(PyObject *))) / sizeof(PyObject *)) {
Expand All @@ -1074,34 +1037,109 @@ _PyTuple_Resize(PyObject **pv, Py_ssize_t newsize)
_Py_ForgetReference((PyObject *) v);
#endif
/* DECREF items deleted by shrinkage */
for (i = newsize; i < oldsize; i++) {
Py_ssize_t oldsize = Py_SIZE(v);
for (Py_ssize_t i = newsize; i < oldsize; i++) {
Py_CLEAR(v->ob_item[i]);
}
_PyReftracerTrack((PyObject *)v, PyRefTracer_DESTROY);
sv = PyObject_GC_Resize(PyTupleObject, v, newsize);

PyTupleObject *sv = PyObject_GC_Resize(PyTupleObject, v, newsize);
if (sv == NULL) {
*pv = NULL;
#ifdef Py_REF_DEBUG
_Py_DecRefTotal(_PyThreadState_GET());
#endif
/* The failed reallocation leaves the original items owned by v. */
i = oldsize;
Py_ssize_t i = oldsize;
while (--i >= 0) {
Py_XDECREF(v->ob_item[i]);
}
PyObject_GC_Del(v);
return -1;
}

_Py_NewReferenceNoTotal((PyObject *) sv);
/* Zero out items added by growing */
if (newsize > oldsize)
if (newsize > oldsize) {
memset(&sv->ob_item[oldsize], 0,
sizeof(*sv->ob_item) * (newsize - oldsize));
sizeof(sv->ob_item[0]) * (newsize - oldsize));
}

*pv = (PyObject *) sv;
_PyObject_GC_TRACK(sv);
return 0;
}


/* The following function breaks the notion that tuples are immutable:
it changes the size of a tuple. We get away with this only if there
is only one module referencing the object. You can also think of it
as creating a new tuple object and destroying the old one, only more
efficiently. In any case, don't use this if the tuple may already be
known to some other part of the code. */

int
_PyTuple_Resize(PyObject **pv, Py_ssize_t newsize)
{
PyTupleObject *v = (PyTupleObject *) *pv;
if (v == NULL || !Py_IS_TYPE(v, &PyTuple_Type) || newsize < 0 ||
(Py_SIZE(v) != 0 && !_PyObject_IsUniquelyReferenced(*pv))) {
*pv = 0;
Py_XDECREF(v);
PyErr_BadInternalCall();
return -1;
}

Py_ssize_t oldsize = Py_SIZE(v);
if (oldsize == newsize) {
return 0;
}

if (newsize == 0) {
*pv = tuple_get_empty();
Py_DECREF(v);
return 0;
}

if (oldsize == 0) {
#ifdef Py_DEBUG
assert(v == &_Py_SINGLETON(tuple_empty));
#endif
/* The empty tuple is statically allocated so we never
resize it in-place. */
*pv = PyTuple_New(newsize);
Py_DECREF(v);
return *pv == NULL ? -1 : 0;
}

// gh-161226: Return a copy if the hash value was already computed
if (v->ob_hash != -1) {
PyTupleObject *new_tuple = tuple_alloc(newsize);
if (new_tuple == NULL) {
*pv = NULL;
Py_DECREF(v);
return -1;
}
// Copy items
Py_ssize_t copy = Py_MIN(oldsize, newsize);
for (Py_ssize_t i = 0; i < copy; i++) {
new_tuple->ob_item[i] = Py_NewRef(v->ob_item[i]);
}
if (newsize > oldsize) {
// Set new items to NULL
memset(&new_tuple->ob_item[oldsize], 0,
(newsize - oldsize) * sizeof(new_tuple->ob_item[0]));
}
_PyObject_GC_TRACK(new_tuple);

*pv = _PyObject_CAST(new_tuple);
Py_DECREF(v);
return 0;
}

return tuple_resize_inplace(pv, newsize);
}

/*********************** Tuple Iterator **************************/

#define _PyTupleIterObject_CAST(op) ((_PyTupleIterObject *)(op))
Expand Down
Loading