Skip to content

gh-133140: Add PyUnstable_Object_IsUniquelyReferenced for free-threading #133144

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
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
18 changes: 18 additions & 0 deletions Doc/c-api/object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -705,3 +705,21 @@ Object Protocol
caller must hold a :term:`strong reference` to *obj* when calling this.

.. versionadded:: 3.14

.. c:function:: int PyUnstable_Object_IsUniquelyReferenced(PyObject *op)

Determine if *op* only has one reference.

On GIL-enabled builds, this function is equivalent to
:c:expr:`Py_REFCNT(op) == 1`.

On a :term:`free threaded <free threading>` build, this checks if *op*'s
:term:`reference count` is equal to one and additionally checks if *op*
is only used by this thread. :c:expr:`Py_REFCNT(op) == 1` is **not**
thread-safe on free threaded builds; prefer this function.

The caller must hold an :term:`attached thread state`, despite the fact
that this function doesn't call into the Python interpreter. This function
cannot fail.

.. versionadded:: 3.14
7 changes: 7 additions & 0 deletions Doc/c-api/refcounting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ of Python objects.

Use the :c:func:`Py_SET_REFCNT()` function to set an object reference count.

.. note::

On :term:`free threaded <free threading>` builds of Python, returning 1
isn't sufficient to determine if it's safe to treat *o* as having no
access by other threads. Use :c:func:`PyUnstable_Object_IsUniquelyReferenced`
for that instead.

.. versionchanged:: 3.10
:c:func:`Py_REFCNT()` is changed to the inline static function.

Expand Down
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2123,6 +2123,9 @@ New features
take a C integer and produce a Python :class:`bool` object. (Contributed by
Pablo Galindo in :issue:`45325`.)

* Add :c:func:`PyUnstable_Object_IsUniquelyReferenced` as a replacement for
``Py_REFCNT(op) == 1`` on :term:`free threaded <free threading>` builds.
(Contributed by Peter Bierma in :gh:`133140`.)

Limited C API changes
---------------------
Expand Down
2 changes: 2 additions & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -551,3 +551,5 @@ PyAPI_FUNC(int) PyUnstable_IsImmortal(PyObject *);
// before calling this function in order to avoid spurious failures.
PyAPI_FUNC(int) PyUnstable_TryIncRef(PyObject *);
PyAPI_FUNC(void) PyUnstable_EnableTryIncRef(PyObject *);

PyAPI_FUNC(int) PyUnstable_Object_IsUniquelyReferenced(PyObject *);
10 changes: 10 additions & 0 deletions Lib/test/test_capi/test_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,16 @@ def silly_func(obj):
self.assertTrue(_testinternalcapi.has_deferred_refcount(silly_list))


class IsUniquelyReferencedTest(unittest.TestCase):
"""Test PyUnstable_Object_IsUniquelyReferenced"""
def test_is_uniquely_referenced(self):
self.assertTrue(_testcapi.is_uniquely_referenced(object()))
self.assertTrue(_testcapi.is_uniquely_referenced([]))
# Immortals
self.assertFalse(_testcapi.is_uniquely_referenced("spanish inquisition"))
self.assertFalse(_testcapi.is_uniquely_referenced(42))
# CRASHES is_uniquely_referenced(NULL)

class CAPITest(unittest.TestCase):
def check_negative_refcount(self, code):
# bpo-35059: Check that Py_DECREF() reports the correct filename
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add :c:func:`PyUnstable_Object_IsUniquelyReferenced` as a replacement for
``Py_REFNCT(op) == 1`` on :term:`free threaded <free threading>`
builds of Python.
8 changes: 8 additions & 0 deletions Modules/_testcapi/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,13 @@ clear_managed_dict(PyObject *self, PyObject *obj)
}


static PyObject *
is_uniquely_referenced(PyObject *self, PyObject *op)
{
return PyBool_FromLong(PyUnstable_Object_IsUniquelyReferenced(op));
}


static PyMethodDef test_methods[] = {
{"call_pyobject_print", call_pyobject_print, METH_VARARGS},
{"pyobject_print_null", pyobject_print_null, METH_VARARGS},
Expand All @@ -495,6 +502,7 @@ static PyMethodDef test_methods[] = {
{"test_py_is_macros", test_py_is_macros, METH_NOARGS},
{"test_py_is_funcs", test_py_is_funcs, METH_NOARGS},
{"clear_managed_dict", clear_managed_dict, METH_O, NULL},
{"is_uniquely_referenced", is_uniquely_referenced, METH_O},
{NULL},
};

Expand Down
8 changes: 8 additions & 0 deletions Objects/object.c
Original file line number Diff line number Diff line change
Expand Up @@ -3199,3 +3199,11 @@ PyUnstable_IsImmortal(PyObject *op)
assert(op != NULL);
return _Py_IsImmortal(op);
}

int
PyUnstable_Object_IsUniquelyReferenced(PyObject *op)
{
_Py_AssertHoldsTstate();
assert(op != NULL);
return _PyObject_IsUniquelyReferenced(op);
}
Loading