]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-37817: Allow assignment to __bases__ of direct subclasses of builtin classes ...
authorSerhiy Storchaka <storchaka@gmail.com>
Mon, 15 Sep 2025 16:40:28 +0000 (19:40 +0300)
committerGitHub <noreply@github.com>
Mon, 15 Sep 2025 16:40:28 +0000 (16:40 +0000)
Lib/test/test_descr.py
Misc/NEWS.d/next/Core_and_Builtins/2025-08-09-11-38-37.gh-issue-37817.Y5Fhde.rst [new file with mode: 0644]
Objects/typeobject.c

index 9dfeeccb81b34dd343a8b2412a5c747f61645860..39b835b03fc599a463f514be4f0b3cb7e1f1e677 100644 (file)
@@ -4077,42 +4077,167 @@ class ClassPropertiesAndMethods(unittest.TestCase):
         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:
+        with self.assertRaisesRegex(TypeError, 'can only assign tuple'):
+            D.__bases__ = [C]
+        with self.assertRaisesRegex(TypeError, 'duplicate base class'):
+            D.__bases__ = (C, C)
+        with self.assertRaisesRegex(TypeError, 'inheritance cycle'):
             D.__bases__ = (D,)
-        except TypeError:
-            pass
-        else:
-            # actually, we'll have crashed by here...
-            self.fail("shouldn't be able to create inheritance cycles")
+        with self.assertRaisesRegex(TypeError, 'inheritance cycle'):
+            D.__bases__ = (E,)
 
-        try:
-            D.__bases__ = (C, C)
-        except TypeError:
-            pass
-        else:
-            self.fail("didn't detect repeated base classes")
+        class A:
+            __slots__ = ()
+            def __repr__(self):
+                return '<A>'
+        class A_with_dict:
+            __slots__ = ('__dict__',)
+            def __repr__(self):
+                return '<A_with_dict>'
+        class A_with_dict_weakref:
+            def __repr__(self):
+                return '<A_with_dict_weakref>'
+        class A_with_slots:
+            __slots__ = ('x',)
+            def __repr__(self):
+                return '<A_with_slots>'
+        class A_with_slots_dict:
+            __slots__ = ('x', '__dict__')
+            def __repr__(self):
+                return '<A_with_slots_dict>'
 
-        try:
-            D.__bases__ = (E,)
-        except TypeError:
-            pass
-        else:
-            self.fail("shouldn't be able to create inheritance cycles")
+        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__')
+        self.assertEqual(repr(b), '<A>')
+        B.__bases__ = (object,)
+        self.assertEqual(repr(b), r)
+
+        class B_with_dict_weakref:
+            pass
+        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), '<A_with_dict_weakref>')
+        B_with_dict_weakref.__bases__ = (A_with_dict,)
+        self.assertEqual(repr(b), '<A_with_dict>')
+        B_with_dict_weakref.__bases__ = (A,)
+        self.assertEqual(repr(b), '<A>')
+        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), '<A>')
+
+        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), '<A_with_dict>')
+        B_with_slots_dict.__bases__ = (A,)
+        self.assertEqual(repr(b), '<A>')
+
+        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), '<A_with_dict_weakref>')
+        B_with_slots_dict_weakref.__bases__ = (A_with_dict,)
+        self.assertEqual(repr(b), '<A_with_dict>')
+        B_with_slots_dict_weakref.__bases__ = (A,)
+        self.assertEqual(repr(b), '<A>')
+
+        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), '<A_with_slots>')
+
+        class C_with_slots_dict(A_with_slots):
+            pass
+        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'):
+            C_with_slots_dict.__bases__ = (A,)
+        C_with_slots_dict.__bases__ = (A_with_slots_dict,)
+        self.assertEqual(repr(c), '<A_with_slots_dict>')
+        C_with_slots_dict.__bases__ = (A_with_slots,)
+        self.assertEqual(repr(c), '<A_with_slots>')
+
+        class A_int(int):
+            __slots__ = ()
+            def __repr__(self):
+                return '<A_int>'
+        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), '<A_int>')
+        B_int.__bases__ = (int,)
+        self.assertEqual(repr(b), '42')
+
+        class A_tuple(tuple):
+            __slots__ = ()
+            def __repr__(self):
+                return '<A_tuple>'
+        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), '<A_tuple>')
+        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 +4290,14 @@ class ClassPropertiesAndMethods(unittest.TestCase):
         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 (file)
index 0000000..5e73188
--- /dev/null
@@ -0,0 +1,2 @@
+Allow assignment to :attr:`~type.__bases__` of direct subclasses of builtin
+classes.
index 9cead729b6fe7ab8bf5e7cdbdcc7ca97d8e1db56..06f3ace1764a86f83b0f8c435fda630878acc3dd 100644 (file)
@@ -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;
     }
 
@@ -7263,10 +7263,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_flags & Py_TPFLAGS_HAVE_GC) ==
-             (parent->tp_flags & Py_TPFLAGS_HAVE_GC)) &&
             (child->tp_dealloc == subtype_dealloc ||
              child->tp_dealloc == parent->tp_dealloc));
 }
@@ -7301,11 +7297,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'",
@@ -7314,6 +7323,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
@@ -7335,17 +7366,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: "
@@ -7422,7 +7443,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) {