https://github.com/python/cpython/commit/f673f0e7b49b305ba7608386a354da328066a664
commit: f673f0e7b49b305ba7608386a354da328066a664
branch: main
author: Mikhail Efimov <[email protected]>
committer: JelleZijlstra <[email protected]>
date: 2025-10-15T09:08:17-07:00
summary:

gh-139817: Attribute `__qualname__` is added to `TypeAliasType` (#139919)

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2025-10-12-01-12-12.gh-issue-139817.PAn-8Z.rst
M Doc/library/typing.rst
M Include/internal/pycore_global_objects_fini_generated.h
M Include/internal/pycore_global_strings.h
M Include/internal/pycore_runtime_init_generated.h
M Include/internal/pycore_unicodeobject_generated.h
M Lib/test/test_type_aliases.py
M Objects/clinic/typevarobject.c.h
M Objects/typevarobject.c

diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst
index 279ae3ef820069..4265c587195b5c 100644
--- a/Doc/library/typing.rst
+++ b/Doc/library/typing.rst
@@ -2243,7 +2243,7 @@ without the dedicated syntax, as documented below.
    .. versionadded:: 3.10
 
 
-.. class:: TypeAliasType(name, value, *, type_params=())
+.. class:: TypeAliasType(name, value, *, type_params=(), qualname=None)
 
    The type of type aliases created through the :keyword:`type` statement.
 
@@ -2267,6 +2267,20 @@ without the dedicated syntax, as documented below.
          >>> Alias.__name__
          'Alias'
 
+   .. attribute:: __qualname__
+
+      The :term:`qualified name` of the type alias:
+
+      .. doctest::
+
+        >>> class Class:
+        ...     type Alias = int
+        ...
+        >>> Class.Alias.__qualname__
+        'Class.Alias'
+
+   .. versionadded:: 3.15
+
    .. attribute:: __module__
 
       The name of the module in which the type alias was defined::
diff --git a/Include/internal/pycore_global_objects_fini_generated.h 
b/Include/internal/pycore_global_objects_fini_generated.h
index f7416c5ffc53eb..92ded14891a101 100644
--- a/Include/internal/pycore_global_objects_fini_generated.h
+++ b/Include/internal/pycore_global_objects_fini_generated.h
@@ -1974,6 +1974,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) {
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ps1));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(ps2));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(qid));
+    _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(qualname));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(query));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(queuetype));
     _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(quotetabs));
