Skip to content
Merged
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
40 changes: 30 additions & 10 deletions Doc/c-api/tuple.rst
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,36 @@ Tuple Objects
.. c:function:: int _PyTuple_Resize(PyObject **p, Py_ssize_t newsize)
Can be used to resize a tuple. *newsize* will be the new length of the tuple.
Because tuples are *supposed* to be immutable, this should only be used if there
is only one reference to the object. Do *not* use this if the tuple may already
be known to some other part of the code. The tuple will always grow or shrink
at the end. Think of this as destroying the old tuple and creating a new one,
only more efficiently. Returns ``0`` on success. Client code should never
assume that the resulting value of ``*p`` will be the same as before calling
this function. If the object referenced by ``*p`` is replaced, the original
``*p`` is destroyed. On failure, returns ``-1`` and sets ``*p`` to ``NULL``, and
raises :exc:`MemoryError` or :exc:`SystemError`.
Resize a tuple.
*newsize* will be the new length of the tuple.
Because tuples are *supposed* to be immutable, this should only be used if
there is only one reference to the object.
Do *not* use this if the tuple may already be known to some other part of
the code.
The tuple hash value must not be computed yet.
The tuple will always grow or shrink at the end.
Think of this as destroying the old tuple and creating a new one, only more
efficiently.
Returns ``0`` on success.
Client code should never assume that the resulting value of ``*p`` will be
the same as before calling this function.
If the object referenced by ``*p`` is replaced, the original ``*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.
.. versionchanged:: next
The function now fails if the tuple hash value was already computed.
.. _struct-sequence-objects:
Expand Down
79 changes: 60 additions & 19 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,30 +299,71 @@ 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)
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:
# 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

# Resizing the empty tuple singleton always create a new tuple
for compute_hash in (False, True):
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)
self.assertEqual(b, a)
b = resize(a, 2)
b = resize(a, 2, True)
self.assertEqual(b, a[:2])
b = resize(a, 5)
b = resize(a, 5, True)
self.assertEqual(len(b), 5)
self.assertEqual(b[:3], a)
self.assertTrue(checknull(b, 3))
self.assertTrue(checknull(b, 4))

# gh-161226: Disallow resizing a tuple if the hash value is cached
a = tuple(object() for _ in range(3))
with self.assertRaises(SystemError):
resize(a, 3, True, compute_hash=True)

a = ()
self.assertRaises(MemoryError, resize, a, PY_SSIZE_T_MAX)
self.assertRaises(SystemError, resize, a, -1)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:c:func:`_PyTuple_Resize` now fails with :exc:`SystemError` 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
7 changes: 5 additions & 2 deletions Objects/tupleobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1031,8 +1031,11 @@ _PyTuple_Resize(PyObject **pv, Py_ssize_t newsize)
Py_ssize_t oldsize;

v = (PyTupleObject *) *pv;
if (v == NULL || !Py_IS_TYPE(v, &PyTuple_Type) || newsize < 0 ||
(Py_SIZE(v) != 0 && !_PyObject_IsUniquelyReferenced(*pv))) {
if (v == NULL || !Py_IS_TYPE(v, &PyTuple_Type) || newsize < 0
|| (Py_SIZE(v) != 0 && !_PyObject_IsUniquelyReferenced(*pv))
// gh-161226: Disallow resizing a tuple if the hash value is cached
|| (Py_SIZE(v) != 0 && v->ob_hash != -1))
{
*pv = 0;
Py_XDECREF(v);
PyErr_BadInternalCall();
Expand Down
Loading