diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst index 5aaa8147dd3176..685aa30a37bc2e 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 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 f204b287b565eb..d0a56f6f21de77 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 c90c554591e748..28d6c647c69a5c 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 b64cfc51f75701..9380b87878ad38 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 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/_collections_abc.py b/Lib/_collections_abc.py index 601107d2d86771..f3ba8e8338eb83 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 c870ae9048b4f1..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.__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 f8a4e11ce9c3b1..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.__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 9b35b6127dd01c..f4b3a6a5149f8e 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 5b646fabca0225..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.__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 2f14aa08334895..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].__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 aaa22bef896602..6dc79ff274ff7a 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 f979890170b1a1..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.__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 ed78d749a47fa8..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('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 b397aca5568079..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__.__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 b7966f8f03875b..a7ba6e45fbd2d7 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 2d7dfbde7082ee..c2045fa57771c6 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 b43dca6f640b9a..a547ad416ea8c0 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 7bf50a33728e53..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 = "%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 85aff58968082d..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.__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 0df7f9d889413c..8804a909ca39ce 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 3e24e28ef58cde..8f86ad11349a0c 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 14845b36028ca1..9f5bbbcb3bbe68 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 63ff6a5d1f8b61..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.__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 811557498bb30e..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.__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 9a3e5cc4bf30e5..c22bceac4b07fc 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 050eaed0b3f58f..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 "%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 00000000000000..e53c2798afe074 --- /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 00000000000000..bddc7f9461f1fe --- /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 999bd866f14814..91b1d3ab8392e7 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 4464b5af8cd15b..2c9ef94d7ca81e 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) {