diff --git a/Include/internal/pycore_global_strings.h 
b/Include/internal/pycore_global_strings.h
index ca71c12836dc00..cd21b0847b7cdd 100644
--- a/Include/internal/pycore_global_strings.h
+++ b/Include/internal/pycore_global_strings.h
@@ -697,6 +697,7 @@ struct _Py_global_strings {
         STRUCT_FOR_ID(ps1)
         STRUCT_FOR_ID(ps2)
         STRUCT_FOR_ID(qid)
+        STRUCT_FOR_ID(qualname)
         STRUCT_FOR_ID(query)
         STRUCT_FOR_ID(queuetype)
         STRUCT_FOR_ID(quotetabs)
diff --git a/Include/internal/pycore_runtime_init_generated.h 
b/Include/internal/pycore_runtime_init_generated.h
index 72996db9f71b8c..50d82d0a365037 100644
--- a/Include/internal/pycore_runtime_init_generated.h
+++ b/Include/internal/pycore_runtime_init_generated.h
@@ -1972,6 +1972,7 @@ extern "C" {
     INIT_ID(ps1), \
     INIT_ID(ps2), \
     INIT_ID(qid), \
+    INIT_ID(qualname), \
     INIT_ID(query), \
     INIT_ID(queuetype), \
     INIT_ID(quotetabs), \
diff --git a/Include/internal/pycore_unicodeobject_generated.h 
b/Include/internal/pycore_unicodeobject_generated.h
index c4cf56ad7f1bbb..b4d920154b6e83 100644
--- a/Include/internal/pycore_unicodeobject_generated.h
+++ b/Include/internal/pycore_unicodeobject_generated.h
@@ -2576,6 +2576,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) 
{
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
     assert(PyUnicode_GET_LENGTH(string) != 1);
+    string = &_Py_ID(qualname);
+    _PyUnicode_InternStatic(interp, &string);
+    assert(_PyUnicode_CheckConsistency(string, 1));
+    assert(PyUnicode_GET_LENGTH(string) != 1);
     string = &_Py_ID(query);
     _PyUnicode_InternStatic(interp, &string);
     assert(_PyUnicode_CheckConsistency(string, 1));
diff --git a/Lib/test/test_type_aliases.py b/Lib/test/test_type_aliases.py
index ee1791bc1d0b9d..9ceee565764c9a 100644
--- a/Lib/test/test_type_aliases.py
+++ b/Lib/test/test_type_aliases.py
@@ -8,6 +8,11 @@
     Callable, TypeAliasType, TypeVar, TypeVarTuple, ParamSpec, Unpack, 
get_args,
 )
 
+type GlobalTypeAlias = int
+
+def get_type_alias():
+    type TypeAliasInFunc = str
+    return TypeAliasInFunc
 
 class TypeParamsInvalidTest(unittest.TestCase):
     def test_name_collisions(self):
@@ -70,6 +75,8 @@ def inner[B](self):
 
 
 class TypeParamsAliasValueTest(unittest.TestCase):
+    type TypeAliasInClass = dict
+
     def test_alias_value_01(self):
         type TA1 = int
 
@@ -142,33 +149,67 @@ def test_subscripting(self):
         self.assertIs(specialized2.__origin__, VeryGeneric)
         self.assertEqual(specialized2.__args__, (int, str, float, [bool, 
range]))
 
+    def test___name__(self):
+        type TypeAliasLocal = GlobalTypeAlias
+
+        self.assertEqual(GlobalTypeAlias.__name__, 'GlobalTypeAlias')
+        self.assertEqual(get_type_alias().__name__, 'TypeAliasInFunc')
+        self.assertEqual(self.TypeAliasInClass.__name__, 'TypeAliasInClass')
+        self.assertEqual(TypeAliasLocal.__name__, 'TypeAliasLocal')
+
+        with self.assertRaisesRegex(
+            AttributeError,
+            "readonly attribute",
+        ):
+            setattr(TypeAliasLocal, '__name__', 'TA')
+
+    def test___qualname__(self):
+        type TypeAliasLocal = GlobalTypeAlias
+
+        self.assertEqual(GlobalTypeAlias.__qualname__,
+                         'GlobalTypeAlias')
+        self.assertEqual(get_type_alias().__qualname__,
+                         'get_type_alias.<locals>.TypeAliasInFunc')
+        self.assertEqual(self.TypeAliasInClass.__qualname__,
+                         'TypeParamsAliasValueTest.TypeAliasInClass')
+        self.assertEqual(TypeAliasLocal.__qualname__,
+                         
'TypeParamsAliasValueTest.test___qualname__.<locals>.TypeAliasLocal')
+
+        with self.assertRaisesRegex(
+            AttributeError,
+            "readonly attribute",
+        ):
+            setattr(TypeAliasLocal, '__qualname__', 'TA')
+
     def test_repr(self):
         type Simple = int
-        type VeryGeneric[T, *Ts, **P] = Callable[P, tuple[T, *Ts]]
+        self.assertEqual(repr(Simple), Simple.__qualname__)
 
-        self.assertEqual(repr(Simple), "Simple")
-        self.assertEqual(repr(VeryGeneric), "VeryGeneric")
+        type VeryGeneric[T, *Ts, **P] = Callable[P, tuple[T, *Ts]]
+        self.assertEqual(repr(VeryGeneric), VeryGeneric.__qualname__)
+        fullname = f"{VeryGeneric.__module__}.{VeryGeneric.__qualname__}"
         self.assertEqual(repr(VeryGeneric[int, bytes, str, [float, object]]),
-                         "VeryGeneric[int, bytes, str, [float, object]]")
+                         f"{fullname}[int, bytes, str, [float, object]]")
         self.assertEqual(repr(VeryGeneric[int, []]),
-                         "VeryGeneric[int, []]")
+                         f"{fullname}[int, []]")
         self.assertEqual(repr(VeryGeneric[int, [VeryGeneric[int], list[str]]]),
-                         "VeryGeneric[int, [VeryGeneric[int], list[str]]]")
+                         f"{fullname}[int, [{fullname}[int], list[str]]]")
 
     def test_recursive_repr(self):
         type Recursive = Recursive
