Skip to content

Commit f9a8c31

Browse files
committed
gh-111696: Add type.__fullyqualname__ attribute
Add PyType_GetFullyQualName() function with documentation and tests.
1 parent 7218bac commit f9a8c31

File tree

9 files changed

+158
-31
lines changed

9 files changed

+158
-31
lines changed

Doc/c-api/type.rst

+7
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,13 @@ Type Objects
185185
186186
.. versionadded:: 3.11
187187
188+
.. c:function:: PyObject* PyType_GetFullyQualName(PyTypeObject *type)
189+
190+
Return the type's fully qualified name. Equivalent to getting the
191+
type's :attr:`__fullyqualname__ <class.__fullyqualname__>` attribute.
192+
193+
.. versionadded:: 3.13
194+
188195
.. c:function:: void* PyType_GetSlot(PyTypeObject *type, int slot)
189196
190197
Return the function pointer stored in the given slot. If the

Doc/library/stdtypes.rst

+9
Original file line numberDiff line numberDiff line change
@@ -5496,6 +5496,15 @@ types, where they are relevant. Some of these are not reported by the
54965496
.. versionadded:: 3.3
54975497

54985498

5499+
.. attribute:: class.__fullyqualname__
5500+
5501+
The fully qualified name of the class instance:
5502+
``f"{class.__module__}.{class.__qualname__}"``, or ``class.__qualname__`` if
5503+
``class.__module__`` is not a string or is equal to ``"builtins"``.
5504+
5505+
.. versionadded:: 3.13
5506+
5507+
54995508
.. attribute:: definition.__type_params__
55005509

55015510
The :ref:`type parameters <type-params>` of generic classes, functions,

Doc/whatsnew/3.13.rst

+10
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ Other Language Changes
125125
equivalent of the :option:`-X frozen_modules <-X>` command-line option.
126126
(Contributed by Yilei Yang in :gh:`111374`.)
127127

128+
* Add :attr:`__fullyqualname__ <class.__fullyqualname__>` read-only attribute
129+
to types: the fully qualified type name.
130+
(Contributed by Victor Stinner in :gh:`111696`.)
131+
132+
128133
New Modules
129134
===========
130135

@@ -1181,6 +1186,11 @@ New Features
11811186
:exc:`KeyError` if the key missing.
11821187
(Contributed by Stefan Behnel and Victor Stinner in :gh:`111262`.)
11831188

1189+
* Add :c:func:`PyType_GetFullyQualName` function: get the type's fully
1190+
qualified name. It is equivalent to getting the type's
1191+
:attr:`__fullyqualname__ <class.__fullyqualname__>` attribute.
1192+
(Contributed by Victor Stinner in :gh:`111696`.)
1193+
11841194

11851195
Porting to Python 3.13
11861196
----------------------

Include/cpython/object.h

+1
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ PyAPI_FUNC(const char *) _PyType_Name(PyTypeObject *);
271271
PyAPI_FUNC(PyObject *) _PyType_Lookup(PyTypeObject *, PyObject *);
272272
PyAPI_FUNC(PyObject *) PyType_GetModuleByDef(PyTypeObject *, PyModuleDef *);
273273
PyAPI_FUNC(PyObject *) PyType_GetDict(PyTypeObject *);
274+
PyAPI_FUNC(PyObject *) PyType_GetFullyQualName(PyTypeObject *);
274275

275276
PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int);
276277
PyAPI_FUNC(void) _Py_BreakPoint(void);

Lib/test/test_builtin.py

