From 41bf51d00d4ae4f1de91187e0a788c3ea8b7dd2e Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 29 Apr 2025 06:06:33 -0400 Subject: [PATCH 01/10] Add PyUnstable_Object_IsUniquelyReferenced() --- Include/cpython/object.h | 2 ++ Objects/object.c | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/Include/cpython/object.h b/Include/cpython/object.h index e2300aee7a207a..aa52338f74c9f9 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -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 *); diff --git a/Objects/object.c b/Objects/object.c index 99bb1d9c0bfad5..17734026681bc4 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -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); +} From 0affc479d5bb4476e4c047ed6e694e8b3f3a7dfc Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 29 Apr 2025 06:13:57 -0400 Subject: [PATCH 02/10] Add a test. --- Lib/test/test_capi/test_object.py | 11 +++++++++++ Modules/_testcapimodule.c | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 3e8fd91b9a67a0..b7707ad1546c06 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -173,6 +173,17 @@ 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)) + + assert_python_failure("-c", "import _testcapi; _testcapi.is_uniquely_referenced(None)") + class CAPITest(unittest.TestCase): def check_negative_refcount(self, code): # bpo-35059: Check that Py_DECREF() reports the correct filename diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 3aa6e4c9e43a26..790e0884a5dd20 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -14,6 +14,7 @@ #include "frameobject.h" // PyFrame_New() #include "marshal.h" // PyMarshal_WriteLongToFile() +#include "object.h" #include // FLT_MAX #include @@ -2546,6 +2547,13 @@ toggle_reftrace_printer(PyObject *ob, PyObject *arg) Py_RETURN_NONE; } +static PyObject * +is_uniquely_referenced(PyObject *self, PyObject *op) +{ + NULLABLE(op); // crashes when op == NULL + return PyBool_FromLong(PyUnstable_Object_IsUniquelyReferenced(op)); +} + static PyMethodDef TestMethods[] = { {"set_errno", set_errno, METH_VARARGS}, {"test_config", test_config, METH_NOARGS}, @@ -2640,6 +2648,7 @@ static PyMethodDef TestMethods[] = { {"test_atexit", test_atexit, METH_NOARGS}, {"code_offset_to_line", _PyCFunction_CAST(code_offset_to_line), METH_FASTCALL}, {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, + {"is_uniquely_referenced", is_uniquely_referenced, METH_O}, {NULL, NULL} /* sentinel */ }; From cc85d55660a6ca0742bfe57f55b6b6fb5cf2650c Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 29 Apr 2025 06:14:07 -0400 Subject: [PATCH 03/10] Fix newline change. --- Lib/test/test_capi/test_object.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index b7707ad1546c06..7c73cdc2d0a22b 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -184,6 +184,7 @@ def test_is_uniquely_referenced(self): assert_python_failure("-c", "import _testcapi; _testcapi.is_uniquely_referenced(None)") + class CAPITest(unittest.TestCase): def check_negative_refcount(self, code): # bpo-35059: Check that Py_DECREF() reports the correct filename From 822ac2d9c145ca0de7166fbe79c7967471455065 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 29 Apr 2025 06:24:46 -0400 Subject: [PATCH 04/10] Update the documentation --- Doc/c-api/object.rst | 18 ++++++++++++++++++ Doc/c-api/refcounting.rst | 7 +++++++ Doc/whatsnew/3.14.rst | 3 +++ 3 files changed, 28 insertions(+) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index bef3a79ccd0e21..bfd6ad9bc4cc50 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -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 ` 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 d75dad737bc992..709b142b83eca8 100644 --- a/Doc/c-api/refcounting.rst +++ b/Doc/c-api/refcounting.rst @@ -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 ` 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. diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 9b28d7b6247309..e02940a05467df 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -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 + :c:expr:`Py_REFCNT(op) == 1` on :term:`free threaded ` builds. + (Contributed by Peter Bierma in :gh:`133140`.) Limited C API changes --------------------- From b190548fcee42eef895f6c6a5951b9861b5844c4 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 29 Apr 2025 06:27:53 -0400 Subject: [PATCH 05/10] Add blurb. --- .../next/C_API/2025-04-29-06-27-46.gh-issue-133140.IPGGc3.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/C_API/2025-04-29-06-27-46.gh-issue-133140.IPGGc3.rst 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..10eae870f13b87 --- /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 +:c:expr:`Py_REFNCT(op) == 1` on :term:`free threaded ` +builds of Python. From c5d1be3a9f62cabb05ef330cecb72f0d13f263d4 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 29 Apr 2025 06:57:25 -0400 Subject: [PATCH 06/10] Don't use :c:expr: --- Doc/c-api/object.rst | 4 ++-- Doc/whatsnew/3.14.rst | 2 +- .../next/C_API/2025-04-29-06-27-46.gh-issue-133140.IPGGc3.rst | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index bfd6ad9bc4cc50..0769fd59bfca2d 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -711,11 +711,11 @@ Object Protocol Determine if *op* only has one reference. On GIL-enabled builds, this function is equivalent to - :c:expr:`Py_REFCNT(op) == 1`. + ``Py_REFCNT(op) == 1``. On a :term:`free threaded ` 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** + is only used by this thread. ``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 diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index e02940a05467df..404d909df29f67 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -2124,7 +2124,7 @@ New features Pablo Galindo in :issue:`45325`.) * Add :c:func:`PyUnstable_Object_IsUniquelyReferenced` as a replacement for - :c:expr:`Py_REFCNT(op) == 1` on :term:`free threaded ` builds. + ``Py_REFCNT(op) == 1`` on :term:`free threaded ` builds. (Contributed by Peter Bierma in :gh:`133140`.) Limited C API changes 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 index 10eae870f13b87..f73b7de2b5c7c2 100644 --- 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 @@ -1,3 +1,3 @@ Add :c:func:`PyUnstable_Object_IsUniquelyReferenced` as a replacement for -:c:expr:`Py_REFNCT(op) == 1` on :term:`free threaded ` +``Py_REFNCT(op) == 1`` on :term:`free threaded ` builds of Python. From 8711752f447646eb665cabc336d446b6cc475413 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 29 Apr 2025 06:58:46 -0400 Subject: [PATCH 07/10] Use :c:expr: where available. --- Doc/c-api/object.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/c-api/object.rst b/Doc/c-api/object.rst index 0769fd59bfca2d..bfd6ad9bc4cc50 100644 --- a/Doc/c-api/object.rst +++ b/Doc/c-api/object.rst @@ -711,11 +711,11 @@ Object Protocol Determine if *op* only has one reference. On GIL-enabled builds, this function is equivalent to - ``Py_REFCNT(op) == 1``. + :c:expr:`Py_REFCNT(op) == 1`. On a :term:`free threaded ` build, this checks if *op*'s :term:`reference count` is equal to one and additionally checks if *op* - is only used by this thread. ``Py_REFCNT(op) == 1`` is **not** + 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 From 5cc3d1535d800eb7dc9a61a089d0a80e5612efdc Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Tue, 29 Apr 2025 12:12:42 +0000 Subject: [PATCH 08/10] Remove crashing test. --- Lib/test/test_capi/test_object.py | 4 +--- Modules/_testcapimodule.c | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/test/test_capi/test_object.py b/Lib/test/test_capi/test_object.py index 7c73cdc2d0a22b..ef82cef3de5cd3 100644 --- a/Lib/test/test_capi/test_object.py +++ b/Lib/test/test_capi/test_object.py @@ -181,9 +181,7 @@ def test_is_uniquely_referenced(self): # Immortals self.assertFalse(_testcapi.is_uniquely_referenced("spanish inquisition")) self.assertFalse(_testcapi.is_uniquely_referenced(42)) - - assert_python_failure("-c", "import _testcapi; _testcapi.is_uniquely_referenced(None)") - + # CRASHES is_uniquely_referenced(NULL) class CAPITest(unittest.TestCase): def check_negative_refcount(self, code): diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 790e0884a5dd20..c4e358e3d286fc 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2550,7 +2550,6 @@ toggle_reftrace_printer(PyObject *ob, PyObject *arg) static PyObject * is_uniquely_referenced(PyObject *self, PyObject *op) { - NULLABLE(op); // crashes when op == NULL return PyBool_FromLong(PyUnstable_Object_IsUniquelyReferenced(op)); } From d9063f56a158ecc46b508fb389690cf7d17fbbbb Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 1 May 2025 15:59:09 -0400 Subject: [PATCH 09/10] Move to object.c --- Modules/_testcapi/object.c | 8 ++++++++ Modules/_testcapimodule.c | 7 ------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Modules/_testcapi/object.c b/Modules/_testcapi/object.c index 2d538627d213fd..4743813dabc9ae 100644 --- a/Modules/_testcapi/object.c +++ b/Modules/_testcapi/object.c @@ -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}, @@ -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}, }; diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index c4e358e3d286fc..811d0905427238 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -14,7 +14,6 @@ #include "frameobject.h" // PyFrame_New() #include "marshal.h" // PyMarshal_WriteLongToFile() -#include "object.h" #include // FLT_MAX #include @@ -2547,11 +2546,6 @@ toggle_reftrace_printer(PyObject *ob, PyObject *arg) Py_RETURN_NONE; } -static PyObject * -is_uniquely_referenced(PyObject *self, PyObject *op) -{ - return PyBool_FromLong(PyUnstable_Object_IsUniquelyReferenced(op)); -} static PyMethodDef TestMethods[] = { {"set_errno", set_errno, METH_VARARGS}, @@ -2647,7 +2641,6 @@ static PyMethodDef TestMethods[] = { {"test_atexit", test_atexit, METH_NOARGS}, {"code_offset_to_line", _PyCFunction_CAST(code_offset_to_line), METH_FASTCALL}, {"toggle_reftrace_printer", toggle_reftrace_printer, METH_O}, - {"is_uniquely_referenced", is_uniquely_referenced, METH_O}, {NULL, NULL} /* sentinel */ }; From 6a52c02b4757ff49e241f1e5decdd66c1867552d Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 1 May 2025 15:59:48 -0400 Subject: [PATCH 10/10] Remove stray newline. --- Modules/_testcapimodule.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 811d0905427238..3aa6e4c9e43a26 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -2546,7 +2546,6 @@ toggle_reftrace_printer(PyObject *ob, PyObject *arg) Py_RETURN_NONE; } - static PyMethodDef TestMethods[] = { {"set_errno", set_errno, METH_VARARGS}, {"test_config", test_config, METH_NOARGS},