-        self.assertEqual(repr(Recursive), "Recursive")
+        self.assertEqual(repr(Recursive), Recursive.__qualname__)
 
         type X = list[Y]
         type Y = list[X]
-        self.assertEqual(repr(X), "X")
-        self.assertEqual(repr(Y), "Y")
+        self.assertEqual(repr(X), X.__qualname__)
+        self.assertEqual(repr(Y), Y.__qualname__)
 
         type GenericRecursive[X] = list[X | GenericRecursive[X]]
-        self.assertEqual(repr(GenericRecursive), "GenericRecursive")
-        self.assertEqual(repr(GenericRecursive[int]), "GenericRecursive[int]")
+        self.assertEqual(repr(GenericRecursive), GenericRecursive.__qualname__)
+        fullname = 
f"{GenericRecursive.__module__}.{GenericRecursive.__qualname__}"
+        self.assertEqual(repr(GenericRecursive[int]), f"{fullname}[int]")
         self.assertEqual(repr(GenericRecursive[GenericRecursive[int]]),
-                         "GenericRecursive[GenericRecursive[int]]")
+                         f"{fullname}[{fullname}[int]]")
 
     def test_raising(self):
         type MissingName = list[_My_X]
@@ -193,15 +234,25 @@ class TypeAliasConstructorTest(unittest.TestCase):
     def test_basic(self):
         TA = TypeAliasType("TA", int)
         self.assertEqual(TA.__name__, "TA")
+        self.assertEqual(TA.__qualname__, "TA")
         self.assertIs(TA.__value__, int)
         self.assertEqual(TA.__type_params__, ())
         self.assertEqual(TA.__module__, __name__)
 
+    def test_with_qualname(self):
+        TA = TypeAliasType("TA", str, qualname="Class.TA")
+        self.assertEqual(TA.__name__, "TA")
+        self.assertEqual(TA.__qualname__, "Class.TA")
+        self.assertIs(TA.__value__, str)
+        self.assertEqual(TA.__type_params__, ())
+        self.assertEqual(TA.__module__, __name__)
+
     def test_attributes_with_exec(self):
         ns = {}
         exec("type TA = int", ns, ns)
         TA = ns["TA"]
         self.assertEqual(TA.__name__, "TA")
+        self.assertEqual(TA.__qualname__, "TA")
         self.assertIs(TA.__value__, int)
         self.assertEqual(TA.__type_params__, ())
         self.assertIs(TA.__module__, None)
@@ -210,6 +261,7 @@ def test_generic(self):
         T = TypeVar("T")
         TA = TypeAliasType("TA", list[T], type_params=(T,))
         self.assertEqual(TA.__name__, "TA")
+        self.assertEqual(TA.__qualname__, "TA")
         self.assertEqual(TA.__value__, list[T])
         self.assertEqual(TA.__type_params__, (T,))
         self.assertEqual(TA.__module__, __name__)
@@ -218,6 +270,7 @@ def test_generic(self):
     def test_not_generic(self):
         TA = TypeAliasType("TA", list[int], type_params=())
         self.assertEqual(TA.__name__, "TA")