+15-2
Original file line numberDiff line numberDiff line change
@@ -2430,6 +2430,7 @@ def test_new_type(self):
24302430
self.assertEqual(A.__name__, 'A')
24312431
self.assertEqual(A.__qualname__, 'A')
24322432
self.assertEqual(A.__module__, __name__)
2433+
self.assertEqual(A.__fullyqualname__, f'{__name__}.A')
24332434
self.assertEqual(A.__bases__, (object,))
24342435
self.assertIs(A.__base__, object)
24352436
x = A()
@@ -2443,6 +2444,7 @@ def ham(self):
24432444
self.assertEqual(C.__name__, 'C')
24442445
self.assertEqual(C.__qualname__, 'C')
24452446
self.assertEqual(C.__module__, __name__)
2447+
self.assertEqual(C.__fullyqualname__, f'{__name__}.C')
24462448
self.assertEqual(C.__bases__, (B, int))
24472449
self.assertIs(C.__base__, int)
24482450
self.assertIn('spam', C.__dict__)
@@ -2464,10 +2466,11 @@ def test_type_nokwargs(self):
24642466
def test_type_name(self):
24652467
for name in 'A', '\xc4', '\U0001f40d', 'B.A', '42', '':
24662468
with self.subTest(name=name):
2467-
A = type(name, (), {})
2469+
A = type(name, (), {'__qualname__': f'Test.{name}'})
24682470
self.assertEqual(A.__name__, name)
2469-
self.assertEqual(A.__qualname__, name)
2471+
self.assertEqual(A.__qualname__, f"Test.{name}")
24702472
self.assertEqual(A.__module__, __name__)
2473+
self.assertEqual(A.__fullyqualname__, f'{__name__}.Test.{name}')
24712474
with self.assertRaises(ValueError):
24722475
type('A\x00B', (), {})
24732476
with self.assertRaises(UnicodeEncodeError):
@@ -2482,6 +2485,7 @@ def test_type_name(self):
24822485
self.assertEqual(C.__name__, name)
24832486
self.assertEqual(C.__qualname__, 'C')
24842487
self.assertEqual(C.__module__, __name__)
2488+
self.assertEqual(C.__fullyqualname__, f'{__name__}.C')
24852489

24862490
A = type('C', (), {})
24872491
with self.assertRaises(ValueError):
@@ -2494,18 +2498,27 @@ def test_type_name(self):
24942498
A.__name__ = b'A'
24952499
self.assertEqual(A.__name__, 'C')
24962500

2501+
# if __module__ is not a string, ignore it silently
2502+
class D:
2503+
pass
2504+
self.assertEqual(D.__fullyqualname__, f'{__name__}.{D.__qualname__}')
2505+
D.__module__ = 123
2506+
self.assertEqual(D.__fullyqualname__, D.__qualname__)
2507+
24972508
def test_type_qualname(self):
24982509
A = type('A', (), {'__qualname__': 'B.C'})
24992510
self.assertEqual(A.__name__, 'A')
25002511
self.assertEqual(A.__qualname__, 'B.C')
25012512
self.assertEqual(A.__module__, __name__)
2513+
self.assertEqual(A.__fullyqualname__, f'{__name__}.B.C')
25022514
with self.assertRaises(TypeError):
25032515
type('A', (), {'__qualname__': b'B'})
25042516
self.assertEqual(A.__qualname__, 'B.C')
25052517

25062518
A.__qualname__ = 'D.E'
25072519
self.assertEqual(A.__name__, 'A')
25082520
self.assertEqual(A.__qualname__, 'D.E')
2521+
self.assertEqual(A.__fullyqualname__, f'{__name__}.D.E')
25092522
with self.assertRaises(TypeError):
25102523
A.__qualname__ = b'B'
25112524
self.assertEqual(A.__qualname__, 'D.E')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add :c:func:`PyType_GetFullyQualName` function: get the type's fully
2+
qualified name. It is equivalent to getting the type's
3+
:attr:`__fullyqualname__ <class.__fullyqualname__>` attribute. Patch by
4+
Victor Stinner.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Add :attr:`__fullyqualname__ <class.__fullyqualname__>` read-only attribute
2+
to types: the fully qualified type name. Patch by Victor Stinner.

Modules/_testcapimodule.c

