gh-37817: Allow assignment to __bases__ of direct subclasses of builtin classes (GH-137585)

This commit is contained in:
Serhiy Storchaka 2025-09-15 19:40:28 +03:00 committed by GitHub
parent 811acc85d5
commit 29d026f93e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 202 additions and 66 deletions

View 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:
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:
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>'
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
else:
self.fail("shouldn't be able to create inheritance cycles")
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):

View file

@ -0,0 +1,2 @@
Allow assignment to :attr:`~type.__bases__` of direct subclasses of builtin
classes.

View 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) {