diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 83745f3d0ba46e..b932c860b9f25e 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -753,23 +753,25 @@ def test_43581(self): self.assertEqual(sys.__stdout__.encoding, sys.__stderr__.encoding) def test_intern(self): - has_is_interned = (test.support.check_impl_detail(cpython=True) - or hasattr(sys, '_is_interned')) self.assertRaises(TypeError, sys.intern) self.assertRaises(TypeError, sys.intern, b'abc') + has_is_interned = (test.support.check_impl_detail(cpython=True) + or hasattr(sys, '_is_interned')) if has_is_interned: self.assertRaises(TypeError, sys._is_interned) self.assertRaises(TypeError, sys._is_interned, b'abc') + + def _is_interned(obj): + tags = sys.get_object_tags(obj) + return tags["interned"] + s = "never interned before" + str(random.randrange(0, 10**9)) self.assertTrue(sys.intern(s) is s) - if has_is_interned: - self.assertIs(sys._is_interned(s), True) + self.assertIs(_is_interned(s), True) s2 = s.swapcase().swapcase() - if has_is_interned: - self.assertIs(sys._is_interned(s2), False) + self.assertIs(_is_interned(s2), False) self.assertTrue(sys.intern(s2) is s) - if has_is_interned: - self.assertIs(sys._is_interned(s2), False) + self.assertIs(_is_interned(s2), False) # Subclasses of string can't be interned, because they # provide too much opportunity for insane things to happen. @@ -781,8 +783,28 @@ def __hash__(self): return 123 self.assertRaises(TypeError, sys.intern, S("abc")) - if has_is_interned: - self.assertIs(sys._is_interned(S("abc")), False) + self.assertIs(_is_interned(S("abc")), False) + + @support.cpython_only + def test_get_object_tags(self): + keys = ("immortal", "interned", "deferred_refcount") + s = "foobar" + tags = sys.get_object_tags(s) + self.assertEqual(len(tags), len(keys)) + for k in keys: + self.assertIn(k, tags) + + @support.cpython_only + def test_set_object_tags(self): + keys = ("immortal", "interned", "deferred_refcount") + s = "should never interned before" + str(random.randrange(0, 10**9)) + origin_tags = sys.get_object_tags(s) + for k in keys: + self.assertFalse(origin_tags[k]) + sys.set_object_tag(s, k) + sys.set_object_tag(s, "unknown") + after_tags = sys.get_object_tags(s) + self.assertEqual(len(origin_tags), len(after_tags)) @support.cpython_only @requires_subinterpreters @@ -847,7 +869,8 @@ def test_subinterp_intern_singleton(self): assert id(s) == {id(s)} t = sys.intern(s) ''')) - self.assertTrue(sys._is_interned(s)) + tags = sys.get_object_tags(s) + self.assertTrue(tags["interned"]) def test_sys_flags(self): self.assertTrue(sys.flags) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-06-03-21-55-03.gh-issue-134819.M9PZZc.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-03-21-55-03.gh-issue-134819.M9PZZc.rst new file mode 100644 index 00000000000000..89b2531a5110df --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-06-03-21-55-03.gh-issue-134819.M9PZZc.rst @@ -0,0 +1,2 @@ +Add :func:`sys.get_object_tags` and :func:`sys.set_object_tags` for handling +CPython object implementation detail. Patch By Donghee Na. diff --git a/Python/clinic/sysmodule.c.h b/Python/clinic/sysmodule.c.h index a47e4d11b54441..c184e6be34caad 100644 --- a/Python/clinic/sysmodule.c.h +++ b/Python/clinic/sysmodule.c.h @@ -405,6 +405,95 @@ sys__is_immortal(PyObject *module, PyObject *op) return return_value; } +PyDoc_STRVAR(sys_get_object_tags__doc__, +"get_object_tags($module, op, /)\n" +"--\n" +"\n" +"Return the tags of the given object."); + +#define SYS_GET_OBJECT_TAGS_METHODDEF \ + {"get_object_tags", (PyCFunction)sys_get_object_tags, METH_O, sys_get_object_tags__doc__}, + +PyDoc_STRVAR(sys_set_object_tag__doc__, +"set_object_tag($module, /, object, tag, *, options=None)\n" +"--\n" +"\n" +"Set the tags of the given object."); + +#define SYS_SET_OBJECT_TAG_METHODDEF \ + {"set_object_tag", _PyCFunction_CAST(sys_set_object_tag), METH_FASTCALL|METH_KEYWORDS, sys_set_object_tag__doc__}, + +static PyObject * +sys_set_object_tag_impl(PyObject *module, PyObject *object, const char *tag, + PyObject *options); + +static PyObject * +sys_set_object_tag(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 3 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + Py_hash_t ob_hash; + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_hash = -1, + .ob_item = { &_Py_ID(object), &_Py_ID(tag), &_Py_ID(options), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"object", "tag", "options", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "set_object_tag", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[3]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2; + PyObject *object; + const char *tag; + PyObject *options = Py_None; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, + /*minpos*/ 2, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf); + if (!args) { + goto exit; + } + object = args[0]; + if (!PyUnicode_Check(args[1])) { + _PyArg_BadArgument("set_object_tag", "argument 'tag'", "str", args[1]); + goto exit; + } + Py_ssize_t tag_length; + tag = PyUnicode_AsUTF8AndSize(args[1], &tag_length); + if (tag == NULL) { + goto exit; + } + if (strlen(tag) != (size_t)tag_length) { + PyErr_SetString(PyExc_ValueError, "embedded null character"); + goto exit; + } + if (!noptargs) { + goto skip_optional_kwonly; + } + options = args[2]; +skip_optional_kwonly: + return_value = sys_set_object_tag_impl(module, object, tag, options); + +exit: + return return_value; +} + PyDoc_STRVAR(sys_settrace__doc__, "settrace($module, function, /)\n" "--\n" @@ -1948,4 +2037,4 @@ _jit_is_active(PyObject *module, PyObject *Py_UNUSED(ignored)) #ifndef SYS_GETANDROIDAPILEVEL_METHODDEF #define SYS_GETANDROIDAPILEVEL_METHODDEF #endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */ -/*[clinic end generated code: output=449d16326e69dcf6 input=a9049054013a1b77]*/ +/*[clinic end generated code: output=cf56a851495bb951 input=a9049054013a1b77]*/ diff --git a/Python/sysmodule.c b/Python/sysmodule.c index e5ae841d195d4f..f5933ea1bdac73 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -18,6 +18,7 @@ Data members: #include "pycore_audit.h" // _Py_AuditHookEntry #include "pycore_call.h" // _PyObject_CallNoArgs() #include "pycore_ceval.h" // _PyEval_SetAsyncGenFinalizer() +#include "pycore_object_deferred.h" // _PyObject_HasDeferredRefcount #include "pycore_frame.h" // _PyInterpreterFrame #include "pycore_import.h" // _PyImport_SetDLOpenFlags() #include "pycore_initconfig.h" // _PyStatus_EXCEPTION() @@ -1052,6 +1053,70 @@ sys__is_immortal_impl(PyObject *module, PyObject *op) return PyUnstable_IsImmortal(op); } + +/*[clinic input] +sys.get_object_tags -> object + + op: object + / +Return the tags of the given object. +[clinic start generated code]*/ + +static PyObject * +sys_get_object_tags(PyObject *module, PyObject *op) +/*[clinic end generated code: output=a68da7f1805c9216 input=75993fb67096e2ff]*/ +{ + assert(op != NULL); + PyObject *dict = PyDict_New(); + if (dict == NULL) { + return NULL; + } + + if (PyDict_SetItemString(dict, "immortal", PyBool_FromLong(PyUnstable_IsImmortal(op))) < 0) { + Py_DECREF(dict); + return NULL; + } + + if (PyDict_SetItemString(dict, "interned", PyBool_FromLong((PyUnicode_Check(op) && PyUnicode_CHECK_INTERNED(op)))) < 0) { + Py_DECREF(dict); + return NULL; + } + + if (PyDict_SetItemString(dict, "deferred_refcount", PyBool_FromLong(_PyObject_HasDeferredRefcount(op))) < 0) { + Py_DECREF(dict); + return NULL; + } + + return dict; +} + +/*[clinic input] +sys.set_object_tag -> object + + object: object + tag: str + * + options: object = None + +Set the tags of the given object. +[clinic start generated code]*/ + +static PyObject * +sys_set_object_tag_impl(PyObject *module, PyObject *object, const char *tag, + PyObject *options) +/*[clinic end generated code: output=b0fb5e9931feb4aa input=b64c9bd958c75f11]*/ +{ + assert(object != NULL); + if (strcmp(tag, "interned") == 0) { + Py_INCREF(object); + _PyUnicode_InternMortal(_PyInterpreterState_GET(), &object); + } + else if(strcmp(tag, "deferred_refcount") == 0) { + PyUnstable_Object_EnableDeferredRefcount(object); + } + Py_RETURN_NONE; +} + /* * Cached interned string objects used for calling the profile and * trace functions. @@ -2796,6 +2861,8 @@ static PyMethodDef sys_methods[] = { SYS__IS_IMMORTAL_METHODDEF SYS_INTERN_METHODDEF SYS__IS_INTERNED_METHODDEF + SYS_GET_OBJECT_TAGS_METHODDEF + SYS_SET_OBJECT_TAG_METHODDEF SYS_IS_FINALIZING_METHODDEF SYS_MDEBUG_METHODDEF SYS_SETSWITCHINTERVAL_METHODDEF