Skip to content

PEP 737: gh-111696: Add type.__fully_qualified_name__ attribute #112133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
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