Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Doc/c-api/type.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:`__fully_qualified_name__ <class.__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
Expand Down
9 changes: 9 additions & 0 deletions Doc/library/stdtypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 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 <type-params>` of generic classes, functions,
Expand Down
10 changes: 10 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:`__fully_qualified_name__ <class.__fully_qualified_name__>` read-only attribute
to types: the fully qualified type name.
(Contributed by Victor Stinner in :gh:`111696`.)


New Modules
===========

Expand Down Expand Up @@ -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:`__fully_qualified_name__ <class.__fully_qualified_name__>` attribute.
(Contributed by Victor Stinner in :gh:`111696`.)


Porting to Python 3.13
----------------------
Expand Down
1 change: 1 addition & 0 deletions Include/cpython/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 1 addition & 3 deletions Lib/_collections_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion Lib/_py_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_"):
Expand Down
2 changes: 1 addition & 1 deletion Lib/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions Lib/codecs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions Lib/contextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Lib/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion Lib/multiprocessing/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)}>')

Expand Down
2 changes: 1 addition & 1 deletion Lib/pdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion Lib/test/support/asyncore.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 15 additions & 2 deletions Lib/test/test_builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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__)
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -2494,18 +2498,27 @@ 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__)

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')

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')
Expand Down
3 changes: 1 addition & 2 deletions Lib/test/test_configparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
12 changes: 6 additions & 6 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ def __str__(self):
err = traceback.format_exception_only(X, X())
self.assertEqual(len(err), 1)
str_value = '<exception str() failed>'
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):
Expand Down Expand Up @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -1928,7 +1928,7 @@ def __str__(self):
1/0
err = self.get_report(X())
str_value = '<exception str() failed>'
str_name = '.'.join([X.__module__, X.__qualname__])
str_name = X.__fully_qualified_name__
self.assertEqual(MODULE_PREFIX + err, f"{str_name}: {str_value}\n")


Expand Down
2 changes: 1 addition & 1 deletion Lib/test/test_zipimport_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
10 changes: 5 additions & 5 deletions Lib/threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
Expand Down
3 changes: 1 addition & 2 deletions Lib/tkinter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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_']
Expand Down
4 changes: 2 additions & 2 deletions Lib/tkinter/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
9 changes: 2 additions & 7 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion Lib/unittest/async_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion Lib/unittest/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading