From 41b92791ada94fda3cbff5d89aacdfd8353654ed Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 18 May 2023 16:14:58 -0700 Subject: [PATCH 1/5] gh-104600: Make type.__type_params__ writable --- Lib/test/test_builtin.py | 13 ++++++++++++ Objects/typeobject.c | 44 +++++++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 821710a7fa3286..60449e66550d67 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -18,6 +18,7 @@ import sys import traceback import types +import typing import unittest import warnings from contextlib import ExitStack @@ -2485,6 +2486,18 @@ def test_type_qualname(self): A.__qualname__ = b'B' self.assertEqual(A.__qualname__, 'D.E') + def test_type_typeparams(self): + class A[T]: + pass + T, = A.__type_params__ + self.assertIsInstance(T, typing.TypeVar) + A.__type_params__ = "whatever" + self.assertEqual(A.__type_params__, "whatever") + del A.__type_params__ + self.assertEqual(A.__type_params__, ()) + with self.assertRaises(AttributeError): + del A.__type_params__ + def test_type_doc(self): for doc in 'x', '\xc4', '\U0001f40d', 'x\x00y', b'x', 42, None: A = type('A', (), {'__doc__': doc}) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 624dc63ce82cc0..30d40c8917d2eb 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1460,6 +1460,36 @@ type_get_annotations(PyTypeObject *type, void *context) return annotations; } +static int +type_set_annotations(PyTypeObject *type, PyObject *value, void *context) +{ + if (_PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE)) { + PyErr_Format(PyExc_TypeError, + "cannot set '__annotations__' attribute of immutable type '%s'", + type->tp_name); + return -1; + } + + int result; + PyObject *dict = lookup_tp_dict(type); + if (value != NULL) { + /* set */ + result = PyDict_SetItem(dict, &_Py_ID(__annotations__), value); + } else { + /* delete */ + if (!PyDict_Contains(dict, &_Py_ID(__annotations__))) { + PyErr_Format(PyExc_AttributeError, "__annotations__"); + return -1; + } + result = PyDict_DelItem(dict, &_Py_ID(__annotations__)); + } + + if (result == 0) { + PyType_Modified(type); + } + return result; +} + static PyObject * type_get_type_params(PyTypeObject *type, void *context) { @@ -1473,11 +1503,11 @@ type_get_type_params(PyTypeObject *type, void *context) } static int -type_set_annotations(PyTypeObject *type, PyObject *value, void *context) +type_set_type_params(PyTypeObject *type, PyObject *value, void *context) { if (_PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE)) { PyErr_Format(PyExc_TypeError, - "cannot set '__annotations__' attribute of immutable type '%s'", + "cannot set '__type_params__' attribute of immutable type '%s'", type->tp_name); return -1; } @@ -1486,14 +1516,14 @@ type_set_annotations(PyTypeObject *type, PyObject *value, void *context) PyObject *dict = lookup_tp_dict(type); if (value != NULL) { /* set */ - result = PyDict_SetItem(dict, &_Py_ID(__annotations__), value); + result = PyDict_SetItem(dict, &_Py_ID(__type_params__), value); } else { /* delete */ - if (!PyDict_Contains(dict, &_Py_ID(__annotations__))) { - PyErr_Format(PyExc_AttributeError, "__annotations__"); + if (!PyDict_Contains(dict, &_Py_ID(__type_params__))) { + PyErr_Format(PyExc_AttributeError, "__type_params__"); return -1; } - result = PyDict_DelItem(dict, &_Py_ID(__annotations__)); + result = PyDict_DelItem(dict, &_Py_ID(__type_params__)); } if (result == 0) { @@ -1548,7 +1578,7 @@ static PyGetSetDef type_getsets[] = { {"__doc__", (getter)type_get_doc, (setter)type_set_doc, NULL}, {"__text_signature__", (getter)type_get_text_signature, NULL, NULL}, {"__annotations__", (getter)type_get_annotations, (setter)type_set_annotations, NULL}, - {"__type_params__", (getter)type_get_type_params, NULL, NULL}, + {"__type_params__", (getter)type_get_type_params, (setter)type_set_type_params, NULL}, {0} }; From 3f4339d94cfcf5b957d19dd5e779e22e33138a32 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 18 May 2023 16:19:12 -0700 Subject: [PATCH 2/5] be like __doc__, not __annotations__ --- Lib/test/test_builtin.py | 5 ++--- Objects/typeobject.c | 18 ++---------------- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 60449e66550d67..1257b529038afb 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -2493,10 +2493,9 @@ class A[T]: self.assertIsInstance(T, typing.TypeVar) A.__type_params__ = "whatever" self.assertEqual(A.__type_params__, "whatever") - del A.__type_params__ - self.assertEqual(A.__type_params__, ()) - with self.assertRaises(AttributeError): + with self.assertRaises(TypeError): del A.__type_params__ + self.assertEqual(A.__type_params__, "whatever") def test_type_doc(self): for doc in 'x', '\xc4', '\U0001f40d', 'x\x00y', b'x', 42, None: diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 30d40c8917d2eb..2fbcafe91aadc6 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1505,26 +1505,12 @@ type_get_type_params(PyTypeObject *type, void *context) static int type_set_type_params(PyTypeObject *type, PyObject *value, void *context) { - if (_PyType_HasFeature(type, Py_TPFLAGS_IMMUTABLETYPE)) { - PyErr_Format(PyExc_TypeError, - "cannot set '__type_params__' attribute of immutable type '%s'", - type->tp_name); + if (!check_set_special_type_attr(type, value, "__type_params__")) { return -1; } - int result; PyObject *dict = lookup_tp_dict(type); - if (value != NULL) { - /* set */ - result = PyDict_SetItem(dict, &_Py_ID(__type_params__), value); - } else { - /* delete */ - if (!PyDict_Contains(dict, &_Py_ID(__type_params__))) { - PyErr_Format(PyExc_AttributeError, "__type_params__"); - return -1; - } - result = PyDict_DelItem(dict, &_Py_ID(__type_params__)); - } + int result = PyDict_SetItem(dict, &_Py_ID(__type_params__), value); if (result == 0) { PyType_Modified(type); From 9b982c4d943a8b326b47b7a94562300bda3d770c Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 18 May 2023 16:40:52 -0700 Subject: [PATCH 3/5] Add tests for generic namedtuples and typeddicts --- Lib/test/test_typing.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index bf038bf143a6c8..ac2d67c5f2ee75 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6795,6 +6795,16 @@ class Y(Generic[T], NamedTuple): with self.assertRaises(TypeError): G[int, str] + def test_generic_pep695(self): + class X[T](NamedTuple): + x: T + T, = X.__type_params__ + self.assertIsInstance(T, TypeVar) + self.assertEqual(T.__name__, 'T') + self.assertEqual(X.__bases__, (tuple, Generic)) + self.assertEqual(X.__orig_bases__, (NamedTuple, Generic[T])) + self.assertEqual(X.__mro__, (X, tuple, Generic, object)) + def test_non_generic_subscript(self): # For backward compatibility, subscription works # on arbitrary NamedTuple types. @@ -7205,6 +7215,20 @@ class FooBarGeneric(BarGeneric[int]): {'a': typing.Optional[T], 'b': int, 'c': str} ) + def test_pep695_generic_typeddict(self): + class A[T](TypedDict): + a: T + + T, = A.__type_params__ + self.assertIsInstance(T, TypeVar) + self.assertEqual(T.__name__, 'T') + self.assertEqual(A.__bases__, (Generic, dict)) + self.assertEqual(A.__orig_bases__, (TypedDict, Generic[T])) + self.assertEqual(A.__mro__, (A, Generic, dict, object)) + self.assertEqual(A.__parameters__, (T,)) + self.assertEqual(A[str].__parameters__, ()) + self.assertEqual(A[str].__args__, (str,)) + def test_generic_inheritance(self): class A(TypedDict, Generic[T]): a: T From b870cd19ff185a63c8c3609863950152758a8296 Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Thu, 18 May 2023 17:12:47 -0700 Subject: [PATCH 4/5] Update test --- Lib/test/test_type_params.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_type_params.py b/Lib/test/test_type_params.py index 96bd1fa0bab990..b046f12a635272 100644 --- a/Lib/test/test_type_params.py +++ b/Lib/test/test_type_params.py @@ -813,10 +813,11 @@ def test_typeparams_dunder_class_03(self): class ClassA[A](): pass ClassA.__type_params__ = () + params = ClassA.__type_params__ """ - with self.assertRaisesRegex(AttributeError, "attribute '__type_params__' of 'type' objects is not writable"): - run_code(code) + ns = run_code(code) + self.assertEqual(ns["params"], ()) def test_typeparams_dunder_function_01(self): def outer[A, B](): From 0bed086951b9c7fda368958f79e4dd130927388e Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Fri, 19 May 2023 06:16:39 -0700 Subject: [PATCH 5/5] Update Lib/test/test_typing.py Co-authored-by: Alex Waygood --- Lib/test/test_typing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index ac2d67c5f2ee75..a4eb734f1b3cc4 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -6804,6 +6804,9 @@ class X[T](NamedTuple): self.assertEqual(X.__bases__, (tuple, Generic)) self.assertEqual(X.__orig_bases__, (NamedTuple, Generic[T])) self.assertEqual(X.__mro__, (X, tuple, Generic, object)) + self.assertEqual(X.__parameters__, (T,)) + self.assertEqual(X[str].__args__, (str,)) + self.assertEqual(X[str].__parameters__, ()) def test_non_generic_subscript(self): # For backward compatibility, subscription works