From 89b6e5aca753080f47326789e967e4989b2ee1ad Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 9 Aug 2025 11:38:41 +0300 Subject: [PATCH 1/3] gh-37817: Allow assignment to __bases__ of direct subclasses of builtin classes --- Lib/test/test_descr.py | 122 +++++++++++------- ...5-08-09-11-38-37.gh-issue-37817.Y5Fhde.rst | 2 + Objects/typeobject.c | 59 ++++++--- 3 files changed, 118 insertions(+), 65 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-11-38-37.gh-issue-37817.Y5Fhde.rst diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 8da6647c3f71fc..da7c35a61d623e 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -4077,42 +4077,82 @@ class E(D): self.assertEqual(e.a, 2) self.assertEqual(C2.__subclasses__(), [D]) - try: + with self.assertRaisesRegex(TypeError, + "cannot delete '__bases__' attribute of immutable type"): del D.__bases__ - except (TypeError, AttributeError): - pass - else: - self.fail("shouldn't be able to delete .__bases__") - - try: + with self.assertRaisesRegex(TypeError, 'can only assign non-empty tuple'): D.__bases__ = () - except TypeError as msg: - if str(msg) == "a new-style class can't have only classic bases": - self.fail("wrong error message for .__bases__ = ()") - else: - self.fail("shouldn't be able to set .__bases__ to ()") - - try: - D.__bases__ = (D,) - except TypeError: - pass - else: - # actually, we'll have crashed by here... - self.fail("shouldn't be able to create inheritance cycles") - - try: + with self.assertRaisesRegex(TypeError, 'can only assign tuple'): + D.__bases__ = [C] + with self.assertRaisesRegex(TypeError, 'duplicate base class'): D.__bases__ = (C, C) - except TypeError: - pass - else: - self.fail("didn't detect repeated base classes") - - try: + with self.assertRaisesRegex(TypeError, 'inheritance cycle'): + D.__bases__ = (D,) + with self.assertRaisesRegex(TypeError, 'inheritance cycle'): D.__bases__ = (E,) - except TypeError: - pass - else: - self.fail("shouldn't be able to create inheritance cycles") + + class A: + __slots__ = () + def __repr__(self): + return '' + class B: + __slots__ = () + b = B() + r = repr(b) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B.__bases__ = (int,) + B.__bases__ = (A,) + self.assertNotHasAttr(b, '__dict__') + self.assertNotHasAttr(b, '__weakref__') + self.assertEqual(repr(b), '') + B.__bases__ = (object,) + self.assertEqual(repr(b), r) + + class A_with_dict: + pass + class B_with_dict: + pass + b = B_with_dict() + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_with_dict.__bases__ = (A_with_dict,) + B_with_dict.__bases__ = (A,) + self.assertHasAttr(b, '__dict__') + self.assertHasAttr(b, '__weakref__') + B_with_dict.__bases__ = (object,) + + class A_int(int): + __slots__ = () + def __repr__(self): + return '' + class B_int(int): + __slots__ = () + b = B_int(42) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_int.__bases__ = (object,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_int.__bases__ = (tuple,) + with self.assertRaisesRegex(TypeError, 'is not an acceptable base type'): + B_int.__bases__ = (bool,) + B_int.__bases__ = (A_int,) + self.assertEqual(repr(b), '') + B_int.__bases__ = (int,) + self.assertEqual(repr(b), '42') + + class A_tuple(tuple): + __slots__ = () + def __repr__(self): + return '' + class B_tuple(tuple): + __slots__ = () + b = B_tuple((1, 2)) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_tuple.__bases__ = (object,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_tuple.__bases__ = (int,) + B_tuple.__bases__ = (A_tuple,) + self.assertEqual(repr(b), '') + B_tuple.__bases__ = (tuple,) + self.assertEqual(repr(b), '(1, 2)') def test_assign_bases_many_subclasses(self): # This is intended to check that typeobject.c:queue_slot_update() can @@ -4165,26 +4205,14 @@ class C(object): class D(C): pass - try: + with self.assertRaisesRegex(TypeError, 'layout differs'): L.__bases__ = (dict,) - except TypeError: - pass - else: - self.fail("shouldn't turn list subclass into dict subclass") - try: + with self.assertRaisesRegex(TypeError, 'immutable type'): list.__bases__ = (dict,) - except TypeError: - pass - else: - self.fail("shouldn't be able to assign to list.__bases__") - try: + with self.assertRaisesRegex(TypeError, 'layout differs'): D.__bases__ = (C, list) - except TypeError: - pass - else: - self.fail("best_base calculation found wanting") def test_unsubclassable_types(self): with self.assertRaises(TypeError): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-11-38-37.gh-issue-37817.Y5Fhde.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-11-38-37.gh-issue-37817.Y5Fhde.rst new file mode 100644 index 00000000000000..5e73188ff2d694 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-11-38-37.gh-issue-37817.Y5Fhde.rst @@ -0,0 +1,2 @@ +Allow assignment to :attr:`~type.__bases__` of direct subclasses of builtin +classes. diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 14bc5a4bc49f84..130e0b9eed24de 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -1748,7 +1748,7 @@ type_get_mro(PyObject *tp, void *Py_UNUSED(closure)) static PyTypeObject *find_best_base(PyObject *); static int mro_internal(PyTypeObject *, int, PyObject **); static int type_is_subtype_base_chain(PyTypeObject *, PyTypeObject *); -static int compatible_for_assignment(PyTypeObject *, PyTypeObject *, const char *); +static int compatible_for_assignment(PyTypeObject *, PyTypeObject *, const char *, int); static int add_subclass(PyTypeObject*, PyTypeObject*); static int add_all_subclasses(PyTypeObject *type, PyObject *bases); static void remove_subclass(PyTypeObject *, PyTypeObject *); @@ -1886,7 +1886,7 @@ type_check_new_bases(PyTypeObject *type, PyObject *new_bases, PyTypeObject **bes if (*best_base == NULL) return -1; - if (!compatible_for_assignment(type->tp_base, *best_base, "__bases__")) { + if (!compatible_for_assignment(type, *best_base, "__bases__", 0)) { return -1; } @@ -7230,8 +7230,6 @@ compatible_with_tp_base(PyTypeObject *child) child->tp_itemsize == parent->tp_itemsize && child->tp_dictoffset == parent->tp_dictoffset && child->tp_weaklistoffset == parent->tp_weaklistoffset && - ((child->tp_flags & Py_TPFLAGS_HAVE_GC) == - (parent->tp_flags & Py_TPFLAGS_HAVE_GC)) && (child->tp_dealloc == subtype_dealloc || child->tp_dealloc == parent->tp_dealloc)); } @@ -7266,11 +7264,24 @@ same_slots_added(PyTypeObject *a, PyTypeObject *b) } static int -compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char* attr) +compatible_flags(int setclass, PyTypeObject *origto, PyTypeObject *newto, unsigned long flags) +{ + /* For __class__ assignment, the flags should be the same. + For __bases__ assignment, the new base flags can only be set + if the original class flags are set. + */ + return setclass ? (origto->tp_flags & flags) == (newto->tp_flags & flags) + : !(~(origto->tp_flags & flags) & (newto->tp_flags & flags)); +} + +static int +compatible_for_assignment(PyTypeObject *origto, PyTypeObject *newto, + const char *attr, int setclass) { PyTypeObject *newbase, *oldbase; + PyTypeObject *oldto = setclass ? origto : origto->tp_base; - if (newto->tp_free != oldto->tp_free) { + if (setclass && newto->tp_free != oldto->tp_free) { PyErr_Format(PyExc_TypeError, "%s assignment: " "'%s' deallocator differs from '%s'", @@ -7279,6 +7290,28 @@ compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char* oldto->tp_name); return 0; } + if (!compatible_flags(setclass, origto, newto, + Py_TPFLAGS_HAVE_GC | + Py_TPFLAGS_INLINE_VALUES | + Py_TPFLAGS_PREHEADER)) + { + goto differs; + } + /* For __class__ assignment, tp_dictoffset and tp_weaklistoffset should + be the same for old and new types. + For __bases__ assignment, they can only be set in the new base + if they are set in the original class with the same value. + */ + if ((setclass || newto->tp_dictoffset) + && origto->tp_dictoffset != newto->tp_dictoffset) + { + goto differs; + } + if ((setclass || newto->tp_weaklistoffset) + && origto->tp_weaklistoffset != newto->tp_weaklistoffset) + { + goto differs; + } /* It's tricky to tell if two arbitrary types are sufficiently compatible as to be interchangeable; e.g., even if they have the same tp_basicsize, they @@ -7300,17 +7333,7 @@ compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char* !same_slots_added(newbase, oldbase))) { goto differs; } - if ((oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) != - ((newto->tp_flags & Py_TPFLAGS_INLINE_VALUES))) - { - goto differs; - } - /* The above does not check for the preheader */ - if ((oldto->tp_flags & Py_TPFLAGS_PREHEADER) == - ((newto->tp_flags & Py_TPFLAGS_PREHEADER))) - { - return 1; - } + return 1; differs: PyErr_Format(PyExc_TypeError, "%s assignment: " @@ -7387,7 +7410,7 @@ object_set_class_world_stopped(PyObject *self, PyTypeObject *newto) return -1; } - if (compatible_for_assignment(oldto, newto, "__class__")) { + if (compatible_for_assignment(oldto, newto, "__class__", 1)) { /* Changing the class will change the implicit dict keys, * so we must materialize the dictionary first. */ if (oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) { From 1c5d516155ddf2f279159e429e987f1a4c1756ee Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 9 Aug 2025 17:10:22 +0300 Subject: [PATCH 2/3] Support also classes with __dict__ and __weakref__. --- Lib/test/test_descr.py | 101 +++++++++++++++++++++++++++++++++++++---- Objects/typeobject.c | 2 - 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index da7c35a61d623e..a760d78b7d25db 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -4095,12 +4095,34 @@ class A: __slots__ = () def __repr__(self): return '' + class A_with_dict: + __slots__ = ('__dict__',) + def __repr__(self): + return '' + class A_with_dict_weakref: + def __repr__(self): + return '' + class A_with_slots: + __slots__ = ('x',) + def __repr__(self): + return '' + class A_with_slots_dict: + __slots__ = ('x', '__dict__') + def __repr__(self): + return '' + class B: __slots__ = () b = B() r = repr(b) with self.assertRaisesRegex(TypeError, 'layout differs'): B.__bases__ = (int,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B.__bases__ = (A_with_dict_weakref,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B.__bases__ = (A_with_dict,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B.__bases__ = (A_with_slots,) B.__bases__ = (A,) self.assertNotHasAttr(b, '__dict__') self.assertNotHasAttr(b, '__weakref__') @@ -4108,17 +4130,80 @@ class B: B.__bases__ = (object,) self.assertEqual(repr(b), r) - class A_with_dict: + class B_with_dict_weakref: pass - class B_with_dict: + b = B_with_dict_weakref() + with self.assertRaisesRegex(TypeError, 'layout differs'): + B.__bases__ = (A_with_slots,) + B_with_dict_weakref.__bases__ = (A_with_dict_weakref,) + self.assertEqual(repr(b), '') + B_with_dict_weakref.__bases__ = (A_with_dict,) + self.assertEqual(repr(b), '') + B_with_dict_weakref.__bases__ = (A,) + self.assertEqual(repr(b), '') + B_with_dict_weakref.__bases__ = (object,) + + class B_with_slots: + __slots__ = ('x',) + b = B_with_slots() + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_with_slots.__bases__ = (A_with_dict_weakref,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_with_slots.__bases__ = (A_with_dict,) + B_with_slots.__bases__ = (A,) + self.assertEqual(repr(b), '') + + class B_with_slots_dict: + __slots__ = ('x', '__dict__') + b = B_with_slots_dict() + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_with_slots_dict.__bases__ = (A_with_dict_weakref,) + B_with_slots_dict.__bases__ = (A_with_dict,) + self.assertEqual(repr(b), '') + B_with_slots_dict.__bases__ = (A,) + self.assertEqual(repr(b), '') + + class B_with_slots_dict_weakref: + __slots__ = ('x', '__dict__', '__weakref__') + b = B_with_slots_dict_weakref() + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_with_slots_dict_weakref.__bases__ = (A_with_slots_dict,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + B_with_slots_dict_weakref.__bases__ = (A_with_slots,) + B_with_slots_dict_weakref.__bases__ = (A_with_dict_weakref,) + self.assertEqual(repr(b), '') + B_with_slots_dict_weakref.__bases__ = (A_with_dict,) + self.assertEqual(repr(b), '') + B_with_slots_dict_weakref.__bases__ = (A,) + self.assertEqual(repr(b), '') + + class C_with_slots(A_with_slots): + __slots__ = () + c = C_with_slots() + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots.__bases__ = (A_with_slots_dict,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots.__bases__ = (A_with_dict_weakref,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots.__bases__ = (A_with_dict,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots.__bases__ = (A,) + C_with_slots.__bases__ = (A_with_slots,) + self.assertEqual(repr(c), '') + + class C_with_slots_dict(A_with_slots): pass - b = B_with_dict() + c = C_with_slots_dict() + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots_dict.__bases__ = (A_with_dict_weakref,) + with self.assertRaisesRegex(TypeError, 'layout differs'): + C_with_slots_dict.__bases__ = (A_with_dict,) with self.assertRaisesRegex(TypeError, 'layout differs'): - B_with_dict.__bases__ = (A_with_dict,) - B_with_dict.__bases__ = (A,) - self.assertHasAttr(b, '__dict__') - self.assertHasAttr(b, '__weakref__') - B_with_dict.__bases__ = (object,) + C_with_slots_dict.__bases__ = (A,) + C_with_slots_dict.__bases__ = (A_with_slots_dict,) + self.assertEqual(repr(c), '') + C_with_slots_dict.__bases__ = (A_with_slots,) + self.assertEqual(repr(c), '') class A_int(int): __slots__ = () diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 130e0b9eed24de..868e94ac3e8ff1 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -7228,8 +7228,6 @@ compatible_with_tp_base(PyTypeObject *child) return (parent != NULL && child->tp_basicsize == parent->tp_basicsize && child->tp_itemsize == parent->tp_itemsize && - child->tp_dictoffset == parent->tp_dictoffset && - child->tp_weaklistoffset == parent->tp_weaklistoffset && (child->tp_dealloc == subtype_dealloc || child->tp_dealloc == parent->tp_dealloc)); } From c24e6978f148a1c29d2713d6d7aaa123fb77c719 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Mon, 15 Sep 2025 19:15:16 +0300 Subject: [PATCH 3/3] Update Objects/typeobject.c Co-authored-by: Victor Stinner --- Objects/typeobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 868e94ac3e8ff1..82ff924afc16d3 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -7269,7 +7269,7 @@ compatible_flags(int setclass, PyTypeObject *origto, PyTypeObject *newto, unsign if the original class flags are set. */ return setclass ? (origto->tp_flags & flags) == (newto->tp_flags & flags) - : !(~(origto->tp_flags & flags) & (newto->tp_flags & flags)); + : !(~(origto->tp_flags & flags) & (newto->tp_flags & flags)); } static int