Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
24 changes: 24 additions & 0 deletions Doc/c-api/object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,30 @@ Object Protocol

.. versionadded:: 3.14

.. c:function:: int PyUnstable_Object_IsUniqueTemporary(PyObject *obj)

Check if *obj* is a unique temporary object on the top most frame of the
interpreter stack. Returns ``1`` if *obj* is a unique temporary object,
and ``0`` otherwise. This check is conservative, and may return ``0``
in some cases even if *obj* is a unique temporary object.

If an object is a unique temporary, it is guaranteed that the reference
count is ``1`` and it may be safe to modify the object in-place becuase
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "may be safe" doesn't quite convey what we want here. We don't want people to be in doubt about whether it's safe from the interpreter's point of view. Maybe something along the lines of "it is guaranteed that the current code owns the only reference to the object"?

It's probably also worth mentioning why a refcount of 1 isn't enough to guarantee it's a unique temporary, something about the interpreter borrowing references internally, similar to the whatsnew entry.

it is not visible to any other code.

In the example below, ``my_func`` is called with a unique temporary object
as its argument::

my_func([1, 2, 3])

In the example below, ``my_func`` is **not** called with a unique temporary
object as its argument::

my_list = [1, 2, 3]
my_func(my_list)

.. versionadded:: 3.14

.. c:function:: int PyUnstable_IsImmortal(PyObject *obj)

This function returns non-zero if *obj* is :term:`immortal`, and zero
Expand Down
5 changes: 5 additions & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,11 @@ PyAPI_FUNC(PyRefTracer) PyRefTracer_GetTracer(void**);
*/
PyAPI_FUNC(int) PyUnstable_Object_EnableDeferredRefcount(PyObject *);

/* Determine if the object exists as a unique temporary variable on the
* top most frame of the interpreter.
*/
PyAPI_FUNC(int) PyUnstable_Object_IsUniqueTemporary(PyObject *);

/* Check whether the object is immortal. This cannot fail. */
PyAPI_FUNC(int) PyUnstable_IsImmortal(PyObject *);

Expand Down
12 changes: 12 additions & 0 deletions Lib/test/test_capi/test_object.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import enum
import sys
import textwrap
import unittest
from test import support
Expand Down Expand Up @@ -223,5 +224,16 @@ def __del__(self):
obj = MyObj()
_testinternalcapi.incref_decref_delayed(obj)

def test_is_unique_temporary(self):
self.assertTrue(_testcapi.pyobject_is_unique_temporary(object()))
obj = object()
self.assertFalse(_testcapi.pyobject_is_unique_temporary(obj))

def func(x):
self.assertEqual(sys.getrefcount(x), 1)
self.assertFalse(_testcapi.pyobject_is_unique_temporary(x))

func(object())

if __name__ == "__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Added the :c:func:`PyUnstable_Object_IsUniqueTemporary` function for
determining if an object exists as a unqiue temporary variable on the
interpreter's stack. This is a replacement for some cases where checking
that :c:func:`Py_REFCNT` is one is no longer sufficient to determine if it's
safe to modify a Python object in-place with no visible side effects.
8 changes: 8 additions & 0 deletions Modules/_testcapi/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,13 @@ pyobject_enable_deferred_refcount(PyObject *self, PyObject *obj)
return PyLong_FromLong(result);
}

static PyObject *
pyobject_is_unique_temporary(PyObject *self, PyObject *obj)
{
int result = PyUnstable_Object_IsUniqueTemporary(obj);
return PyLong_FromLong(result);
}

static int MyObject_dealloc_called = 0;

static void
Expand Down Expand Up @@ -478,6 +485,7 @@ static PyMethodDef test_methods[] = {
{"pyobject_print_os_error", pyobject_print_os_error, METH_VARARGS},
{"pyobject_clear_weakrefs_no_callbacks", pyobject_clear_weakrefs_no_callbacks, METH_O},
{"pyobject_enable_deferred_refcount", pyobject_enable_deferred_refcount, METH_O},
{"pyobject_is_unique_temporary", pyobject_is_unique_temporary, METH_O},
{"test_py_try_inc_ref", test_py_try_inc_ref, METH_NOARGS},
{"test_xincref_doesnt_leak",test_xincref_doesnt_leak, METH_NOARGS},
{"test_incref_doesnt_leak", test_incref_doesnt_leak, METH_NOARGS},
Expand Down
30 changes: 30 additions & 0 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include "pycore_hamt.h" // _PyHamtItems_Type
#include "pycore_initconfig.h" // _PyStatus_OK()
#include "pycore_instruction_sequence.h" // _PyInstructionSequence_Type
#include "pycore_interpframe.h" // _PyFrame_Stackbase()
#include "pycore_list.h" // _PyList_DebugMallocStats()
#include "pycore_long.h" // _PyLong_GetZero()
#include "pycore_memoryobject.h" // _PyManagedBuffer_Type
Expand Down Expand Up @@ -2616,6 +2617,35 @@ PyUnstable_Object_EnableDeferredRefcount(PyObject *op)
#endif
}

int
PyUnstable_Object_IsUniqueTemporary(PyObject *op)
{
if (!_PyObject_IsUniquelyReferenced(op)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this just demonstrates that _PyObject_IsUniquelyReferenced is badly named, otherwise we wouldn't need a new function.

We need to check that the following are true:

  • The object has an explicit (not borrowed) reference count of 1
  • It is mortal
  • It does not support deferred reclamation.

I think _PyObject_IsUniquelyReferenced does that, but it isn't clear from the name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss the name for _PyObject_IsUniquelyReferenced in #133144

return 0;
}

_PyInterpreterFrame *frame = _PyEval_GetFrame();
if (frame == NULL) {
return 0;
}

_PyStackRef *base = _PyFrame_Stackbase(frame);
_PyStackRef *stackpointer = frame->stackpointer;
int found = 0;
while (stackpointer > base) {
stackpointer--;
if (op == PyStackRef_AsPyObjectBorrow(*stackpointer)) {
if (!PyStackRef_IsHeapSafe(*stackpointer)) {
return 0;
}
found++;
}
}

// Check that we found exactly one reference to `op`
return found == 1;
}

int
PyUnstable_TryIncRef(PyObject *op)
{
Expand Down
Loading