+45-9
Original file line numberDiff line numberDiff line change
@@ -573,19 +573,19 @@ static PyObject *
573573
test_get_type_name(PyObject *self, PyObject *Py_UNUSED(ignored))
574574
{
575575
PyObject *tp_name = PyType_GetName(&PyLong_Type);
576-
assert(strcmp(PyUnicode_AsUTF8(tp_name), "int") == 0);
576+
assert(PyUnicode_EqualToUTF8(tp_name, "int"));
577577
Py_DECREF(tp_name);
578578

579579
tp_name = PyType_GetName(&PyModule_Type);
580-
assert(strcmp(PyUnicode_AsUTF8(tp_name), "module") == 0);
580+
assert(PyUnicode_EqualToUTF8(tp_name, "module"));
581581
Py_DECREF(tp_name);
582582

583583
PyObject *HeapTypeNameType = PyType_FromSpec(&HeapTypeNameType_Spec);
584584
if (HeapTypeNameType == NULL) {
585585
Py_RETURN_NONE;
586586
}
587587
tp_name = PyType_GetName((PyTypeObject *)HeapTypeNameType);
588-
assert(strcmp(PyUnicode_AsUTF8(tp_name), "HeapTypeNameType") == 0);
588+
assert(PyUnicode_EqualToUTF8(tp_name, "HeapTypeNameType"));
589589
Py_DECREF(tp_name);
590590

591591
PyObject *name = PyUnicode_FromString("test_name");
@@ -597,7 +597,7 @@ test_get_type_name(PyObject *self, PyObject *Py_UNUSED(ignored))
597597
goto done;
598598
}
599599
tp_name = PyType_GetName((PyTypeObject *)HeapTypeNameType);
600-
assert(strcmp(PyUnicode_AsUTF8(tp_name), "test_name") == 0);
600+
assert(PyUnicode_EqualToUTF8(tp_name, "test_name"));
601601
Py_DECREF(name);
602602
Py_DECREF(tp_name);
603603

@@ -611,19 +611,19 @@ static PyObject *
611611
test_get_type_qualname(PyObject *self, PyObject *Py_UNUSED(ignored))
612612
{
613613
PyObject *tp_qualname = PyType_GetQualName(&PyLong_Type);
614-
assert(strcmp(PyUnicode_AsUTF8(tp_qualname), "int") == 0);
614+
assert(PyUnicode_EqualToUTF8(tp_qualname, "int"));
615615
Py_DECREF(tp_qualname);
616616

617617
tp_qualname = PyType_GetQualName(&PyODict_Type);
618-
assert(strcmp(PyUnicode_AsUTF8(tp_qualname), "OrderedDict") == 0);
618+
assert(PyUnicode_EqualToUTF8(tp_qualname, "OrderedDict"));
619619
Py_DECREF(tp_qualname);
620620

621621
PyObject *HeapTypeNameType = PyType_FromSpec(&HeapTypeNameType_Spec);
622622
if (HeapTypeNameType == NULL) {
623623
Py_RETURN_NONE;
624624
}
625625
tp_qualname = PyType_GetQualName((PyTypeObject *)HeapTypeNameType);
626-
assert(strcmp(PyUnicode_AsUTF8(tp_qualname), "HeapTypeNameType") == 0);
626+
assert(PyUnicode_EqualToUTF8(tp_qualname, "HeapTypeNameType"));
627627
Py_DECREF(tp_qualname);
628628

629629
PyObject *spec_name = PyUnicode_FromString(HeapTypeNameType_Spec.name);
@@ -636,8 +636,7 @@ test_get_type_qualname(PyObject *self, PyObject *Py_UNUSED(ignored))
636636
goto done;
637637
}
638638
tp_qualname = PyType_GetQualName((PyTypeObject *)HeapTypeNameType);
639-
assert(strcmp(PyUnicode_AsUTF8(tp_qualname),
640-
"_testcapi.HeapTypeNameType") == 0);
639+
assert(PyUnicode_EqualToUTF8(tp_qualname, "_testcapi.HeapTypeNameType"));
641640
Py_DECREF(spec_name);
642641
Py_DECREF(tp_qualname);
643642

