From 9c95806ee0c0837b7a10a71329b471615eab1bb9 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 15 Nov 2023 22:38:56 +0100 Subject: [PATCH] gh-111696: Add type.__fullyqualname__ attribute Add PyType_GetFullyQualName() function. --- Doc/library/stdtypes.rst | 9 ++++ Doc/whatsnew/3.13.rst | 5 +++ Include/cpython/object.h | 1 + Lib/test/test_builtin.py | 17 +++++++- Modules/_testcapimodule.c | 54 ++++++++++++++++++++---- Objects/typeobject.c | 89 +++++++++++++++++++++++++++++---------- 6 files changed, 142 insertions(+), 33 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index f204b287b565eb3..212c46b80c12f2f 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5496,6 +5496,15 @@ types, where they are relevant. Some of these are not reported by the .. versionadded:: 3.3 +.. attribute:: class.__fullyqualname__ + + The fully qualified name of the class instance: + ``f"{class.__module__}.{class.__qualname__}"``, or ``class.__qualname__`` if + ``class.__module__`` is not a string or is equal to ``"builtins"``. + + .. versionadded:: 3.13 + + .. attribute:: definition.__type_params__ The :ref:`type parameters ` of generic classes, functions, diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index b64cfc51f75701b..0f1e2d8925aeae8 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -125,6 +125,11 @@ Other Language Changes equivalent of the :option:`-X frozen_modules <-X>` command-line option. (Contributed by Yilei Yang in :gh:`111374`.) +* Add :attr:`__fullyqualname__ ` read-only attribute + to types: the fully qualified type name. + (Contributed by Victor Stinner in :gh:`111696`.) + + New Modules =========== diff --git a/Include/cpython/object.h b/Include/cpython/object.h index 762e8a3b86ee1e5..b63608702525ca6 100644 --- a/Include/cpython/object.h +++ b/Include/cpython/object.h @@ -271,6 +271,7 @@ PyAPI_FUNC(const char *) _PyType_Name(PyTypeObject *); PyAPI_FUNC(PyObject *) _PyType_Lookup(PyTypeObject *, PyObject *); PyAPI_FUNC(PyObject *) PyType_GetModuleByDef(PyTypeObject *, PyModuleDef *); PyAPI_FUNC(PyObject *) PyType_GetDict(PyTypeObject *); +PyAPI_FUNC(PyObject *) PyType_GetFullyQualName(PyTypeObject *); PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int); PyAPI_FUNC(void) _Py_BreakPoint(void); diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index b7966f8f03875b3..13d25abf217efb6 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -2430,6 +2430,7 @@ def test_new_type(self): self.assertEqual(A.__name__, 'A') self.assertEqual(A.__qualname__, 'A') self.assertEqual(A.__module__, __name__) + self.assertEqual(A.__fullyqualname__, f'{__name__}.A') self.assertEqual(A.__bases__, (object,)) self.assertIs(A.__base__, object) x = A() @@ -2443,6 +2444,7 @@ def ham(self): self.assertEqual(C.__name__, 'C') self.assertEqual(C.__qualname__, 'C') self.assertEqual(C.__module__, __name__) + self.assertEqual(C.__fullyqualname__, f'{__name__}.C') self.assertEqual(C.__bases__, (B, int)) self.assertIs(C.__base__, int) self.assertIn('spam', C.__dict__) @@ -2464,10 +2466,11 @@ def test_type_nokwargs(self): def test_type_name(self): for name in 'A', '\xc4', '\U0001f40d', 'B.A', '42', '': with self.subTest(name=name): - A = type(name, (), {}) + A = type(name, (), {'__qualname__': f'Test.{name}'}) self.assertEqual(A.__name__, name) - self.assertEqual(A.__qualname__, name) + self.assertEqual(A.__qualname__, f"Test.{name}") self.assertEqual(A.__module__, __name__) + self.assertEqual(A.__fullyqualname__, f'{__name__}.Test.{name}') with self.assertRaises(ValueError): type('A\x00B', (), {}) with self.assertRaises(UnicodeEncodeError): @@ -2482,6 +2485,7 @@ def test_type_name(self): self.assertEqual(C.__name__, name) self.assertEqual(C.__qualname__, 'C') self.assertEqual(C.__module__, __name__) + self.assertEqual(C.__fullyqualname__, f'{__name__}.C') A = type('C', (), {}) with self.assertRaises(ValueError): @@ -2494,11 +2498,19 @@ def test_type_name(self): A.__name__ = b'A' self.assertEqual(A.__name__, 'C') + # if __module__ is not a string, ignore it silently + class D: + pass + self.assertEqual(D.__fullyqualname__, f'{__name__}.{D.__qualname__}') + D.__module__ = 123 + self.assertEqual(D.__fullyqualname__, D.__qualname__) + def test_type_qualname(self): A = type('A', (), {'__qualname__': 'B.C'}) self.assertEqual(A.__name__, 'A') self.assertEqual(A.__qualname__, 'B.C') self.assertEqual(A.__module__, __name__) + self.assertEqual(A.__fullyqualname__, f'{__name__}.B.C') with self.assertRaises(TypeError): type('A', (), {'__qualname__': b'B'}) self.assertEqual(A.__qualname__, 'B.C') @@ -2506,6 +2518,7 @@ def test_type_qualname(self): A.__qualname__ = 'D.E' self.assertEqual(A.__name__, 'A') self.assertEqual(A.__qualname__, 'D.E') + self.assertEqual(A.__fullyqualname__, f'{__name__}.D.E') with self.assertRaises(TypeError): A.__qualname__ = b'B' self.assertEqual(A.__qualname__, 'D.E') diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 999bd866f148144..17b2437db76d75b 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -573,11 +573,11 @@ static PyObject * test_get_type_name(PyObject *self, PyObject *Py_UNUSED(ignored)) { PyObject *tp_name = PyType_GetName(&PyLong_Type); - assert(strcmp(PyUnicode_AsUTF8(tp_name), "int") == 0); + assert(PyUnicode_EqualToUTF8(tp_name, "int")); Py_DECREF(tp_name); tp_name = PyType_GetName(&PyModule_Type); - assert(strcmp(PyUnicode_AsUTF8(tp_name), "module") == 0); + assert(PyUnicode_EqualToUTF8(tp_name, "module")); Py_DECREF(tp_name); PyObject *HeapTypeNameType = PyType_FromSpec(&HeapTypeNameType_Spec); @@ -585,7 +585,7 @@ test_get_type_name(PyObject *self, PyObject *Py_UNUSED(ignored)) Py_RETURN_NONE; } tp_name = PyType_GetName((PyTypeObject *)HeapTypeNameType); - assert(strcmp(PyUnicode_AsUTF8(tp_name), "HeapTypeNameType") == 0); + assert(PyUnicode_EqualToUTF8(tp_name, "HeapTypeNameType")); Py_DECREF(tp_name); PyObject *name = PyUnicode_FromString("test_name"); @@ -597,7 +597,7 @@ test_get_type_name(PyObject *self, PyObject *Py_UNUSED(ignored)) goto done; } tp_name = PyType_GetName((PyTypeObject *)HeapTypeNameType); - assert(strcmp(PyUnicode_AsUTF8(tp_name), "test_name") == 0); + assert(PyUnicode_EqualToUTF8(tp_name, "test_name")); Py_DECREF(name); Py_DECREF(tp_name); @@ -611,11 +611,11 @@ static PyObject * test_get_type_qualname(PyObject *self, PyObject *Py_UNUSED(ignored)) { PyObject *tp_qualname = PyType_GetQualName(&PyLong_Type); - assert(strcmp(PyUnicode_AsUTF8(tp_qualname), "int") == 0); + assert(PyUnicode_EqualToUTF8(tp_qualname, "int")); Py_DECREF(tp_qualname); tp_qualname = PyType_GetQualName(&PyODict_Type); - assert(strcmp(PyUnicode_AsUTF8(tp_qualname), "OrderedDict") == 0); + assert(PyUnicode_EqualToUTF8(tp_qualname, "OrderedDict")); Py_DECREF(tp_qualname); PyObject *HeapTypeNameType = PyType_FromSpec(&HeapTypeNameType_Spec); @@ -623,7 +623,7 @@ test_get_type_qualname(PyObject *self, PyObject *Py_UNUSED(ignored)) Py_RETURN_NONE; } tp_qualname = PyType_GetQualName((PyTypeObject *)HeapTypeNameType); - assert(strcmp(PyUnicode_AsUTF8(tp_qualname), "HeapTypeNameType") == 0); + assert(PyUnicode_EqualToUTF8(tp_qualname, "HeapTypeNameType")); Py_DECREF(tp_qualname); PyObject *spec_name = PyUnicode_FromString(HeapTypeNameType_Spec.name); @@ -636,8 +636,7 @@ test_get_type_qualname(PyObject *self, PyObject *Py_UNUSED(ignored)) goto done; } tp_qualname = PyType_GetQualName((PyTypeObject *)HeapTypeNameType); - assert(strcmp(PyUnicode_AsUTF8(tp_qualname), - "_testcapi.HeapTypeNameType") == 0); + assert(PyUnicode_EqualToUTF8(tp_qualname, "_testcapi.HeapTypeNameType")); Py_DECREF(spec_name); Py_DECREF(tp_qualname); @@ -646,6 +645,42 @@ test_get_type_qualname(PyObject *self, PyObject *Py_UNUSED(ignored)) Py_RETURN_NONE; } +static PyObject * +test_get_type_fullyqualname(PyObject *self, PyObject *Py_UNUSED(ignored)) +{ + PyObject *name = PyType_GetFullyQualName(&PyLong_Type); + assert(PyUnicode_EqualToUTF8(name, "int")); + Py_DECREF(name); + + name = PyType_GetFullyQualName(&PyODict_Type); + assert(PyUnicode_EqualToUTF8(name, "collections.OrderedDict")); + Py_DECREF(name); + + PyObject *HeapTypeNameType = PyType_FromSpec(&HeapTypeNameType_Spec); + if (HeapTypeNameType == NULL) { + Py_RETURN_NONE; + } + name = PyType_GetFullyQualName((PyTypeObject *)HeapTypeNameType); + assert(PyUnicode_EqualToUTF8(name, "_testcapi.HeapTypeNameType")); + Py_DECREF(name); + + PyObject *new_name = PyUnicode_FromString("override_name"); + if (new_name == NULL) { + goto done; + } + + int res = PyObject_SetAttrString(HeapTypeNameType, + "__fullyqualname__", new_name); + Py_DECREF(new_name); + assert(res < 0); + assert(PyErr_ExceptionMatches(PyExc_AttributeError)); + PyErr_Clear(); + + done: + Py_DECREF(HeapTypeNameType); + Py_RETURN_NONE; +} + static PyObject * test_get_type_dict(PyObject *self, PyObject *Py_UNUSED(ignored)) { @@ -3212,6 +3247,7 @@ static PyMethodDef TestMethods[] = { {"test_get_statictype_slots", test_get_statictype_slots, METH_NOARGS}, {"test_get_type_name", test_get_type_name, METH_NOARGS}, {"test_get_type_qualname", test_get_type_qualname, METH_NOARGS}, + {"test_get_type_fullyqualname", test_get_type_fullyqualname, METH_NOARGS}, {"test_get_type_dict", test_get_type_dict, METH_NOARGS}, {"_test_thread_state", test_thread_state, METH_VARARGS}, #ifndef MS_WINDOWS diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 4464b5af8cd15be..9e17cea1a07ea97 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1123,6 +1123,58 @@ type_set_module(PyTypeObject *type, PyObject *value, void *context) return PyDict_SetItem(dict, &_Py_ID(__module__), value); } + +static PyObject* +type_fullyqualname(PyTypeObject *type, int is_repr) +{ + // type is a static type and PyType_Ready() was not called on it yet? + if (type->tp_name == NULL) { + PyErr_SetString(PyExc_TypeError, "static type not initialized"); + return NULL; + } + + if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) { + return PyUnicode_FromString(type->tp_name); + } + + PyObject *qualname = type_qualname(type, NULL); + if (qualname == NULL) { + return NULL; + } + + PyObject *module = type_module(type, NULL); + if (module == NULL) { + if (is_repr) { + // type_repr() ignores type_module() errors + PyErr_Clear(); + return qualname; + } + + Py_DECREF(qualname); + return NULL; + } + + PyObject *result; + if (PyUnicode_Check(module) + && !_PyUnicode_Equal(module, &_Py_ID(builtins))) + { + result = PyUnicode_FromFormat("%U.%U", module, qualname); + } + else { + result = Py_NewRef(qualname); + } + Py_DECREF(module); + Py_DECREF(qualname); + return result; +} + +static PyObject * +type_get_fullyqualname(PyTypeObject *type, void *context) +{ + return type_fullyqualname(type, 0); +} + + static PyObject * type_abstractmethods(PyTypeObject *type, void *context) { @@ -1583,6 +1635,7 @@ type___subclasscheck___impl(PyTypeObject *self, PyObject *subclass) static PyGetSetDef type_getsets[] = { {"__name__", (getter)type_name, (setter)type_set_name, NULL}, {"__qualname__", (getter)type_qualname, (setter)type_set_qualname, NULL}, + {"__fullyqualname__", (getter)type_get_fullyqualname, NULL, NULL}, {"__bases__", (getter)type_get_bases, (setter)type_set_bases, NULL}, {"__mro__", (getter)type_get_mro, NULL, NULL}, {"__module__", (getter)type_module, (setter)type_set_module, NULL}, @@ -1600,33 +1653,18 @@ static PyObject * type_repr(PyTypeObject *type) { if (type->tp_name == NULL) { - // type_repr() called before the type is fully initialized - // by PyType_Ready(). + // If type_repr() is called before the type is fully initialized + // by PyType_Ready(), just format the type memory address. return PyUnicode_FromFormat("", type); } - PyObject *mod, *name, *rtn; - - mod = type_module(type, NULL); - if (mod == NULL) - PyErr_Clear(); - else if (!PyUnicode_Check(mod)) { - Py_SETREF(mod, NULL); - } - name = type_qualname(type, NULL); - if (name == NULL) { - Py_XDECREF(mod); + PyObject *fullqualname = type_fullyqualname(type, 1); + if (fullqualname == NULL) { return NULL; } - - if (mod != NULL && !_PyUnicode_Equal(mod, &_Py_ID(builtins))) - rtn = PyUnicode_FromFormat("", mod, name); - else - rtn = PyUnicode_FromFormat("", type->tp_name); - - Py_XDECREF(mod); - Py_DECREF(name); - return rtn; + PyObject *result = PyUnicode_FromFormat("", fullqualname); + Py_DECREF(fullqualname); + return result; } static PyObject * @@ -4540,6 +4578,13 @@ PyType_GetQualName(PyTypeObject *type) return type_qualname(type, NULL); } +PyObject * +PyType_GetFullyQualName(PyTypeObject *type) +{ + return type_fullyqualname(type, 0); +} + + void * PyType_GetSlot(PyTypeObject *type, int slot) {