+        self.assertEqual(TA.__qualname__, "TA")
         self.assertEqual(TA.__value__, list[int])
         self.assertEqual(TA.__type_params__, ())
         self.assertEqual(TA.__module__, __name__)
@@ -268,8 +321,9 @@ def test_expects_type_like(self):
             TypeAliasType("A", int, type_params=(T, 2))
 
     def test_keywords(self):
-        TA = TypeAliasType(name="TA", value=int)
+        TA = TypeAliasType(name="TA", value=int, type_params=(), qualname=None)
         self.assertEqual(TA.__name__, "TA")
+        self.assertEqual(TA.__qualname__, "TA")
         self.assertIs(TA.__value__, int)
         self.assertEqual(TA.__type_params__, ())
         self.assertEqual(TA.__module__, __name__)
@@ -283,6 +337,8 @@ def test_errors(self):
             TypeAliasType("TA", list, ())
         with self.assertRaises(TypeError):
             TypeAliasType("TA", list, type_params=42)
+        with self.assertRaises(TypeError):
+            TypeAliasType("TA", list, qualname=range(5))
 
 
 class TypeAliasTypeTest(unittest.TestCase):
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-12-01-12-12.gh-issue-139817.PAn-8Z.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-12-01-12-12.gh-issue-139817.PAn-8Z.rst
new file mode 100644
index 00000000000000..b205d21edfec0c
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-12-01-12-12.gh-issue-139817.PAn-8Z.rst
@@ -0,0 +1,2 @@
+Attribute ``__qualname__`` is added to :class:`typing.TypeAliasType`.
+Patch by Mikhail Efimov.
diff --git a/Objects/clinic/typevarobject.c.h b/Objects/clinic/typevarobject.c.h
index 9ae2f51f758403..bd4c7a0e64fd49 100644
--- a/Objects/clinic/typevarobject.c.h
+++ b/Objects/clinic/typevarobject.c.h
@@ -688,14 +688,14 @@ typealias_reduce(PyObject *self, PyObject 
*Py_UNUSED(ignored))
 }
 
 PyDoc_STRVAR(typealias_new__doc__,
-"typealias(name, value, *, type_params=<unrepresentable>)\n"
+"typealias(name, value, *, type_params=<unrepresentable>, qualname=None)\n"
 "--\n"
 "\n"
 "Create a TypeAliasType.");
 
 static PyObject *
 typealias_new_impl(PyTypeObject *type, PyObject *name, PyObject *value,
-                   PyObject *type_params);
+                   PyObject *type_params, PyObject *qualname);
 
 static PyObject *
 typealias_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
@@ -703,7 +703,7 @@ typealias_new(PyTypeObject *type, PyObject *args, PyObject 
*kwargs)
     PyObject *return_value = NULL;
     #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
 
-    #define NUM_KEYWORDS 3
+    #define NUM_KEYWORDS 4
     static struct {
         PyGC_Head _this_is_not_used;
         PyObject_VAR_HEAD
@@ -712,7 +712,7 @@ typealias_new(PyTypeObject *type, PyObject *args, PyObject 
*kwargs)
     } _kwtuple = {
         .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
         .ob_hash = -1,
-        .ob_item = { &_Py_ID(name), &_Py_ID(value), &_Py_ID(type_params), },
+        .ob_item = { &_Py_ID(name), &_Py_ID(value), &_Py_ID(type_params), 
&_Py_ID(qualname), },
     };
     #undef NUM_KEYWORDS
     #define KWTUPLE (&_kwtuple.ob_base.ob_base)
@@ -721,20 +721,21 @@ typealias_new(PyTypeObject *type, PyObject *args, 
PyObject *kwargs)
     #  define KWTUPLE NULL
     #endif  // !Py_BUILD_CORE
 
