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 5aaa8147dd3176..bb97e18fbfec01 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 f204b287b565eb..212c46b80c12f2 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 b64cfc51f75701..5ff1f2302cd369 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 762e8a3b86ee1e..44305e8e9606fd 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 b7966f8f03875b..13d25abf217efb 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 00000000000000..9e6b0fac07ba1e --- /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 00000000000000..c0119adf092757 --- /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 999bd866f14814..000cd8cd1bedb7 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 4464b5af8cd15b..3bd361f5e8cdfb 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 601107d2d86771..8af47dc9aeb9fe 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 c870ae9048b4f1..8698742d65d3d1 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 f8a4e11ce9c3b1..9646272cc3b6c7 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 9b35b6127dd01c..d77d6ff3039ba7 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 5b646fabca0225..dc4fca3c5645d3 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 2f14aa08334895..6f0b5564aba5e9 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 aaa22bef896602..6121463a242f08 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 f979890170b1a1..1f7bff4ab226bb 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 ed78d749a47fa8..b6c8f68a3b0b4c 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 b397aca5568079..70c387ca0f7fc6 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 2d7dfbde7082ee..e118a168c34a76 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 b43dca6f640b9a..2f62b0d268c9a7 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 7bf50a33728e53..67983576624803 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 85aff58968082d..e8c830376773ec 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 0df7f9d889413c..966bb6ad4d83f1 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 3e24e28ef58cde..82abb9efd010cd 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 14845b36028ca1..7c6528579dadd7 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 63ff6a5d1f8b61..5c98d1f9fd6395 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 811557498bb30e..763727a607020c 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 9a3e5cc4bf30e5..4874710e54e475 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 050eaed0b3f58f..47f79a9304c042 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 bb97e18fbfec01..14b5b79829d5f6 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 212c46b80c12f2..af673ca1ef3901 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 5ff1f2302cd369..3481e594c0955f 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 8af47dc9aeb9fe..f3ba8e8338eb83 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 8698742d65d3d1..ee760fcd834f09 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 9646272cc3b6c7..9e6cf72041f589 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 d77d6ff3039ba7..f4b3a6a5149f8e 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 dc4fca3c5645d3..bce7cf24cd88a6 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 6f0b5564aba5e9..867005bdea94c5 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 6121463a242f08..6dc79ff274ff7a 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 1f7bff4ab226bb..96fc38c1924e70 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 b6c8f68a3b0b4c..abb270a345eb5b 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 70c387ca0f7fc6..c90f553cfc9d62 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 13d25abf217efb..d6378fd4488bc6 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 e118a168c34a76..c2045fa57771c6 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 2f62b0d268c9a7..a547ad416ea8c0 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 67983576624803..5383df673bc29e 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 e8c830376773ec..374bca2fe10c78 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 966bb6ad4d83f1..8804a909ca39ce 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 82abb9efd010cd..8f86ad11349a0c 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 7c6528579dadd7..9f5bbbcb3bbe68 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 5c98d1f9fd6395..b02b06e58b1165 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 763727a607020c..995e8a26dcb1cc 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 4874710e54e475..c22bceac4b07fc 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 47f79a9304c042..c65e02dd965fdb 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 9e6b0fac07ba1e..e53c2798afe074 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 c0119adf092757..bddc7f9461f1fe 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 000cd8cd1bedb7..90fb40df8a0976 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 3bd361f5e8cdfb..05b01368e4b0be 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 14b5b79829d5f6..685aa30a37bc2e 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 dad745348f9b4b..99fce8578d175a 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 ad013944ce3ca3..1c2a87023e4651 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 5b49339650f0e9..866087ad0aef0c 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 fc954724bb72fe..07ad90d9f94c50 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 b463c0b6d0e402..6cce68e8ec51b9 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 85a53e6aa7a78b..a7012e3397060f 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 93387fb0b45038..c4e9270351c51e 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 af673ca1ef3901..d0a56f6f21de77 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 c90c554591e748..4d87e0b4e9e6c0 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 884de08eab1b16..cc1d55293e3df1 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 f7d3d2d0bbec23..0f174196524a88 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 a7beeea29b4556..cd4e11532e381b 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 39c8d114f1e2c5..ad6cad04012a77 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 3481e594c0955f..9380b87878ad38 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 d6378fd4488bc6..a7ba6e45fbd2d7 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 90fb40df8a0976..91b1d3ab8392e7 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 05b01368e4b0be..2c9ef94d7ca81e 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 4d87e0b4e9e6c0..28d6c647c69a5c 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``.