diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 5aaa8147dd3176a..685aa30a37bc2e1 100644 --- a/Doc/c-api/type.rst +++ b/Doc/c-api/type.rst @@ -185,6 +185,14 @@ Type Objects .. versionadded:: 3.11 +.. c:function:: PyObject* PyType_GetFullyQualifiedName(PyTypeObject *type) + + Return the type's :term:`fully qualified name`. Equivalent to getting the + type's :attr:`__fully_qualified_name__ ` + 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/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 f204b287b565eb3..d0a56f6f21de776 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.__fully_qualified_name__ + + 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"``. + + .. versionadded:: 3.13 + + .. attribute:: definition.__type_params__ The :ref:`type parameters ` of generic classes, functions, diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index c90c554591e7484..28d6c647c69a5cf 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 b64cfc51f75701b..9380b87878ad382 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 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`.) + + 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 + :term:`fully qualified name`. It is equivalent to getting the type's + :attr:`__fully_qualified_name__ ` 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/_collections_abc.py b/Lib/_collections_abc.py index 601107d2d867718..f3ba8e8338eb83b 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.__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 c870ae9048b4f13..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.__module__}.{cls.__qualname__}", 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 f8a4e11ce9c3b1e..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.__module__}.{cls.__qualname__}", 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 9b35b6127dd01c7..f4b3a6a5149f8e4 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__.__fully_qualified_name__, self.name, id(self)) def __getnewargs__(self): diff --git a/Lib/contextlib.py b/Lib/contextlib.py index 5b646fabca0225b..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.__module__}.{cls.__qualname__}' 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.__module__}.{cls.__qualname__}' 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 2f14aa083348958..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].__module__}.{exception[0].__qualname__}:", + f"{exception[0].__fully_qualified_name__}:", ) exc_msg_index = next( index diff --git a/Lib/inspect.py b/Lib/inspect.py index aaa22bef8966028..6dc79ff274ff7a2 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.__fully_qualified_name__ return repr(annotation) def formatannotationrelativeto(object): diff --git a/Lib/multiprocessing/pool.py b/Lib/multiprocessing/pool.py index f979890170b1a1f..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.__module__}.{cls.__qualname__} ' + 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 ed78d749a47fa8e..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('Class %s.%s' % (value.__module__, value.__qualname__)) + 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 b397aca5568079d..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__.__module__+"."+self.__class__.__qualname__] + 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 b7966f8f03875b3..a7ba6e45fbd2d73 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.__fully_qualified_name__, 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.__fully_qualified_name__, 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.__fully_qualified_name__, 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.__fully_qualified_name__, f'{__name__}.C') A = type('C', (), {}) with self.assertRaises(ValueError): @@ -2494,11 +2498,41 @@ 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.__fully_qualified_name__, f'{__name__}.{D.__qualname__}') + 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') self.assertEqual(A.__qualname__, 'B.C') self.assertEqual(A.__module__, __name__) + 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') @@ -2506,6 +2540,7 @@ def test_type_qualname(self): A.__qualname__ = 'D.E' self.assertEqual(A.__name__, 'A') self.assertEqual(A.__qualname__, '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 2d7dfbde7082ee3..c2045fa57771c61 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.__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 b43dca6f640b9ae..a547ad416ea8c0f 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.__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 = '.'.join([A.B.X.__module__, A.B.X.__qualname__]) + str_name = A.B.X.__fully_qualified_name__ 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.__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 = '.'.join([X.__module__, X.__qualname__]) + 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 7bf50a33728e53a..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 = "%s.%s" % (obj.__module__, obj.__qualname__) + 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 85aff58968082d8..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.__module__}.{cls.__qualname__} 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.__module__}.{cls.__qualname__} 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.__module__}.{cls.__qualname__} 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.__module__}.{cls.__qualname__} at {id(self):#x}: broken>" - return (f"<{cls.__module__}.{cls.__qualname__} 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 0df7f9d889413c8..8804a909ca39cea 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__.__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 3e24e28ef58cde9..8f86ad11349a0cf 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__.__fully_qualified_name__}" + 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..9f5bbbcb3bbe682 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.__fully_qualified_name__ 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.__fully_qualified_name__}.' @_tp_cache def __getitem__(self, params): diff --git a/Lib/unittest/async_case.py b/Lib/unittest/async_case.py index 63ff6a5d1f8b611..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.__module__}.{cls.__qualname__}' 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 811557498bb30ed..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.__module__}.{cls.__qualname__}' 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 9a3e5cc4bf30e57..c22bceac4b07fcc 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.__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 050eaed0b3f58fb..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 "%s.%s" % (cls.__module__, cls.__qualname__) + 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 new file mode 100644 index 000000000000000..e53c2798afe0742 --- /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:`__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 new file mode 100644 index 000000000000000..bddc7f9461f1fee --- /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:`__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 999bd866f148144..91b1d3ab8392e70 100644 --- a/Modules/_testcapimodule.c +++ b/Modules/_testcapimodule.c @@ -573,19 +573,19 @@ 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); if (HeapTypeNameType == NULL) { - Py_RETURN_NONE; + return NULL; } 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,19 +611,19 @@ 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); if (HeapTypeNameType == NULL) { - Py_RETURN_NONE; + return NULL; } 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) { + return NULL; + } + 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, + "__fully_qualified_name__", 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..2c9ef94d7ca81ec 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_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) { + 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_fullyqualname(PyTypeObject *type, void *context) +{ + return PyType_GetFullyQualifiedName(type); +} + + 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}, + {"__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}, @@ -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_impl(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_fullyqualname_impl(type, 0); +} + + void * PyType_GetSlot(PyTypeObject *type, int slot) {