-    static const char * const _keywords[] = {"name", "value", "type_params", 
NULL};
+    static const char * const _keywords[] = {"name", "value", "type_params", 
"qualname", NULL};
     static _PyArg_Parser _parser = {
         .keywords = _keywords,
         .fname = "typealias",
         .kwtuple = KWTUPLE,
     };
     #undef KWTUPLE
-    PyObject *argsbuf[3];
+    PyObject *argsbuf[4];
     PyObject * const *fastargs;
     Py_ssize_t nargs = PyTuple_GET_SIZE(args);
     Py_ssize_t noptargs = nargs + (kwargs ? PyDict_GET_SIZE(kwargs) : 0) - 2;
     PyObject *name;
     PyObject *value;
     PyObject *type_params = NULL;
+    PyObject *qualname = NULL;
 
     fastargs = _PyArg_UnpackKeywords(_PyTuple_CAST(args)->ob_item, nargs, 
kwargs, NULL, &_parser,
             /*minpos*/ 2, /*maxpos*/ 2, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
@@ -750,11 +751,17 @@ typealias_new(PyTypeObject *type, PyObject *args, 
PyObject *kwargs)
     if (!noptargs) {
         goto skip_optional_kwonly;
     }
-    type_params = fastargs[2];
+    if (fastargs[2]) {
+        type_params = fastargs[2];
+        if (!--noptargs) {
+            goto skip_optional_kwonly;
+        }
+    }
+    qualname = fastargs[3];
 skip_optional_kwonly:
-    return_value = typealias_new_impl(type, name, value, type_params);
+    return_value = typealias_new_impl(type, name, value, type_params, 
qualname);
 
 exit:
     return return_value;
 }
