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 1f6b27b14d074b..c420616ec3f6f5 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1973,6 +1973,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 6959343947c1f4..f52875fb4e8a62 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -696,6 +696,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 be4eae42b5de1b..9345e42d3f44a3 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1971,6 +1971,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 45b00a20a07dda..c83efad38df48a 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -2572,6 +2572,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..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 - 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=)\n" +"typealias(name, value, *, type_params=, 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 522e9fd9c955af..b1fbef1a68935b 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); @@ -2088,14 +2092,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"); @@ -2112,8 +2117,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; } @@ -2179,10 +2195,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,