From d5e8a3725e3307906f51ca093869340420a55bca Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Wed, 15 Nov 2023 22:38:56 +0100 Subject: [PATCH 1/8] gh-111696: Add type.__fullyqualname__ attribute Add PyType_GetFullyQualifiedName() function with documentation and tests. --- Doc/c-api/type.rst | 7 ++ Doc/library/stdtypes.rst | 9 ++ Doc/whatsnew/3.13.rst | 10 +++ Include/cpython/object.h | 1 + Lib/test/test_builtin.py | 17 +++- ...-11-15-23-43-23.gh-issue-111696.RP59HZ.rst | 3 + ...-11-15-23-43-53.gh-issue-111696.zwQTow.rst | 2 + Modules/_testcapimodule.c | 54 ++++++++++-- Objects/typeobject.c | 85 ++++++++++++++----- 9 files changed, 157 insertions(+), 31 deletions(-) create mode 100644 Misc/NEWS.d/next/C API/2023-11-15-23-43-23.gh-issue-111696.RP59HZ.rst create mode 100644 Misc/NEWS.d/next/Core and Builtins/2023-11-15-23-43-53.gh-issue-111696.zwQTow.rst diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 5aaa8147dd3176a..bb97e18fbfec013 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -185,6 +185,13 @@ Type Objects .. versionadded:: 3.11 +.. c:function:: PyObject* PyType_GetFullyQualifiedName(PyTypeObject *type) + + Return the type's fully qualified name. Equivalent to getting the + type's :attr:`__fullyqualname__ ` attribute. + + .. versionadded:: 3.13 + .. c:function:: void* PyType_GetSlot(PyTypeObject *type, int slot) Return the function pointer stored in the given slot. If the 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..5ff1f2302cd3692 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 =========== @@ -1181,6 +1186,11 @@ New Features :exc:`KeyError` if the key missing. (Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.) +* Add :c:func:`PyType_GetFullyQualifiedName` function: get the type's fully + qualified name. It is equivalent to getting the type's + :attr:`__fullyqualname__ ` attribute. + (Contributed by Victor Stinner in :gh:`111696`.) + Porting to Python 3.13 ---------------------- diff --git a/Include/cpython/object.h b/Include/cpython/object.h index 762e8a3b86ee1e5..44305e8e9606fd8 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_GetFullyQualifiedName(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/Misc/NEWS.d/next/C API/2023-11-15-23-43-23.gh-issue-111696.RP59HZ.rst b/Misc/NEWS.d/next/C API/2023-11-15-23-43-23.gh-issue-111696.RP59HZ.rst new file mode 100644 index 000000000000000..9e6b0fac07ba1e6 --- /dev/null +++ b/Misc/NEWS.d/next/C API/2023-11-15-23-43-23.gh-issue-111696.RP59HZ.rst @@ -0,0 +1,3 @@ +Add :c:func:`PyType_GetFullyQualifiedName` function: get the type's fully +qualified name. It is equivalent to getting the type's :attr:`__fullyqualname__ +` attribute. Patch by Victor Stinner. diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-11-15-23-43-53.gh-issue-111696.zwQTow.rst b/Misc/NEWS.d/next/Core and Builtins/2023-11-15-23-43-53.gh-issue-111696.zwQTow.rst new file mode 100644 index 000000000000000..c0119adf092757e --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2023-11-15-23-43-53.gh-issue-111696.zwQTow.rst @@ -0,0 +1,2 @@ +Add :attr:`__fullyqualname__ ` read-only attribute +to types: the fully qualified type name. Patch by Victor Stinner. diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 999bd866f148144..000cd8cd1bedb7c 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_GetFullyQualifiedName(&PyLong_Type); + assert(PyUnicode_EqualToUTF8(name, "int")); + Py_DECREF(name); + + name = PyType_GetFullyQualifiedName(&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_GetFullyQualifiedName((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..3bd361f5e8cdfb0 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); + PyObject *name = type_fullyqualname(type, 1); if (name == NULL) { - Py_XDECREF(mod); 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); + PyObject *result = PyUnicode_FromFormat("", name); Py_DECREF(name); - return rtn; + return result; } static PyObject * @@ -4540,6 +4578,13 @@ PyType_GetQualName(PyTypeObject *type) return type_qualname(type, NULL); } +PyObject * +PyType_GetFullyQualifiedName(PyTypeObject *type) +{ + return type_get_fullyqualname(type, NULL); +} + + void * PyType_GetSlot(PyTypeObject *type, int slot) { From 82dd9564a1ec1b64dd34a28466df81edc5c8ee7e Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 16 Nov 2023 02:54:25 +0100 Subject: [PATCH 2/8] Use type.__fullyqualname__ attribute --- Lib/_collections_abc.py | 4 +--- Lib/_py_abc.py | 2 +- Lib/abc.py | 2 +- Lib/codecs.py | 4 ++-- Lib/contextlib.py | 4 ++-- Lib/doctest.py | 2 +- Lib/inspect.py | 4 ++-- Lib/multiprocessing/pool.py | 2 +- Lib/pdb.py | 2 +- Lib/test/support/asyncore.py | 2 +- Lib/test/test_configparser.py | 3 +-- Lib/test/test_traceback.py | 12 ++++++------ Lib/test/test_zipimport_support.py | 2 +- Lib/threading.py | 10 +++++----- Lib/tkinter/__init__.py | 3 +-- Lib/tkinter/font.py | 4 ++-- Lib/typing.py | 9 ++------- Lib/unittest/async_case.py | 2 +- Lib/unittest/case.py | 2 +- Lib/unittest/loader.py | 4 +--- Lib/unittest/util.py | 2 +- 21 files changed, 35 insertions(+), 46 deletions(-) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 601107d2d867718..8af47dc9aeb9fe4 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -528,9 +528,7 @@ def _type_repr(obj): (Keep this roughly in sync with the typing version.) """ if isinstance(obj, type): - if obj.__module__ == 'builtins': - return obj.__qualname__ - return f'{obj.__module__}.{obj.__qualname__}' + return obj.__fullyqualname__ if obj is Ellipsis: return '...' if isinstance(obj, FunctionType): diff --git a/Lib/_py_abc.py b/Lib/_py_abc.py index c870ae9048b4f13..8698742d65d3d17 100644 --- a/Lib/_py_abc.py +++ b/Lib/_py_abc.py @@ -71,7 +71,7 @@ def register(cls, subclass): def _dump_registry(cls, file=None): """Debug helper to print the ABC registry.""" - print(f"Class: {cls.__module__}.{cls.__qualname__}", file=file) + print(f"Class: {cls.__fullyqualname__}", file=file) print(f"Inv. counter: {get_cache_token()}", file=file) for name in cls.__dict__: if name.startswith("_abc_"): diff --git a/Lib/abc.py b/Lib/abc.py index f8a4e11ce9c3b1e..9646272cc3b6c73 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -124,7 +124,7 @@ def __subclasscheck__(cls, subclass): def _dump_registry(cls, file=None): """Debug helper to print the ABC registry.""" - print(f"Class: {cls.__module__}.{cls.__qualname__}", file=file) + print(f"Class: {cls.__fullyqualname__}", file=file) print(f"Inv. counter: {get_cache_token()}", file=file) (_abc_registry, _abc_cache, _abc_negative_cache, _abc_negative_cache_version) = _get_dump(cls) diff --git a/Lib/codecs.py b/Lib/codecs.py index 9b35b6127dd01c7..d77d6ff3039ba79 100644 --- a/Lib/codecs.py +++ b/Lib/codecs.py @@ -107,8 +107,8 @@ def __new__(cls, encode, decode, streamreader=None, streamwriter=None, return self def __repr__(self): - return "<%s.%s object for encoding %s at %#x>" % \ - (self.__class__.__module__, self.__class__.__qualname__, + return "<%s object for encoding %s at %#x>" % \ + (self.__class__.__fullyqualname__, self.name, id(self)) def __getnewargs__(self): diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 5b646fabca0225b..dc4fca3c5645d31 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -525,7 +525,7 @@ def enter_context(self, cm): _enter = cls.__enter__ _exit = cls.__exit__ except AttributeError: - raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " + raise TypeError(f"'{cls.__fullyqualname__}' object does " f"not support the context manager protocol") from None result = _enter(cm) self._push_cm_exit(cm, _exit) @@ -662,7 +662,7 @@ async def enter_async_context(self, cm): _enter = cls.__aenter__ _exit = cls.__aexit__ except AttributeError: - raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " + raise TypeError(f"'{cls.__fullyqualname__}' object does " f"not support the asynchronous context manager protocol" ) from None result = await _enter(cm) diff --git a/Lib/doctest.py b/Lib/doctest.py index 2f14aa083348958..6f0b5564aba5e9a 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1401,7 +1401,7 @@ def __run(self, test, compileflags, out): # They start with `SyntaxError:` (or any other class name) exception_line_prefixes = ( f"{exception[0].__qualname__}:", - f"{exception[0].__module__}.{exception[0].__qualname__}:", + f"{exception[0].__fullyqualname__}:", ) exc_msg_index = next( index diff --git a/Lib/inspect.py b/Lib/inspect.py index aaa22bef8966028..6121463a242f08b 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1501,9 +1501,9 @@ def repl(match): if isinstance(annotation, types.GenericAlias): return str(annotation) if isinstance(annotation, type): - if annotation.__module__ in ('builtins', base_module): + if annotation.__module__ == base_module: return annotation.__qualname__ - return annotation.__module__+'.'+annotation.__qualname__ + return annotation.__fullyqualname__ return repr(annotation) def formatannotationrelativeto(object): diff --git a/Lib/multiprocessing/pool.py b/Lib/multiprocessing/pool.py index f979890170b1a1f..1f7bff4ab226bb7 100644 --- a/Lib/multiprocessing/pool.py +++ b/Lib/multiprocessing/pool.py @@ -272,7 +272,7 @@ def __del__(self, _warn=warnings.warn, RUN=RUN): def __repr__(self): cls = self.__class__ - return (f'<{cls.__module__}.{cls.__qualname__} ' + return (f'<{cls.__fullyqualname__} ' f'state={self._state} ' f'pool_size={len(self._pool)}>') diff --git a/Lib/pdb.py b/Lib/pdb.py index ed78d749a47fa8e..b6c8f68a3b0b4c2 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1726,7 +1726,7 @@ def do_whatis(self, arg): return # Is it a class? if value.__class__ is type: - self.message('Class %s.%s' % (value.__module__, value.__qualname__)) + self.message(f'Class {value.__fullyqualname__}') return # None of the above... self.message(type(value)) diff --git a/Lib/test/support/asyncore.py b/Lib/test/support/asyncore.py index b397aca5568079d..70c387ca0f7fc6a 100644 --- a/Lib/test/support/asyncore.py +++ b/Lib/test/support/asyncore.py @@ -256,7 +256,7 @@ def __init__(self, sock=None, map=None): self.socket = None def __repr__(self): - status = [self.__class__.__module__+"."+self.__class__.__qualname__] + status = [self.__class__.__fullyqualname__] if self.accepting and self.addr: status.append('listening') elif self.connected: diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index 2d7dfbde7082ee3..e118a168c34a76f 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -587,8 +587,7 @@ def get_error(self, cf, exc, section, option): except exc as e: return e else: - self.fail("expected exception type %s.%s" - % (exc.__module__, exc.__qualname__)) + self.fail(f"expected exception type {exc.__fullyqualname__}") def test_boolean(self): cf = self.fromstring( diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index b43dca6f640b9ae..2f62b0d268c9a78 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -209,10 +209,10 @@ def __str__(self): err = traceback.format_exception_only(X, X()) self.assertEqual(len(err), 1) str_value = '' - if X.__module__ in ('__main__', 'builtins'): + if X.__module__ == '__main__': str_name = X.__qualname__ else: - str_name = '.'.join([X.__module__, X.__qualname__]) + str_name = X.__fullyqualname__ self.assertEqual(err[0], "%s: %s\n" % (str_name, str_value)) def test_format_exception_group_without_show_group(self): @@ -1875,7 +1875,7 @@ def __str__(self): err = self.get_report(A.B.X()) str_value = 'I am X' - str_name = '.'.join([A.B.X.__module__, A.B.X.__qualname__]) + str_name = A.B.X.__fullyqualname__ exp = "%s: %s\n" % (str_name, str_value) self.assertEqual(exp, MODULE_PREFIX + err) @@ -1889,10 +1889,10 @@ def __str__(self): with self.subTest(modulename=modulename): err = self.get_report(X()) str_value = 'I am X' - if modulename in ['builtins', '__main__']: + if modulename == '__main__': str_name = X.__qualname__ else: - str_name = '.'.join([X.__module__, X.__qualname__]) + str_name = X.__fullyqualname__ exp = "%s: %s\n" % (str_name, str_value) self.assertEqual(exp, err) @@ -1928,7 +1928,7 @@ def __str__(self): 1/0 err = self.get_report(X()) str_value = '' - str_name = '.'.join([X.__module__, X.__qualname__]) + str_name = X.__fullyqualname__ self.assertEqual(MODULE_PREFIX + err, f"{str_name}: {str_value}\n") diff --git a/Lib/test/test_zipimport_support.py b/Lib/test/test_zipimport_support.py index 7bf50a33728e53a..679835766248036 100644 --- a/Lib/test/test_zipimport_support.py +++ b/Lib/test/test_zipimport_support.py @@ -39,7 +39,7 @@ def _run_object_doctest(obj, module): # Use the object's fully qualified name if it has one # Otherwise, use the module's name try: - name = "%s.%s" % (obj.__module__, obj.__qualname__) + name = obj.__fullyqualname__ except AttributeError: name = module.__name__ for example in finder.find(obj, name, module): diff --git a/Lib/threading.py b/Lib/threading.py index 85aff58968082d8..e8c830376773ec4 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -457,7 +457,7 @@ def __init__(self, value=1): def __repr__(self): cls = self.__class__ - return (f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}:" + return (f"<{cls.__fullyqualname__} at {id(self):#x}:" f" value={self._value}>") def acquire(self, blocking=True, timeout=None): @@ -547,7 +547,7 @@ def __init__(self, value=1): def __repr__(self): cls = self.__class__ - return (f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}:" + return (f"<{cls.__fullyqualname__} at {id(self):#x}:" f" value={self._value}/{self._initial_value}>") def release(self, n=1): @@ -587,7 +587,7 @@ def __init__(self): def __repr__(self): cls = self.__class__ status = 'set' if self._flag else 'unset' - return f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}: {status}>" + return f"<{cls.__fullyqualname__} at {id(self):#x}: {status}>" def _at_fork_reinit(self): # Private method called by Thread._after_fork() @@ -690,8 +690,8 @@ def __init__(self, parties, action=None, timeout=None): def __repr__(self): cls = self.__class__ if self.broken: - return f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}: broken>" - return (f"<{cls.__module__}.{cls.__qualname__} at {id(self):#x}:" + return f"<{cls.__fullyqualname__} at {id(self):#x}: broken>" + return (f"<{cls.__fullyqualname__} at {id(self):#x}:" f" waiters={self.n_waiting}/{self.parties}>") def wait(self, timeout=None): diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 0df7f9d889413c8..966bb6ad4d83f19 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -1802,8 +1802,7 @@ def __str__(self): return self._w def __repr__(self): - return '<%s.%s object %s>' % ( - self.__class__.__module__, self.__class__.__qualname__, self._w) + return f'<{self.__class__.__fullyqualname__} object {self._w}>' # Pack methods that apply to the master _noarg_ = ['_noarg_'] diff --git a/Lib/tkinter/font.py b/Lib/tkinter/font.py index 3e24e28ef58cde9..82abb9efd010cd9 100644 --- a/Lib/tkinter/font.py +++ b/Lib/tkinter/font.py @@ -101,8 +101,8 @@ def __str__(self): return self.name def __repr__(self): - return f"<{self.__class__.__module__}.{self.__class__.__qualname__}" \ - f" object {self.name!r}>" + return (f"<{self.__class__.__fullyqualname__}" + f" object {self.name!r}>") def __eq__(self, other): if not isinstance(other, Font): diff --git a/Lib/typing.py b/Lib/typing.py index 14845b36028ca12..7c6528579dadd73 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -236,9 +236,7 @@ def _type_repr(obj): # `_collections_abc._type_repr`, which does the same thing # and must be consistent with this one. if isinstance(obj, type): - if obj.__module__ == 'builtins': - return obj.__qualname__ - return f'{obj.__module__}.{obj.__qualname__}' + return obj.__fullyqualname__ if obj is ...: return '...' if isinstance(obj, types.FunctionType): @@ -1402,10 +1400,7 @@ def __init__(self, origin, nparams, *, inst=True, name=None): name = origin.__name__ super().__init__(origin, inst=inst, name=name) self._nparams = nparams - if origin.__module__ == 'builtins': - self.__doc__ = f'A generic version of {origin.__qualname__}.' - else: - self.__doc__ = f'A generic version of {origin.__module__}.{origin.__qualname__}.' + self.__doc__ = f'A generic version of {origin.__fullyqualname__}.' @_tp_cache def __getitem__(self, params): diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index 63ff6a5d1f8b611..5c98d1f9fd63955 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -74,7 +74,7 @@ async def enterAsyncContext(self, cm): enter = cls.__aenter__ exit = cls.__aexit__ except AttributeError: - raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " + raise TypeError(f"'{cls.__fullyqualname__}' object does " f"not support the asynchronous context manager protocol" ) from None result = await enter(cm) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 811557498bb30ed..763727a607020c5 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -111,7 +111,7 @@ def _enter_context(cm, addcleanup): enter = cls.__enter__ exit = cls.__exit__ except AttributeError: - raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " + raise TypeError(f"'{cls.__fullyqualname__}' object does " f"not support the context manager protocol") from None result = enter(cm) addcleanup(exit, cm, None, None, None) diff --git a/Lib/unittest/loader.py b/Lib/unittest/loader.py index 9a3e5cc4bf30e57..4874710e54e4753 100644 --- a/Lib/unittest/loader.py +++ b/Lib/unittest/loader.py @@ -216,9 +216,7 @@ def shouldIncludeMethod(attrname): testFunc = getattr(testCaseClass, attrname) if not callable(testFunc): return False - fullName = f'%s.%s.%s' % ( - testCaseClass.__module__, testCaseClass.__qualname__, attrname - ) + fullName = f'{testCaseClass.__fullyqualname__}.{attrname}' return self.testNamePatterns is None or \ any(fnmatchcase(fullName, pattern) for pattern in self.testNamePatterns) testFnNames = list(filter(shouldIncludeMethod, dir(testCaseClass))) diff --git a/Lib/unittest/util.py b/Lib/unittest/util.py index 050eaed0b3f58fb..47f79a9304c042b 100644 --- a/Lib/unittest/util.py +++ b/Lib/unittest/util.py @@ -52,7 +52,7 @@ def safe_repr(obj, short=False): return result[:_MAX_LENGTH] + ' [truncated]...' def strclass(cls): - return "%s.%s" % (cls.__module__, cls.__qualname__) + return cls.__fullyqualname__ def sorted_list_difference(expected, actual): """Finds elements in only one or the other of two, sorted input lists. From 76dcfd59efad2bd9a0ab5fbcd9b4092e08a893ab Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 16 Nov 2023 14:47:44 +0100 Subject: [PATCH 3/8] Rename the attribute to __fully_qualified_name__ --- Doc/c-api/type.rst | 2 +- Doc/library/stdtypes.rst | 2 +- Doc/whatsnew/3.13.rst | 4 ++-- Lib/_collections_abc.py | 2 +- Lib/_py_abc.py | 2 +- Lib/abc.py | 2 +- Lib/codecs.py | 2 +- Lib/contextlib.py | 4 ++-- Lib/doctest.py | 2 +- Lib/inspect.py | 2 +- Lib/multiprocessing/pool.py | 2 +- Lib/pdb.py | 2 +- Lib/test/support/asyncore.py | 2 +- Lib/test/test_builtin.py | 16 ++++++++-------- Lib/test/test_configparser.py | 2 +- Lib/test/test_traceback.py | 8 ++++---- Lib/test/test_zipimport_support.py | 2 +- Lib/threading.py | 10 +++++----- Lib/tkinter/__init__.py | 2 +- Lib/tkinter/font.py | 2 +- Lib/typing.py | 4 ++-- Lib/unittest/async_case.py | 2 +- Lib/unittest/case.py | 2 +- Lib/unittest/loader.py | 2 +- Lib/unittest/util.py | 2 +- ...023-11-15-23-43-23.gh-issue-111696.RP59HZ.rst | 4 ++-- ...023-11-15-23-43-53.gh-issue-111696.zwQTow.rst | 2 +- Modules/_testcapimodule.c | 2 +- Objects/typeobject.c | 2 +- 29 files changed, 47 insertions(+), 47 deletions(-) diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index bb97e18fbfec013..14b5b79829d5f63 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -188,7 +188,7 @@ Type Objects .. c:function:: PyObject* PyType_GetFullyQualifiedName(PyTypeObject *type) Return the type's fully qualified name. Equivalent to getting the - type's :attr:`__fullyqualname__ ` attribute. + type's :attr:`__fully_qualified_name__ ` attribute. .. versionadded:: 3.13 diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 212c46b80c12f2f..af673ca1ef3901a 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5496,7 +5496,7 @@ types, where they are relevant. Some of these are not reported by the .. versionadded:: 3.3 -.. attribute:: class.__fullyqualname__ +.. attribute:: class.__fully_qualified_name__ The fully qualified name of the class instance: ``f"{class.__module__}.{class.__qualname__}"``, or ``class.__qualname__`` if diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 5ff1f2302cd3692..3481e594c0955fb 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -125,7 +125,7 @@ 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 +* Add :attr:`__fully_qualified_name__ ` read-only attribute to types: the fully qualified type name. (Contributed by Victor Stinner in :gh:`111696`.) @@ -1188,7 +1188,7 @@ New Features * Add :c:func:`PyType_GetFullyQualifiedName` function: get the type's fully qualified name. It is equivalent to getting the type's - :attr:`__fullyqualname__ ` attribute. + :attr:`__fully_qualified_name__ ` attribute. (Contributed by Victor Stinner in :gh:`111696`.) diff --git a/Lib/_collections_abc.py b/Lib/_collections_abc.py index 8af47dc9aeb9fe4..f3ba8e8338eb83b 100644 --- a/Lib/_collections_abc.py +++ b/Lib/_collections_abc.py @@ -528,7 +528,7 @@ def _type_repr(obj): (Keep this roughly in sync with the typing version.) """ if isinstance(obj, type): - return obj.__fullyqualname__ + return obj.__fully_qualified_name__ if obj is Ellipsis: return '...' if isinstance(obj, FunctionType): diff --git a/Lib/_py_abc.py b/Lib/_py_abc.py index 8698742d65d3d17..ee760fcd834f099 100644 --- a/Lib/_py_abc.py +++ b/Lib/_py_abc.py @@ -71,7 +71,7 @@ def register(cls, subclass): def _dump_registry(cls, file=None): """Debug helper to print the ABC registry.""" - print(f"Class: {cls.__fullyqualname__}", file=file) + print(f"Class: {cls.__fully_qualified_name__}", file=file) print(f"Inv. counter: {get_cache_token()}", file=file) for name in cls.__dict__: if name.startswith("_abc_"): diff --git a/Lib/abc.py b/Lib/abc.py index 9646272cc3b6c73..9e6cf72041f5896 100644 --- a/Lib/abc.py +++ b/Lib/abc.py @@ -124,7 +124,7 @@ def __subclasscheck__(cls, subclass): def _dump_registry(cls, file=None): """Debug helper to print the ABC registry.""" - print(f"Class: {cls.__fullyqualname__}", file=file) + print(f"Class: {cls.__fully_qualified_name__}", file=file) print(f"Inv. counter: {get_cache_token()}", file=file) (_abc_registry, _abc_cache, _abc_negative_cache, _abc_negative_cache_version) = _get_dump(cls) diff --git a/Lib/codecs.py b/Lib/codecs.py index d77d6ff3039ba79..f4b3a6a5149f8e4 100644 --- a/Lib/codecs.py +++ b/Lib/codecs.py @@ -108,7 +108,7 @@ def __new__(cls, encode, decode, streamreader=None, streamwriter=None, def __repr__(self): return "<%s object for encoding %s at %#x>" % \ - (self.__class__.__fullyqualname__, + (self.__class__.__fully_qualified_name__, self.name, id(self)) def __getnewargs__(self): diff --git a/Lib/contextlib.py b/Lib/contextlib.py index dc4fca3c5645d31..bce7cf24cd88a6a 100644 --- a/Lib/contextlib.py +++ b/Lib/contextlib.py @@ -525,7 +525,7 @@ def enter_context(self, cm): _enter = cls.__enter__ _exit = cls.__exit__ except AttributeError: - raise TypeError(f"'{cls.__fullyqualname__}' object does " + raise TypeError(f"'{cls.__fully_qualified_name__}' object does " f"not support the context manager protocol") from None result = _enter(cm) self._push_cm_exit(cm, _exit) @@ -662,7 +662,7 @@ async def enter_async_context(self, cm): _enter = cls.__aenter__ _exit = cls.__aexit__ except AttributeError: - raise TypeError(f"'{cls.__fullyqualname__}' object does " + raise TypeError(f"'{cls.__fully_qualified_name__}' object does " f"not support the asynchronous context manager protocol" ) from None result = await _enter(cm) diff --git a/Lib/doctest.py b/Lib/doctest.py index 6f0b5564aba5e9a..867005bdea94c53 100644 --- a/Lib/doctest.py +++ b/Lib/doctest.py @@ -1401,7 +1401,7 @@ def __run(self, test, compileflags, out): # They start with `SyntaxError:` (or any other class name) exception_line_prefixes = ( f"{exception[0].__qualname__}:", - f"{exception[0].__fullyqualname__}:", + f"{exception[0].__fully_qualified_name__}:", ) exc_msg_index = next( index diff --git a/Lib/inspect.py b/Lib/inspect.py index 6121463a242f08b..6dc79ff274ff7a2 100644 --- a/Lib/inspect.py +++ b/Lib/inspect.py @@ -1503,7 +1503,7 @@ def repl(match): if isinstance(annotation, type): if annotation.__module__ == base_module: return annotation.__qualname__ - return annotation.__fullyqualname__ + return annotation.__fully_qualified_name__ return repr(annotation) def formatannotationrelativeto(object): diff --git a/Lib/multiprocessing/pool.py b/Lib/multiprocessing/pool.py index 1f7bff4ab226bb7..96fc38c1924e704 100644 --- a/Lib/multiprocessing/pool.py +++ b/Lib/multiprocessing/pool.py @@ -272,7 +272,7 @@ def __del__(self, _warn=warnings.warn, RUN=RUN): def __repr__(self): cls = self.__class__ - return (f'<{cls.__fullyqualname__} ' + return (f'<{cls.__fully_qualified_name__} ' f'state={self._state} ' f'pool_size={len(self._pool)}>') diff --git a/Lib/pdb.py b/Lib/pdb.py index b6c8f68a3b0b4c2..abb270a345eb5b6 100755 --- a/Lib/pdb.py +++ b/Lib/pdb.py @@ -1726,7 +1726,7 @@ def do_whatis(self, arg): return # Is it a class? if value.__class__ is type: - self.message(f'Class {value.__fullyqualname__}') + self.message(f'Class {value.__fully_qualified_name__}') return # None of the above... self.message(type(value)) diff --git a/Lib/test/support/asyncore.py b/Lib/test/support/asyncore.py index 70c387ca0f7fc6a..c90f553cfc9d628 100644 --- a/Lib/test/support/asyncore.py +++ b/Lib/test/support/asyncore.py @@ -256,7 +256,7 @@ def __init__(self, sock=None, map=None): self.socket = None def __repr__(self): - status = [self.__class__.__fullyqualname__] + status = [self.__class__.__fully_qualified_name__] if self.accepting and self.addr: status.append('listening') elif self.connected: diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 13d25abf217efb6..d6378fd4488bc6e 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -2430,7 +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.__fully_qualified_name__, f'{__name__}.A') self.assertEqual(A.__bases__, (object,)) self.assertIs(A.__base__, object) x = A() @@ -2444,7 +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.__fully_qualified_name__, f'{__name__}.C') self.assertEqual(C.__bases__, (B, int)) self.assertIs(C.__base__, int) self.assertIn('spam', C.__dict__) @@ -2470,7 +2470,7 @@ def test_type_name(self): self.assertEqual(A.__name__, name) self.assertEqual(A.__qualname__, f"Test.{name}") self.assertEqual(A.__module__, __name__) - self.assertEqual(A.__fullyqualname__, f'{__name__}.Test.{name}') + self.assertEqual(A.__fully_qualified_name__, f'{__name__}.Test.{name}') with self.assertRaises(ValueError): type('A\x00B', (), {}) with self.assertRaises(UnicodeEncodeError): @@ -2485,7 +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') + self.assertEqual(C.__fully_qualified_name__, f'{__name__}.C') A = type('C', (), {}) with self.assertRaises(ValueError): @@ -2501,16 +2501,16 @@ def test_type_name(self): # if __module__ is not a string, ignore it silently class D: pass - self.assertEqual(D.__fullyqualname__, f'{__name__}.{D.__qualname__}') + self.assertEqual(D.__fully_qualified_name__, f'{__name__}.{D.__qualname__}') D.__module__ = 123 - self.assertEqual(D.__fullyqualname__, D.__qualname__) + self.assertEqual(D.__fully_qualified_name__, 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') + self.assertEqual(A.__fully_qualified_name__, f'{__name__}.B.C') with self.assertRaises(TypeError): type('A', (), {'__qualname__': b'B'}) self.assertEqual(A.__qualname__, 'B.C') @@ -2518,7 +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') + self.assertEqual(A.__fully_qualified_name__, f'{__name__}.D.E') with self.assertRaises(TypeError): A.__qualname__ = b'B' self.assertEqual(A.__qualname__, 'D.E') diff --git a/Lib/test/test_configparser.py b/Lib/test/test_configparser.py index e118a168c34a76f..c2045fa57771c61 100644 --- a/Lib/test/test_configparser.py +++ b/Lib/test/test_configparser.py @@ -587,7 +587,7 @@ def get_error(self, cf, exc, section, option): except exc as e: return e else: - self.fail(f"expected exception type {exc.__fullyqualname__}") + self.fail(f"expected exception type {exc.__fully_qualified_name__}") def test_boolean(self): cf = self.fromstring( diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 2f62b0d268c9a78..a547ad416ea8c0f 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -212,7 +212,7 @@ def __str__(self): if X.__module__ == '__main__': str_name = X.__qualname__ else: - str_name = X.__fullyqualname__ + str_name = X.__fully_qualified_name__ self.assertEqual(err[0], "%s: %s\n" % (str_name, str_value)) def test_format_exception_group_without_show_group(self): @@ -1875,7 +1875,7 @@ def __str__(self): err = self.get_report(A.B.X()) str_value = 'I am X' - str_name = A.B.X.__fullyqualname__ + str_name = A.B.X.__fully_qualified_name__ exp = "%s: %s\n" % (str_name, str_value) self.assertEqual(exp, MODULE_PREFIX + err) @@ -1892,7 +1892,7 @@ def __str__(self): if modulename == '__main__': str_name = X.__qualname__ else: - str_name = X.__fullyqualname__ + str_name = X.__fully_qualified_name__ exp = "%s: %s\n" % (str_name, str_value) self.assertEqual(exp, err) @@ -1928,7 +1928,7 @@ def __str__(self): 1/0 err = self.get_report(X()) str_value = '' - str_name = X.__fullyqualname__ + str_name = X.__fully_qualified_name__ self.assertEqual(MODULE_PREFIX + err, f"{str_name}: {str_value}\n") diff --git a/Lib/test/test_zipimport_support.py b/Lib/test/test_zipimport_support.py index 679835766248036..5383df673bc29ea 100644 --- a/Lib/test/test_zipimport_support.py +++ b/Lib/test/test_zipimport_support.py @@ -39,7 +39,7 @@ def _run_object_doctest(obj, module): # Use the object's fully qualified name if it has one # Otherwise, use the module's name try: - name = obj.__fullyqualname__ + name = obj.__fully_qualified_name__ except AttributeError: name = module.__name__ for example in finder.find(obj, name, module): diff --git a/Lib/threading.py b/Lib/threading.py index e8c830376773ec4..374bca2fe10c783 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -457,7 +457,7 @@ def __init__(self, value=1): def __repr__(self): cls = self.__class__ - return (f"<{cls.__fullyqualname__} at {id(self):#x}:" + return (f"<{cls.__fully_qualified_name__} at {id(self):#x}:" f" value={self._value}>") def acquire(self, blocking=True, timeout=None): @@ -547,7 +547,7 @@ def __init__(self, value=1): def __repr__(self): cls = self.__class__ - return (f"<{cls.__fullyqualname__} at {id(self):#x}:" + return (f"<{cls.__fully_qualified_name__} at {id(self):#x}:" f" value={self._value}/{self._initial_value}>") def release(self, n=1): @@ -587,7 +587,7 @@ def __init__(self): def __repr__(self): cls = self.__class__ status = 'set' if self._flag else 'unset' - return f"<{cls.__fullyqualname__} at {id(self):#x}: {status}>" + return f"<{cls.__fully_qualified_name__} at {id(self):#x}: {status}>" def _at_fork_reinit(self): # Private method called by Thread._after_fork() @@ -690,8 +690,8 @@ def __init__(self, parties, action=None, timeout=None): def __repr__(self): cls = self.__class__ if self.broken: - return f"<{cls.__fullyqualname__} at {id(self):#x}: broken>" - return (f"<{cls.__fullyqualname__} at {id(self):#x}:" + return f"<{cls.__fully_qualified_name__} at {id(self):#x}: broken>" + return (f"<{cls.__fully_qualified_name__} at {id(self):#x}:" f" waiters={self.n_waiting}/{self.parties}>") def wait(self, timeout=None): diff --git a/Lib/tkinter/__init__.py b/Lib/tkinter/__init__.py index 966bb6ad4d83f19..8804a909ca39cea 100644 --- a/Lib/tkinter/__init__.py +++ b/Lib/tkinter/__init__.py @@ -1802,7 +1802,7 @@ def __str__(self): return self._w def __repr__(self): - return f'<{self.__class__.__fullyqualname__} object {self._w}>' + return f'<{self.__class__.__fully_qualified_name__} object {self._w}>' # Pack methods that apply to the master _noarg_ = ['_noarg_'] diff --git a/Lib/tkinter/font.py b/Lib/tkinter/font.py index 82abb9efd010cd9..8f86ad11349a0cf 100644 --- a/Lib/tkinter/font.py +++ b/Lib/tkinter/font.py @@ -101,7 +101,7 @@ def __str__(self): return self.name def __repr__(self): - return (f"<{self.__class__.__fullyqualname__}" + return (f"<{self.__class__.__fully_qualified_name__}" f" object {self.name!r}>") def __eq__(self, other): diff --git a/Lib/typing.py b/Lib/typing.py index 7c6528579dadd73..9f5bbbcb3bbe682 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -236,7 +236,7 @@ def _type_repr(obj): # `_collections_abc._type_repr`, which does the same thing # and must be consistent with this one. if isinstance(obj, type): - return obj.__fullyqualname__ + return obj.__fully_qualified_name__ if obj is ...: return '...' if isinstance(obj, types.FunctionType): @@ -1400,7 +1400,7 @@ def __init__(self, origin, nparams, *, inst=True, name=None): name = origin.__name__ super().__init__(origin, inst=inst, name=name) self._nparams = nparams - self.__doc__ = f'A generic version of {origin.__fullyqualname__}.' + self.__doc__ = f'A generic version of {origin.__fully_qualified_name__}.' @_tp_cache def __getitem__(self, params): diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index 5c98d1f9fd63955..b02b06e58b1165d 100644 --- a/Lib/unittest/async_case.py +++ b/Lib/unittest/async_case.py @@ -74,7 +74,7 @@ async def enterAsyncContext(self, cm): enter = cls.__aenter__ exit = cls.__aexit__ except AttributeError: - raise TypeError(f"'{cls.__fullyqualname__}' object does " + raise TypeError(f"'{cls.__fully_qualified_name__}' object does " f"not support the asynchronous context manager protocol" ) from None result = await enter(cm) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 763727a607020c5..995e8a26dcb1cc8 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -111,7 +111,7 @@ def _enter_context(cm, addcleanup): enter = cls.__enter__ exit = cls.__exit__ except AttributeError: - raise TypeError(f"'{cls.__fullyqualname__}' object does " + raise TypeError(f"'{cls.__fully_qualified_name__}' object does " f"not support the context manager protocol") from None result = enter(cm) addcleanup(exit, cm, None, None, None) diff --git a/Lib/unittest/loader.py b/Lib/unittest/loader.py index 4874710e54e4753..c22bceac4b07fcc 100644 --- a/Lib/unittest/loader.py +++ b/Lib/unittest/loader.py @@ -216,7 +216,7 @@ def shouldIncludeMethod(attrname): testFunc = getattr(testCaseClass, attrname) if not callable(testFunc): return False - fullName = f'{testCaseClass.__fullyqualname__}.{attrname}' + fullName = f'{testCaseClass.__fully_qualified_name__}.{attrname}' return self.testNamePatterns is None or \ any(fnmatchcase(fullName, pattern) for pattern in self.testNamePatterns) testFnNames = list(filter(shouldIncludeMethod, dir(testCaseClass))) diff --git a/Lib/unittest/util.py b/Lib/unittest/util.py index 47f79a9304c042b..c65e02dd965fdbd 100644 --- a/Lib/unittest/util.py +++ b/Lib/unittest/util.py @@ -52,7 +52,7 @@ def safe_repr(obj, short=False): return result[:_MAX_LENGTH] + ' [truncated]...' def strclass(cls): - return cls.__fullyqualname__ + return cls.__fully_qualified_name__ def sorted_list_difference(expected, actual): """Finds elements in only one or the other of two, sorted input lists. diff --git a/Misc/NEWS.d/next/C API/2023-11-15-23-43-23.gh-issue-111696.RP59HZ.rst b/Misc/NEWS.d/next/C API/2023-11-15-23-43-23.gh-issue-111696.RP59HZ.rst index 9e6b0fac07ba1e6..e53c2798afe0742 100644 --- a/Misc/NEWS.d/next/C API/2023-11-15-23-43-23.gh-issue-111696.RP59HZ.rst +++ b/Misc/NEWS.d/next/C API/2023-11-15-23-43-23.gh-issue-111696.RP59HZ.rst @@ -1,3 +1,3 @@ Add :c:func:`PyType_GetFullyQualifiedName` function: get the type's fully -qualified name. It is equivalent to getting the type's :attr:`__fullyqualname__ -` attribute. Patch by Victor Stinner. +qualified name. It is equivalent to getting the type's :attr:`__fully_qualified_name__ +` attribute. Patch by Victor Stinner. diff --git a/Misc/NEWS.d/next/Core and Builtins/2023-11-15-23-43-53.gh-issue-111696.zwQTow.rst b/Misc/NEWS.d/next/Core and Builtins/2023-11-15-23-43-53.gh-issue-111696.zwQTow.rst index c0119adf092757e..bddc7f9461f1fee 100644 --- a/Misc/NEWS.d/next/Core and Builtins/2023-11-15-23-43-53.gh-issue-111696.zwQTow.rst +++ b/Misc/NEWS.d/next/Core and Builtins/2023-11-15-23-43-53.gh-issue-111696.zwQTow.rst @@ -1,2 +1,2 @@ -Add :attr:`__fullyqualname__ ` read-only attribute +Add :attr:`__fully_qualified_name__ ` read-only attribute to types: the fully qualified type name. Patch by Victor Stinner. diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 000cd8cd1bedb7c..90fb40df8a0976a 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -670,7 +670,7 @@ test_get_type_fullyqualname(PyObject *self, PyObject *Py_UNUSED(ignored)) } int res = PyObject_SetAttrString(HeapTypeNameType, - "__fullyqualname__", new_name); + "__fully_qualified_name__", new_name); Py_DECREF(new_name); assert(res < 0); assert(PyErr_ExceptionMatches(PyExc_AttributeError)); diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 3bd361f5e8cdfb0..05b01368e4b0bef 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1635,7 +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}, + {"__fully_qualified_name__", (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}, From 36cf2a9d0f3b2325fb7cf8f19d831a469dda32ae Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 17 Nov 2023 22:35:26 +0100 Subject: [PATCH 4/8] Doc: Add "fully qualified name" term to the glossary --- Doc/c-api/type.rst | 5 +++-- Doc/glossary.rst | 19 +++++++++++++++++++ Doc/library/doctest.rst | 6 +++--- Doc/library/email.contentmanager.rst | 4 ++-- Doc/library/importlib.rst | 12 ++++++------ Doc/library/inspect.rst | 2 +- Doc/library/logging.config.rst | 2 +- Doc/library/pickle.rst | 2 +- Doc/library/stdtypes.rst | 2 +- Doc/library/unittest.rst | 4 ++-- Doc/library/warnings.rst | 3 ++- Doc/reference/datamodel.rst | 2 +- Doc/reference/import.rst | 10 +++++----- Doc/using/cmdline.rst | 4 ++-- Doc/whatsnew/3.13.rst | 8 ++++---- 15 files changed, 53 insertions(+), 32 deletions(-) diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 14b5b79829d5f63..685aa30a37bc2e1 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -187,8 +187,9 @@ Type Objects .. c:function:: PyObject* PyType_GetFullyQualifiedName(PyTypeObject *type) - Return the type's fully qualified name. Equivalent to getting the - type's :attr:`__fully_qualified_name__ ` attribute. + Return the type's :term:`fully qualified name`. Equivalent to getting the + type's :attr:`__fully_qualified_name__ ` + attribute. .. versionadded:: 3.13 diff --git a/Doc/glossary.rst b/Doc/glossary.rst index dad745348f9b4b8..99fce8578d175a8 100644 --- a/Doc/glossary.rst +++ b/Doc/glossary.rst @@ -435,6 +435,25 @@ Glossary division. Note that ``(-11) // 4`` is ``-3`` because that is ``-2.75`` rounded *downward*. See :pep:`238`. + fully qualified name + The fully qualified name is the entire dotted path to a class or a + module. + + The :attr:`class.__fully_qualified_name__` attribute includes the module + name, except for built-in classes. Example:: + + >>> import collections + >>> collections.OrderedDict.__fully_qualified_name__ + 'collections.OrderedDict' + + When used to refer to modules, the *fully qualified name* means the + entire dotted path to the module, including any parent packages, + e.g. ``email.mime.text``:: + + >>> import email.mime.text + >>> email.mime.text.__name__ + 'email.mime.text' + function A series of statements which returns some value to a caller. It can also be passed zero or more :term:`arguments ` which may be used in diff --git a/Doc/library/doctest.rst b/Doc/library/doctest.rst index ad013944ce3ca36..1c2a87023e46514 100644 --- a/Doc/library/doctest.rst +++ b/Doc/library/doctest.rst @@ -587,13 +587,13 @@ doctest decides whether actual output matches an example's expected output: .. data:: IGNORE_EXCEPTION_DETAIL When specified, doctests expecting exceptions pass so long as an exception - of the expected type is raised, even if the details - (message and fully qualified exception name) don't match. + of the expected type is raised, even if the details (message and + :term:`fully qualified exception name `) don't match. For example, an example expecting ``ValueError: 42`` will pass if the actual exception raised is ``ValueError: 3*14``, but will fail if, say, a :exc:`TypeError` is raised instead. - It will also ignore any fully qualified name included before the + It will also ignore any :term:`fully qualified name` included before the exception class, which can vary between implementations and versions of Python and the code/libraries in use. Hence, all three of these variations will work with the flag specified: diff --git a/Doc/library/email.contentmanager.rst b/Doc/library/email.contentmanager.rst index 5b49339650f0e92..866087ad0aef0c7 100644 --- a/Doc/library/email.contentmanager.rst +++ b/Doc/library/email.contentmanager.rst @@ -56,8 +56,8 @@ found: * the type itself (``typ``) - * the type's fully qualified name (``typ.__module__ + '.' + - typ.__qualname__``). + * the type's :term:`fully qualified name` + (:attr:`typ.__fully_qualified_name__ `). * the type's qualname (``typ.__qualname__``) * the type's name (``typ.__name__``). diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst index fc954724bb72fea..07ad90d9f94c500 100644 --- a/Doc/library/importlib.rst +++ b/Doc/library/importlib.rst @@ -356,7 +356,7 @@ ABC hierarchy:: reloaded): - :attr:`__name__` - The module's fully qualified name. + The module's :term:`fully qualified name`. It is ``'__main__'`` for an executed module. - :attr:`__file__` @@ -377,8 +377,8 @@ ABC hierarchy:: as an indicator that the module is a package. - :attr:`__package__` - The fully qualified name of the package the module is in (or the - empty string for a top-level module). + The :term:`fully qualified name` of the package the module is in + (or the empty string for a top-level module). If the module is a package then this is the same as :attr:`__name__`. - :attr:`__loader__` @@ -1181,7 +1181,7 @@ find and load modules. (:attr:`__name__`) - The module's fully qualified name. + The module's :term:`fully qualified name`. The :term:`finder` should always set this attribute to a non-empty string. .. attribute:: loader @@ -1230,8 +1230,8 @@ find and load modules. (:attr:`__package__`) - (Read-only) The fully qualified name of the package the module is in (or the - empty string for a top-level module). + (Read-only) The :term:`fully qualified name` of the package the module is in + (or the empty string for a top-level module). If the module is a package then this is the same as :attr:`name`. .. attribute:: has_location diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst index b463c0b6d0e4020..6cce68e8ec51b96 100644 --- a/Doc/library/inspect.rst +++ b/Doc/library/inspect.rst @@ -182,7 +182,7 @@ attributes (see :ref:`import-mod-attrs` for module attributes): | | co_name | name with which this code | | | | object was defined | +-----------+-------------------+---------------------------+ -| | co_qualname | fully qualified name with | +| | co_qualname | qualified name with | | | | which this code object | | | | was defined | +-----------+-------------------+---------------------------+ diff --git a/Doc/library/logging.config.rst b/Doc/library/logging.config.rst index 85a53e6aa7a78b0..a7012e3397060f1 100644 --- a/Doc/library/logging.config.rst +++ b/Doc/library/logging.config.rst @@ -286,7 +286,7 @@ otherwise, the context is used to determine what to instantiate. The configuring dict is searched for the following keys: - * ``class`` (mandatory). This is the fully qualified name of the + * ``class`` (mandatory). This is the :term:`fully qualified name` of the handler class. * ``level`` (optional). The level of the handler. diff --git a/Doc/library/pickle.rst b/Doc/library/pickle.rst index 93387fb0b450382..c4e9270351c51ef 100644 --- a/Doc/library/pickle.rst +++ b/Doc/library/pickle.rst @@ -525,7 +525,7 @@ the function's code, nor any of its function attributes are pickled. Thus the defining module must be importable in the unpickling environment, and the module must contain the named object, otherwise an exception will be raised. [#]_ -Similarly, classes are pickled by fully qualified name, so the same restrictions in +Similarly, classes are pickled by :term:`fully qualified name`, so the same restrictions in the unpickling environment apply. Note that none of the class's code or data is pickled, so in the following example the class attribute ``attr`` is not restored in the unpickling environment:: diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index af673ca1ef3901a..d0a56f6f21de776 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5498,7 +5498,7 @@ types, where they are relevant. Some of these are not reported by the .. attribute:: class.__fully_qualified_name__ - The fully qualified name of the class instance: + The :term:`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"``. diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index c90c554591e7484..4d87e0b4e9e6c05 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -234,8 +234,8 @@ Command-line options test name using :meth:`fnmatch.fnmatchcase`; otherwise simple case-sensitive substring matching is used. - Patterns are matched against the fully qualified test method name as - imported by the test loader. + Patterns are matched against the :term:`fully qualified test method name + `_ as imported by the test loader. For example, ``-k foo`` matches ``foo_tests.SomeTest.test_something``, ``bar_tests.SomeTest.test_foo``, but not ``bar_tests.FooTest.test_something``. diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index 884de08eab1b16a..cc1d55293e3df15 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -163,7 +163,8 @@ the disposition of the match. Each entry is a tuple of the form (*action*, category must be a subclass in order to match. * *module* is a string containing a regular expression that the start of the - fully qualified module name must match, case-sensitively. In :option:`-W` and + :term:`fully qualified module name ` must match, + case-sensitively. In :option:`-W` and :envvar:`PYTHONWARNINGS`, *module* is a literal string that the fully qualified module name must be equal to (case-sensitively), ignoring any whitespace at the start or end of *module*. diff --git a/Doc/reference/datamodel.rst b/Doc/reference/datamodel.rst index f7d3d2d0bbec23a..0f174196524a887 100644 --- a/Doc/reference/datamodel.rst +++ b/Doc/reference/datamodel.rst @@ -1076,7 +1076,7 @@ indirectly) to mutable objects. single: co_qualname (code object attribute) Special read-only attributes: :attr:`co_name` gives the function name; -:attr:`co_qualname` gives the fully qualified function name; +:attr:`co_qualname` gives the qualified function name; :attr:`co_argcount` is the total number of positional arguments (including positional-only arguments and arguments with default values); :attr:`co_posonlyargcount` is the number of positional-only arguments diff --git a/Doc/reference/import.rst b/Doc/reference/import.rst index a7beeea29b45567..cd4e11532e381b6 100644 --- a/Doc/reference/import.rst +++ b/Doc/reference/import.rst @@ -157,8 +157,8 @@ See also :pep:`420` for the namespace package specification. Searching ========= -To begin the search, Python needs the :term:`fully qualified ` -name of the module (or package, but for the purposes of this discussion, the +To begin the search, Python needs the :term:`fully qualified name` +of the module (or package, but for the purposes of this discussion, the difference is immaterial) being imported. This name may come from various arguments to the :keyword:`import` statement, or from the parameters to the :func:`importlib.import_module` or :func:`__import__` functions. @@ -547,7 +547,7 @@ listed below. .. attribute:: __name__ - The ``__name__`` attribute must be set to the fully qualified name of + The ``__name__`` attribute must be set to the :term:`fully qualified name` of the module. This name is used to uniquely identify the module in the import system. @@ -885,7 +885,7 @@ contribute portions to namespace packages, path entry finders must implement the :meth:`~importlib.abc.PathEntryFinder.find_spec` method. :meth:`~importlib.abc.PathEntryFinder.find_spec` takes two arguments: the -fully qualified name of the module being imported, and the (optional) target +:term:`fully qualified name` of the module being imported, and the (optional) target module. ``find_spec()`` returns a fully populated spec for the module. This spec will always have "loader" set (with one exception). @@ -905,7 +905,7 @@ a list containing the portion. implemented on the path entry finder, the legacy methods are ignored. :meth:`!find_loader` takes one argument, the - fully qualified name of the module being imported. ``find_loader()`` + :term:`fully qualified name` of the module being imported. ``find_loader()`` returns a 2-tuple where the first item is the loader and the second item is a namespace :term:`portion`. diff --git a/Doc/using/cmdline.rst b/Doc/using/cmdline.rst index 39c8d114f1e2c5e..ad6cad04012a770 100644 --- a/Doc/using/cmdline.rst +++ b/Doc/using/cmdline.rst @@ -464,8 +464,8 @@ Miscellaneous options whether the actual warning category of the message is a subclass of the specified warning category. - The *module* field matches the (fully qualified) module name; this match is - case-sensitive. + The *module* field matches the :term:`fully qualified module name `; this match is case-sensitive. The *lineno* field matches the line number, where zero matches all line numbers and is thus equivalent to an omitted line number. diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 3481e594c0955fb..9380b87878ad382 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -125,8 +125,8 @@ Other Language Changes equivalent of the :option:`-X frozen_modules <-X>` command-line option. (Contributed by Yilei Yang in :gh:`111374`.) -* Add :attr:`__fully_qualified_name__ ` read-only attribute - to types: the fully qualified type name. +* Add the :attr:`__fully_qualified_name__ ` + read-only attribute to types: the :term:`fully qualified name` of the type. (Contributed by Victor Stinner in :gh:`111696`.) @@ -1186,8 +1186,8 @@ New Features :exc:`KeyError` if the key missing. (Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.) -* Add :c:func:`PyType_GetFullyQualifiedName` function: get the type's fully - qualified name. It is equivalent to getting the type's +* Add :c:func:`PyType_GetFullyQualifiedName` function: get the type's + :term:`fully qualified name`. It is equivalent to getting the type's :attr:`__fully_qualified_name__ ` attribute. (Contributed by Victor Stinner in :gh:`111696`.) From 266a8e521e86e2562d71c88cf3949ebc3c15db93 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 17 Nov 2023 22:44:10 +0100 Subject: [PATCH 5/8] Add tests on more types: built-in and types --- Lib/test/test_builtin.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index d6378fd4488bc6e..a7ba6e45fbd2d73 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -2505,6 +2505,28 @@ class D: D.__module__ = 123 self.assertEqual(D.__fully_qualified_name__, D.__qualname__) + # built-in type + self.assertEqual(str.__name__, 'str') + self.assertEqual(str.__qualname__, 'str') + self.assertEqual(str.__module__, 'builtins') + self.assertEqual(str.__fully_qualified_name__, 'str') + + def func(): + return 3 + CodeType = type(func.__code__) + self.assertEqual(CodeType.__name__, 'code') + self.assertEqual(CodeType.__qualname__, 'code') + self.assertEqual(CodeType.__module__, 'builtins') + self.assertEqual(CodeType.__fully_qualified_name__, 'code') + + # fully qualified name which contains the module name + SimpleNamespace = types.SimpleNamespace + self.assertEqual(SimpleNamespace.__name__, 'SimpleNamespace') + self.assertEqual(SimpleNamespace.__qualname__, 'SimpleNamespace') + self.assertEqual(SimpleNamespace.__module__, 'types') + self.assertEqual(SimpleNamespace.__fully_qualified_name__, + 'types.SimpleNamespace') + def test_type_qualname(self): A = type('A', (), {'__qualname__': 'B.C'}) self.assertEqual(A.__name__, 'A') From e63c93730d73f671064c1791958bd7c6c8a3a739 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 17 Nov 2023 22:45:47 +0100 Subject: [PATCH 6/8] Fix PyType_FromSpec() error handling --- Modules/_testcapimodule.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/_testcapimodule.c b/Modules/_testcapimodule.c index 90fb40df8a0976a..91b1d3ab8392e70 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -582,7 +582,7 @@ test_get_type_name(PyObject *self, PyObject *Py_UNUSED(ignored)) PyObject *HeapTypeNameType = PyType_FromSpec(&HeapTypeNameType_Spec); if (HeapTypeNameType == NULL) { - Py_RETURN_NONE; + return NULL; } tp_name = PyType_GetName((PyTypeObject *)HeapTypeNameType); assert(PyUnicode_EqualToUTF8(tp_name, "HeapTypeNameType")); @@ -620,7 +620,7 @@ test_get_type_qualname(PyObject *self, PyObject *Py_UNUSED(ignored)) PyObject *HeapTypeNameType = PyType_FromSpec(&HeapTypeNameType_Spec); if (HeapTypeNameType == NULL) { - Py_RETURN_NONE; + return NULL; } tp_qualname = PyType_GetQualName((PyTypeObject *)HeapTypeNameType); assert(PyUnicode_EqualToUTF8(tp_qualname, "HeapTypeNameType")); @@ -658,7 +658,7 @@ test_get_type_fullyqualname(PyObject *self, PyObject *Py_UNUSED(ignored)) PyObject *HeapTypeNameType = PyType_FromSpec(&HeapTypeNameType_Spec); if (HeapTypeNameType == NULL) { - Py_RETURN_NONE; + return NULL; } name = PyType_GetFullyQualifiedName((PyTypeObject *)HeapTypeNameType); assert(PyUnicode_EqualToUTF8(name, "_testcapi.HeapTypeNameType")); From c79c51980520df1cd3723497acdfea33a3ad3c43 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Fri, 17 Nov 2023 22:50:19 +0100 Subject: [PATCH 7/8] type.__fully_qualified_name__ getter calls PyType_GetFullyQualifiedName() --- Objects/typeobject.c | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 05b01368e4b0bef..2c9ef94d7ca81ec 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1125,7 +1125,7 @@ type_set_module(PyTypeObject *type, PyObject *value, void *context) static PyObject* -type_fullyqualname(PyTypeObject *type, int is_repr) +type_fullyqualname_impl(PyTypeObject *type, int is_repr) { // type is a static type and PyType_Ready() was not called on it yet? if (type->tp_name == NULL) { @@ -1169,9 +1169,9 @@ type_fullyqualname(PyTypeObject *type, int is_repr) } static PyObject * -type_get_fullyqualname(PyTypeObject *type, void *context) +type_fullyqualname(PyTypeObject *type, void *context) { - return type_fullyqualname(type, 0); + return PyType_GetFullyQualifiedName(type); } @@ -1635,7 +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}, - {"__fully_qualified_name__", (getter)type_get_fullyqualname, NULL, NULL}, + {"__fully_qualified_name__", (getter)type_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}, @@ -1658,7 +1658,7 @@ type_repr(PyTypeObject *type) return PyUnicode_FromFormat("", type); } - PyObject *name = type_fullyqualname(type, 1); + PyObject *name = type_fullyqualname_impl(type, 1); if (name == NULL) { return NULL; } @@ -4581,7 +4581,7 @@ PyType_GetQualName(PyTypeObject *type) PyObject * PyType_GetFullyQualifiedName(PyTypeObject *type) { - return type_get_fullyqualname(type, NULL); + return type_fullyqualname_impl(type, 0); } From 46fb6dfe0c0d7a7ff521481d2342ea142d848f1b Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 18 Nov 2023 02:22:38 +0100 Subject: [PATCH 8/8] Fix typo in the doc --- Doc/library/unittest.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 4d87e0b4e9e6c05..28d6c647c69a5cf 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -235,7 +235,7 @@ Command-line options substring matching is used. Patterns are matched against the :term:`fully qualified test method name - `_ as imported by the test loader. + ` as imported by the test loader. For example, ``-k foo`` matches ``foo_tests.SomeTest.test_something``, ``bar_tests.SomeTest.test_foo``, but not ``bar_tests.FooTest.test_something``.