-/*[clinic end generated code: output=9dad71445e079303 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=67ab9a5d1869f2c9 input=a9049054013a1b77]*/
diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c
index 4fe46e9fccb939..8a3a1e9834583a 100644
--- a/Objects/typevarobject.c
+++ b/Objects/typevarobject.c
@@ -53,6 +53,7 @@ typedef struct {
 typedef struct {
     PyObject_HEAD
     PyObject *name;
+    PyObject *qualname;
     PyObject *type_params;
     PyObject *compute_value;
     PyObject *value;
@@ -1858,6 +1859,7 @@ typealias_dealloc(PyObject *self)
     _PyObject_GC_UNTRACK(self);
     typealiasobject *ta = typealiasobject_CAST(self);
     Py_XDECREF(ta->name);
+    Py_XDECREF(ta->qualname);
     Py_XDECREF(ta->type_params);
     Py_XDECREF(ta->compute_value);
     Py_XDECREF(ta->value);
@@ -1884,11 +1886,12 @@ static PyObject *
 typealias_repr(PyObject *self)
 {
     typealiasobject *ta = (typealiasobject *)self;
-    return Py_NewRef(ta->name);
+    return Py_NewRef(ta->qualname);
 }
 
 static PyMemberDef typealias_members[] = {
     {"__name__", _Py_T_OBJECT, offsetof(typealiasobject, name), Py_READONLY},
+    {"__qualname__", _Py_T_OBJECT, offsetof(typealiasobject, qualname), 
Py_READONLY},
     {0}
 };
 
@@ -2003,7 +2006,7 @@ typealias_check_type_params(PyObject *type_params, int 
*err) {
 }
 
 static PyObject *
-typelias_convert_type_params(PyObject *type_params)
+typealias_convert_type_params(PyObject *type_params)
 {
     if (
         type_params == NULL
@@ -2018,14 +2021,15 @@ typelias_convert_type_params(PyObject *type_params)
 }
 
 static typealiasobject *
-typealias_alloc(PyObject *name, PyObject *type_params, PyObject *compute_value,
-                PyObject *value, PyObject *module)
+typealias_alloc(PyObject *name, PyObject *qualname, PyObject *type_params,
+                PyObject *compute_value, PyObject *value, PyObject *module)
 {
     typealiasobject *ta = PyObject_GC_New(typealiasobject, &_PyTypeAlias_Type);
     if (ta == NULL) {
         return NULL;
     }
     ta->name = Py_NewRef(name);
+    ta->qualname = Py_NewRef(qualname);
     ta->type_params = Py_XNewRef(type_params);
     ta->compute_value = Py_XNewRef(compute_value);
     ta->value = Py_XNewRef(value);
@@ -2039,6 +2043,7 @@ typealias_traverse(PyObject *op, visitproc visit, void 
*arg)
 {
     typealiasobject *self = typealiasobject_CAST(op);
     Py_VISIT(self->name);
+    Py_VISIT(self->qualname);
     Py_VISIT(self->type_params);
     Py_VISIT(self->compute_value);
     Py_VISIT(self->value);
@@ -2051,6 +2056,7 @@ typealias_clear(PyObject *op)
 {
     typealiasobject *self = typealiasobject_CAST(op);
     Py_CLEAR(self->name);
+    Py_CLEAR(self->qualname);
     Py_CLEAR(self->type_params);
     Py_CLEAR(self->compute_value);
     Py_CLEAR(self->value);
@@ -2096,14 +2102,15 @@ typealias.__new__ as typealias_new
     value: object
     *
     type_params: object = NULL
+    qualname: object(c_default="NULL") = None
 
 Create a TypeAliasType.
 [clinic start generated code]*/
 
 static PyObject *
 typealias_new_impl(PyTypeObject *type, PyObject *name, PyObject *value,
-                   PyObject *type_params)
-/*[clinic end generated code: output=8920ce6bdff86f00 input=df163c34e17e1a35]*/
+                   PyObject *type_params, PyObject *qualname)
+/*[clinic end generated code: output=b7f6d9f1c577cd9c input=cbec290f8c4886ef]*/
 {
     if (type_params != NULL && !PyTuple_Check(type_params)) {
         PyErr_SetString(PyExc_TypeError, "type_params must be a tuple");
@@ -2120,8 +2127,19 @@ typealias_new_impl(PyTypeObject *type, PyObject *name, 
PyObject *value,
     if (module == NULL) {
         return NULL;
     }
-    PyObject *ta = (PyObject *)typealias_alloc(name, checked_params, NULL, 
value,
-                                               module);
+
+    if (qualname == NULL || qualname == Py_None) {
+        // If qualname was not set directly, we use name instead.
+        qualname = name;
+    } else {
+        if (!PyUnicode_Check(qualname)) {
+            PyErr_SetString(PyExc_TypeError, "qualname must be a string");
+            return NULL;
+        }
+    }
+
+    PyObject *ta = (PyObject *)typealias_alloc(
+        name, qualname, checked_params, NULL, value, module);
     Py_DECREF(module);
     return ta;
 }
@@ -2187,10 +2205,17 @@ _Py_make_typealias(PyThreadState* unused, PyObject 
*args)
     assert(PyTuple_GET_SIZE(args) == 3);
     PyObject *name = PyTuple_GET_ITEM(args, 0);
     assert(PyUnicode_Check(name));
-    PyObject *type_params = 
typelias_convert_type_params(PyTuple_GET_ITEM(args, 1));
+    PyObject *type_params = 
typealias_convert_type_params(PyTuple_GET_ITEM(args, 1));
     PyObject *compute_value = PyTuple_GET_ITEM(args, 2);
     assert(PyFunction_Check(compute_value));
-    return (PyObject *)typealias_alloc(name, type_params, compute_value, NULL, 
NULL);
+
+    PyFunctionObject *compute_func = (PyFunctionObject *)compute_value;
+    PyCodeObject *code_obj = (PyCodeObject *)compute_func->func_code;
+    PyObject *qualname = code_obj->co_qualname;
+    assert(qualname != NULL);
+
+    return (PyObject *)typealias_alloc(
+        name, qualname, type_params, compute_value, NULL, NULL);
 }
 
 PyDoc_STRVAR(generic_doc,

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to