From 2bb30890205946de77e9375e8c2220f56d514d13 Mon Sep 17 00:00:00 2001 From: Mikhail Efimov Date: Fri, 10 Oct 2025 22:10:43 +0300 Subject: [PATCH 1/3] Parameter __qualname__ is added to TypeAliasType --- Lib/test/test_type_aliases.py | 26 ++++++++++++++------------ Objects/typevarobject.c | 29 +++++++++++++++++++++-------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/Lib/test/test_type_aliases.py b/Lib/test/test_type_aliases.py index ee1791bc1d0b9d..7deca5779ea406 100644 --- a/Lib/test/test_type_aliases.py +++ b/Lib/test/test_type_aliases.py @@ -144,31 +144,33 @@ def test_subscripting(self): 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] diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 522e9fd9c955af..93beca4ef2b1d5 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; @@ -1852,6 +1853,7 @@ typealias_dealloc(PyObject *self) _PyObject_GC_UNTRACK(self); typealiasobject *ta = typealiasobject_CAST(self); Py_DECREF(ta->name); + Py_DECREF(ta->qualname); Py_XDECREF(ta->type_params); Py_XDECREF(ta->compute_value); Py_XDECREF(ta->value); @@ -1878,11 +1880,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} }; @@ -1997,7 +2000,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 @@ -2012,14 +2015,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); @@ -2112,8 +2116,10 @@ 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); + + // It's impossible to determine qualname, so we use name insted. + PyObject *ta = (PyObject *)typealias_alloc( + name, name, checked_params, NULL, value, module); Py_DECREF(module); return ta; } @@ -2179,10 +2185,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, From c9f3f8b008ba55526e7eff05c812f9b37927f76d Mon Sep 17 00:00:00 2001 From: Mikhail Efimov Date: Fri, 10 Oct 2025 22:16:35 +0300 Subject: [PATCH 2/3] Typo fix --- Objects/typevarobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c index 93beca4ef2b1d5..cc04de73bd6060 100644 --- a/Objects/typevarobject.c +++ b/Objects/typevarobject.c @@ -2117,7 +2117,7 @@ typealias_new_impl(PyTypeObject *type, PyObject *name, PyObject *value, return NULL; } - // It's impossible to determine qualname, so we use name insted. + // It's impossible to determine qualname, so we use name instead. PyObject *ta = (PyObject *)typealias_alloc( name, name, checked_params, NULL, value, module); Py_DECREF(module); From ef9609a94fac99144add2d93893d43369a7a6927 Mon Sep 17 00:00:00 2001 From: Mikhail Efimov Date: Sun, 12 Oct 2025 01:16:32 +0300 Subject: [PATCH 3/3] Tests on __qualname__, docs and news --- Doc/library/typing.rst | 14 +++++++ Lib/test/test_type_aliases.py | 39 +++++++++++++++++++ ...-10-12-01-12-12.gh-issue-139817.PAn-8Z.rst | 2 + 3 files changed, 55 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-12-01-12-12.gh-issue-139817.PAn-8Z.rst diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 279ae3ef820069..16b5f0d9697674 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -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/Lib/test/test_type_aliases.py b/Lib/test/test_type_aliases.py index 7deca5779ea406..2d6c926c97f6ab 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,6 +149,38 @@ 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..TypeAliasInFunc') + self.assertEqual(self.TypeAliasInClass.__qualname__, + 'TypeParamsAliasValueTest.TypeAliasInClass') + self.assertEqual(TypeAliasLocal.__qualname__, + 'TypeParamsAliasValueTest.test___qualname__..TypeAliasLocal') + + with self.assertRaisesRegex( + AttributeError, + "readonly attribute", + ): + setattr(TypeAliasLocal, '__qualname__', 'TA') + def test_repr(self): type Simple = int self.assertEqual(repr(Simple), Simple.__qualname__) 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.