@@ -646,6 +645,42 @@ test_get_type_qualname(PyObject *self, PyObject *Py_UNUSED(ignored))
646645
Py_RETURN_NONE;
647646
}
648647

648+
static PyObject *
649+
test_get_type_fullyqualname(PyObject *self, PyObject *Py_UNUSED(ignored))
650+
{
651+
PyObject *name = PyType_GetFullyQualName(&PyLong_Type);
652+
assert(PyUnicode_EqualToUTF8(name, "int"));
653+
Py_DECREF(name);
654+
655+
name = PyType_GetFullyQualName(&PyODict_Type);
656+
assert(PyUnicode_EqualToUTF8(name, "collections.OrderedDict"));
657+
Py_DECREF(name);
658+
659+
PyObject *HeapTypeNameType = PyType_FromSpec(&HeapTypeNameType_Spec);
660+
if (HeapTypeNameType == NULL) {
661+
Py_RETURN_NONE;
662+
}
663+
name = PyType_GetFullyQualName((PyTypeObject *)HeapTypeNameType);
664+
assert(PyUnicode_EqualToUTF8(name, "_testcapi.HeapTypeNameType"));
665+
Py_DECREF(name);
666+
667+
PyObject *new_name = PyUnicode_FromString("override_name");
668+
if (new_name == NULL) {
669+
goto done;
670+
}
671+
672+
int res = PyObject_SetAttrString(HeapTypeNameType,
673+
"__fullyqualname__", new_name);
674+
Py_DECREF(new_name);
675+
assert(res < 0);
676+
assert(PyErr_ExceptionMatches(PyExc_AttributeError));
677+
PyErr_Clear();
678+
679+
done:
680+
Py_DECREF(HeapTypeNameType);
681+
Py_RETURN_NONE;
682+
}
683+
649684
static PyObject *
650685
test_get_type_dict(PyObject *self, PyObject *Py_UNUSED(ignored))
651686
{
@@ -3212,6 +3247,7 @@ static PyMethodDef TestMethods[] = {
32123247
{"test_get_statictype_slots", test_get_statictype_slots, METH_NOARGS},
32133248
{"test_get_type_name", test_get_type_name, METH_NOARGS},
32143249
{"test_get_type_qualname", test_get_type_qualname, METH_NOARGS},
3250+
{"test_get_type_fullyqualname", test_get_type_fullyqualname, METH_NOARGS},
32153251
{"test_get_type_dict", test_get_type_dict, METH_NOARGS},
32163252
{"_test_thread_state", test_thread_state, METH_VARARGS},
32173253
#ifndef MS_WINDOWS

Objects/typeobject.c

+65-20
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,58 @@ type_set_module(PyTypeObject *type, PyObject *value, void *context)
11231123
return PyDict_SetItem(dict, &_Py_ID(__module__), value);
11241124
}
11251125

1126+
1127+
static PyObject*
1128+
type_fullyqualname(PyTypeObject *type, int is_repr)
1129+
{
1130+
// type is a static type and PyType_Ready() was not called on it yet?
1131+
if (type->tp_name == NULL) {
1132+
PyErr_SetString(PyExc_TypeError, "static type not initialized");
1133+
return NULL;
1134+
}
1135+
1136+
if (!(type->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
1137+
return PyUnicode_FromString(type->tp_name);
1138+
}
1139+
1140+
PyObject *qualname = type_qualname(type, NULL);
1141+
if (qualname == NULL) {
1142+
return NULL;
1143+
}
1144+
1145+
PyObject *module = type_module(type, NULL);
1146+
if (module == NULL) {
1147+
if (is_repr) {
1148+
// type_repr() ignores type_module() errors
1149+
PyErr_Clear();
1150+
return qualname;
1151+
}
1152+
1153+
Py_DECREF(qualname);
1154+
return NULL;
1155+
}
1156+
1157+
PyObject *result;
1158+
if (PyUnicode_Check(module)
1159+
&& !_PyUnicode_Equal(module, &_Py_ID(builtins)))
1160+
{
1161+
result = PyUnicode_FromFormat("%U.%U", module, qualname);
1162+
}
1163+
else {
1164+
result = Py_NewRef(qualname);
1165+
}
1166+
Py_DECREF(module);
1167+
Py_DECREF(qualname);
1168+
return result;
1169+
}
1170+
1171+
static PyObject *
1172+
type_get_fullyqualname(PyTypeObject *type, void *context)
1173+
{
1174+
return type_fullyqualname(type, 0);
1175+
}
1176+
1177+
11261178
static PyObject *
11271179
type_abstractmethods(PyTypeObject *type, void *context)
11281180
{
@@ -1583,6 +1635,7 @@ type___subclasscheck___impl(PyTypeObject *self, PyObject *subclass)
15831635
static PyGetSetDef type_getsets[] = {
15841636
{"__name__", (getter)type_name, (setter)type_set_name, NULL},
15851637
{"__qualname__", (getter)type_qualname, (setter)type_set_qualname, NULL},
1638+
{"__fullyqualname__", (getter)type_get_fullyqualname, NULL, NULL},
15861639
{"__bases__", (getter)type_get_bases, (setter)type_set_bases, NULL},
15871640
{"__mro__", (getter)type_get_mro, NULL, NULL},
15881641
{"__module__", (getter)type_module, (setter)type_set_module, NULL},
@@ -1600,33 +1653,18 @@ static PyObject *
16001653
type_repr(PyTypeObject *type)
16011654
{
16021655
if (type->tp_name == NULL) {
1603-
// type_repr() called before the type is fully initialized
1604-
// by PyType_Ready().
1656+
// If type_repr() is called before the type is fully initialized
1657+
// by PyType_Ready(), just format the type memory address.
16051658
return PyUnicode_FromFormat("<class at %p>", type);
16061659
}
16071660

1608-
PyObject *mod, *name, *rtn;
1609-
1610-
mod = type_module(type, NULL);
1611-
if (mod == NULL)
1612-
PyErr_Clear();
1613-
else if (!PyUnicode_Check(mod)) {
1614-
Py_SETREF(mod, NULL);
1615-
}
1616-
name = type_qualname(type, NULL);
1661+
PyObject *name = type_fullyqualname(type, 1);
16171662
if (name == NULL) {
1618-
Py_XDECREF(mod);
16191663
return NULL;
16201664
}
1621-
1622-
if (mod != NULL && !_PyUnicode_Equal(mod, &_Py_ID(builtins)))
1623-
rtn = PyUnicode_FromFormat("<class '%U.%U'>", mod, name);
1624-
else
1625-
rtn = PyUnicode_FromFormat("<class '%s'>", type->tp_name);
1626-
1627-
Py_XDECREF(mod);
1665+
PyObject *result = PyUnicode_FromFormat("<class '%U'>", name);
16281666
Py_DECREF(name);
1629-
return rtn;
1667+
return result;
16301668
}
16311669

16321670
static PyObject *
@@ -4540,6 +4578,13 @@ PyType_GetQualName(PyTypeObject *type)
45404578
return type_qualname(type, NULL);
45414579
}
45424580

4581+
PyObject *
4582+
PyType_GetFullyQualName(PyTypeObject *type)
4583+
{
4584+
return type_get_fullyqualname(type, NULL);
4585+
}
4586+
4587+
45434588
void *
45444589
PyType_GetSlot(PyTypeObject *type, int slot)
45454590
{

0 commit comments

Comments
 (0)