Skip to content

Commit

Permalink
pythongh-111696: Add type.__fullyqualname__ attribute
Browse files Browse the repository at this point in the history
Add PyType_GetFullyQualName() function.
  • Loading branch information
vstinner committed Nov 15, 2023
1 parent d9fd33a commit 9c95806
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 33 deletions.
9 changes: 9 additions & 0 deletions Doc/library/stdtypes.rst
Expand Up @@ -5496,6 +5496,15 @@ types, where they are relevant. Some of these are not reported by the
.. versionadded:: 3.3


.. attribute:: class.__fullyqualname__

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
5 changes: 5 additions & 0 deletions Doc/whatsnew/3.13.rst
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:`__fullyqualname__ <class.__fullyqualname__>` read-only attribute
to types: the fully qualified type name.
(Contributed by Victor Stinner in :gh:`111696`.)


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

Expand Down
1 change: 1 addition & 0 deletions Include/cpython/object.h
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_GetFullyQualName(PyTypeObject *);

PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int);
PyAPI_FUNC(void) _Py_BreakPoint(void);
Expand Down
17 changes: 15 additions & 2 deletions Lib/test/test_builtin.py
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.__fullyqualname__, 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.__fullyqualname__, 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.__fullyqualname__, 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.__fullyqualname__, 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.__fullyqualname__, f'{__name__}.{D.__qualname__}')
D.__module__ = 123
self.assertEqual(D.__fullyqualname__, 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.__fullyqualname__, 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.__fullyqualname__, f'{__name__}.D.E')
with self.assertRaises(TypeError):
A.__qualname__ = b'B'
self.assertEqual(A.__qualname__, 'D.E')
Expand Down
54 changes: 45 additions & 9 deletions Modules/_testcapimodule.c
Expand Up @@ -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;
}
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");
Expand All @@ -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);

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

Expand All @@ -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_GetFullyQualName(&PyLong_Type);
assert(PyUnicode_EqualToUTF8(name, "int"));
Py_DECREF(name);

name = PyType_GetFullyQualName(&PyODict_Type);
assert(PyUnicode_EqualToUTF8(name, "collections.OrderedDict"));
Py_DECREF(name);

PyObject *HeapTypeNameType = PyType_FromSpec(&HeapTypeNameType_Spec);
if (HeapTypeNameType == NULL) {
Py_RETURN_NONE;
}
name = PyType_GetFullyQualName((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,
"__fullyqualname__", 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))
{
Expand Down Expand Up @@ -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
Expand Down
89 changes: 67 additions & 22 deletions Objects/typeobject.c
Expand Up @@ -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(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_get_fullyqualname(PyTypeObject *type, void *context)
{
return type_fullyqualname(type, 0);
}


static PyObject *
type_abstractmethods(PyTypeObject *type, void *context)
{
Expand Down Expand Up @@ -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},
{"__fullyqualname__", (getter)type_get_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},
Expand All @@ -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("<class at %p>", 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);
if (name == NULL) {
Py_XDECREF(mod);
PyObject *fullqualname = type_fullyqualname(type, 1);
if (fullqualname == NULL) {
return NULL;
}

if (mod != NULL && !_PyUnicode_Equal(mod, &_Py_ID(builtins)))
rtn = PyUnicode_FromFormat("<class '%U.%U'>", mod, name);
else
rtn = PyUnicode_FromFormat("<class '%s'>", type->tp_name);

Py_XDECREF(mod);
Py_DECREF(name);
return rtn;
PyObject *result = PyUnicode_FromFormat("<class '%U'>", fullqualname);
Py_DECREF(fullqualname);
return result;
}

static PyObject *
Expand Down Expand Up @@ -4540,6 +4578,13 @@ PyType_GetQualName(PyTypeObject *type)
return type_qualname(type, NULL);
}

PyObject *
PyType_GetFullyQualName(PyTypeObject *type)
{
return type_fullyqualname(type, 0);
}


void *
PyType_GetSlot(PyTypeObject *type, int slot)
{
Expand Down

0 comments on commit 9c95806

Please sign in to comment.