https://github.com/python/cpython/commit/b275b8f34210bae2fc1e0af23515df5efe911c8e
commit: b275b8f34210bae2fc1e0af23515df5efe911c8e
branch: main
author: Peter Bierma <zintensity...@gmail.com>
committer: vstinner <vstin...@python.org>
date: 2025-05-05T21:01:20+02:00
summary:

gh-133140: Add `PyUnstable_Object_IsUniquelyReferenced` for free-threading 
(#133144)

files:
A Misc/NEWS.d/next/C_API/2025-04-29-06-27-46.gh-issue-133140.IPGGc3.rst
M Doc/c-api/object.rst
M Doc/c-api/refcounting.rst
M Doc/whatsnew/3.14.rst
M Include/cpython/object.h
M Lib/test/test_capi/test_object.py
M Modules/_testcapi/object.c
M Objects/object.c

diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst
index efad4d215b1986..0fd159f1eb87f8 100644
--- a/Doc/c-api/object.rst
+++ b/Doc/c-api/object.rst
@@ -737,3 +737,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
diff --git a/Doc/c-api/refcounting.rst b/Doc/c-api/refcounting.rst
index 83febcf70a5548..b23f016f9b0a06 100644
--- a/Doc/c-api/refcounting.rst
+++ b/Doc/c-api/refcounting.rst
@@ -23,7 +23,14 @@ of Python objects.
 
    Use the :c:func:`Py_SET_REFCNT()` function to set an object reference count.
 
-   See also the function 
:c:func:`PyUnstable_Object_IsUniqueReferencedTemporary()`.
+   .. 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.
+
+      See also the function 
:c:func:`PyUnstable_Object_IsUniqueReferencedTemporary()`.
 
    .. versionchanged:: 3.10
       :c:func:`Py_REFCNT()` is changed to the inline static function.
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 795403a9b7c107..851611ad170c91 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -2469,6 +2469,10 @@ New features
   be used in some cases as a replacement for checking if :c:func:`Py_REFCNT`
   is ``1`` for Python objects passed as arguments to C API functions.
 
+* 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
 ---------------------
diff --git a/Include/cpython/object.h b/Include/cpython/object.h
index 818fc7d05605ff..36000c887b2d9a 100644
--- a/Include/cpython/object.h
+++ b/Include/cpython/object.h
@@ -501,3 +501,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 *);
diff --git a/Lib/test/test_capi/test_object.py 
b/Lib/test/test_capi/test_object.py
index 54a01ac7c4a7ae..127862546b1bce 100644
--- a/Lib/test/test_capi/test_object.py
+++ b/Lib/test/test_capi/test_object.py
@@ -174,6 +174,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
diff --git 
a/Misc/NEWS.d/next/C_API/2025-04-29-06-27-46.gh-issue-133140.IPGGc3.rst 
b/Misc/NEWS.d/next/C_API/2025-04-29-06-27-46.gh-issue-133140.IPGGc3.rst
new file mode 100644
index 00000000000000..f73b7de2b5c7c2
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-04-29-06-27-46.gh-issue-133140.IPGGc3.rst
@@ -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.
diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c
index 5c67adfee29dc1..798ef97c495aeb 100644
--- a/Modules/_testcapi/object.c
+++ b/Modules/_testcapi/object.c
@@ -478,6 +478,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},
@@ -503,6 +510,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},
 };
 
diff --git a/Objects/object.c b/Objects/object.c
index f5fe25eb5aaf8f..e9a93f87be4cf7 100644
--- a/Objects/object.c
+++ b/Objects/object.c
@@ -3275,3 +3275,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);
+}

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to