Skip to content

Commit 29d026f

Browse files
gh-37817: Allow assignment to __bases__ of direct subclasses of builtin classes (GH-137585)
1 parent 811acc8 commit 29d026f

File tree

3 files changed

+202
-66
lines changed

3 files changed

+202
-66
lines changed

Lib/test/test_descr.py

Lines changed: 159 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4077,42 +4077,167 @@ class E(D):
40774077
self.assertEqual(e.a, 2)
40784078
self.assertEqual(C2.__subclasses__(), [D])
40794079

4080-
try:
4080+
with self.assertRaisesRegex(TypeError,
4081+
"cannot delete '__bases__' attribute of immutable type"):
40814082
del D.__bases__
4082-
except (TypeError, AttributeError):
4083-
pass
4084-
else:
4085-
self.fail("shouldn't be able to delete .__bases__")
4086-
4087-
try:
4083+
with self.assertRaisesRegex(TypeError, 'can only assign non-empty tuple'):
40884084
D.__bases__ = ()
4089-
except TypeError as msg:
4090-
if str(msg) == "a new-style class can't have only classic bases":
4091-
self.fail("wrong error message for .__bases__ = ()")
4092-
else:
4093-
self.fail("shouldn't be able to set .__bases__ to ()")
4094-
4095-
try:
4085+
with self.assertRaisesRegex(TypeError, 'can only assign tuple'):
4086+
D.__bases__ = [C]
4087+
with self.assertRaisesRegex(TypeError, 'duplicate base class'):
4088+
D.__bases__ = (C, C)
4089+
with self.assertRaisesRegex(TypeError, 'inheritance cycle'):
40964090
D.__bases__ = (D,)
4097-
except TypeError:
4098-
pass
4099-
else:
4100-
# actually, we'll have crashed by here...
4101-
self.fail("shouldn't be able to create inheritance cycles")
4091+
with self.assertRaisesRegex(TypeError, 'inheritance cycle'):
4092+
D.__bases__ = (E,)
41024093

4103-
try:
4104-
D.__bases__ = (C, C)
4105-
except TypeError:
4106-
pass
4107-
else:
4108-
self.fail("didn't detect repeated base classes")
4094+
class A:
4095+
__slots__ = ()
4096+
def __repr__(self):
4097+
return '<A>'
4098+
class A_with_dict:
4099+
__slots__ = ('__dict__',)
4100+
def __repr__(self):
4101+
return '<A_with_dict>'
4102+
class A_with_dict_weakref:
4103+
def __repr__(self):
4104+
return '<A_with_dict_weakref>'
4105+
class A_with_slots:
4106+
__slots__ = ('x',)
4107+
def __repr__(self):
4108+
return '<A_with_slots>'
4109+
class A_with_slots_dict:
4110+
__slots__ = ('x', '__dict__')
4111+
def __repr__(self):
4112+
return '<A_with_slots_dict>'
41094113

4110-
try:
4111-
D.__bases__ = (E,)
4112-
except TypeError:
4113-
pass
4114-
else:
4115-
self.fail("shouldn't be able to create inheritance cycles")
4114+
class B:
4115+
__slots__ = ()
4116+
b = B()
4117+
r = repr(b)
4118+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4119+
B.__bases__ = (int,)
4120+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4121+
B.__bases__ = (A_with_dict_weakref,)
4122+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4123+
B.__bases__ = (A_with_dict,)
4124+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4125+
B.__bases__ = (A_with_slots,)
4126+
B.__bases__ = (A,)
4127+
self.assertNotHasAttr(b, '__dict__')
4128+
self.assertNotHasAttr(b, '__weakref__')
4129+
self.assertEqual(repr(b), '<A>')
4130+
B.__bases__ = (object,)
4131+
self.assertEqual(repr(b), r)
4132+
4133+
class B_with_dict_weakref:
4134+
pass
4135+
b = B_with_dict_weakref()
4136+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4137+
B.__bases__ = (A_with_slots,)
4138+
B_with_dict_weakref.__bases__ = (A_with_dict_weakref,)
4139+
self.assertEqual(repr(b), '<A_with_dict_weakref>')
4140+
B_with_dict_weakref.__bases__ = (A_with_dict,)
4141+
self.assertEqual(repr(b), '<A_with_dict>')
4142+
B_with_dict_weakref.__bases__ = (A,)
4143+
self.assertEqual(repr(b), '<A>')
4144+
B_with_dict_weakref.__bases__ = (object,)
4145+
4146+
class B_with_slots:
4147+
__slots__ = ('x',)
4148+
b = B_with_slots()
4149+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4150+
B_with_slots.__bases__ = (A_with_dict_weakref,)
4151+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4152+
B_with_slots.__bases__ = (A_with_dict,)
4153+
B_with_slots.__bases__ = (A,)
4154+
self.assertEqual(repr(b), '<A>')
4155+
4156+
class B_with_slots_dict:
4157+
__slots__ = ('x', '__dict__')
4158+
b = B_with_slots_dict()
4159+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4160+
B_with_slots_dict.__bases__ = (A_with_dict_weakref,)
4161+
B_with_slots_dict.__bases__ = (A_with_dict,)
4162+
self.assertEqual(repr(b), '<A_with_dict>')
4163+
B_with_slots_dict.__bases__ = (A,)
4164+
self.assertEqual(repr(b), '<A>')
4165+
4166+
class B_with_slots_dict_weakref:
4167+
__slots__ = ('x', '__dict__', '__weakref__')
4168+
b = B_with_slots_dict_weakref()
4169+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4170+
B_with_slots_dict_weakref.__bases__ = (A_with_slots_dict,)
4171+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4172+
B_with_slots_dict_weakref.__bases__ = (A_with_slots,)
4173+
B_with_slots_dict_weakref.__bases__ = (A_with_dict_weakref,)
4174+
self.assertEqual(repr(b), '<A_with_dict_weakref>')
4175+
B_with_slots_dict_weakref.__bases__ = (A_with_dict,)
4176+
self.assertEqual(repr(b), '<A_with_dict>')
4177+
B_with_slots_dict_weakref.__bases__ = (A,)
4178+
self.assertEqual(repr(b), '<A>')
4179+
4180+
class C_with_slots(A_with_slots):
4181+
__slots__ = ()
4182+
c = C_with_slots()
4183+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4184+
C_with_slots.__bases__ = (A_with_slots_dict,)
4185+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4186+
C_with_slots.__bases__ = (A_with_dict_weakref,)
4187+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4188+
C_with_slots.__bases__ = (A_with_dict,)
4189+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4190+
C_with_slots.__bases__ = (A,)
4191+
C_with_slots.__bases__ = (A_with_slots,)
4192+
self.assertEqual(repr(c), '<A_with_slots>')
4193+
4194+
class C_with_slots_dict(A_with_slots):
4195+
pass
4196+
c = C_with_slots_dict()
4197+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4198+
C_with_slots_dict.__bases__ = (A_with_dict_weakref,)
4199+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4200+
C_with_slots_dict.__bases__ = (A_with_dict,)
4201+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4202+
C_with_slots_dict.__bases__ = (A,)
4203+
C_with_slots_dict.__bases__ = (A_with_slots_dict,)
4204+
self.assertEqual(repr(c), '<A_with_slots_dict>')
4205+
C_with_slots_dict.__bases__ = (A_with_slots,)
4206+
self.assertEqual(repr(c), '<A_with_slots>')
4207+
4208+
class A_int(int):
4209+
__slots__ = ()
4210+
def __repr__(self):
4211+
return '<A_int>'
4212+
class B_int(int):
4213+
__slots__ = ()
4214+
b = B_int(42)
4215+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4216+
B_int.__bases__ = (object,)
4217+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4218+
B_int.__bases__ = (tuple,)
4219+
with self.assertRaisesRegex(TypeError, 'is not an acceptable base type'):
4220+
B_int.__bases__ = (bool,)
4221+
B_int.__bases__ = (A_int,)
4222+
self.assertEqual(repr(b), '<A_int>')
4223+
B_int.__bases__ = (int,)
4224+
self.assertEqual(repr(b), '42')
4225+
4226+
class A_tuple(tuple):
4227+
__slots__ = ()
4228+
def __repr__(self):
4229+
return '<A_tuple>'
4230+
class B_tuple(tuple):
4231+
__slots__ = ()
4232+
b = B_tuple((1, 2))
4233+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4234+
B_tuple.__bases__ = (object,)
4235+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4236+
B_tuple.__bases__ = (int,)
4237+
B_tuple.__bases__ = (A_tuple,)
4238+
self.assertEqual(repr(b), '<A_tuple>')
4239+
B_tuple.__bases__ = (tuple,)
4240+
self.assertEqual(repr(b), '(1, 2)')
41164241

41174242
def test_assign_bases_many_subclasses(self):
41184243
# This is intended to check that typeobject.c:queue_slot_update() can
@@ -4165,26 +4290,14 @@ class C(object):
41654290
class D(C):
41664291
pass
41674292

4168-
try:
4293+
with self.assertRaisesRegex(TypeError, 'layout differs'):
41694294
L.__bases__ = (dict,)
4170-
except TypeError:
4171-
pass
4172-
else:
4173-
self.fail("shouldn't turn list subclass into dict subclass")
41744295

4175-
try:
4296+
with self.assertRaisesRegex(TypeError, 'immutable type'):
41764297
list.__bases__ = (dict,)
4177-
except TypeError:
4178-
pass
4179-
else:
4180-
self.fail("shouldn't be able to assign to list.__bases__")
41814298

4182-
try:
4299+
with self.assertRaisesRegex(TypeError, 'layout differs'):
41834300
D.__bases__ = (C, list)
4184-
except TypeError:
4185-
pass
4186-
else:
4187-
self.fail("best_base calculation found wanting")
41884301

41894302
def test_unsubclassable_types(self):
41904303
with self.assertRaises(TypeError):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Allow assignment to :attr:`~type.__bases__` of direct subclasses of builtin
2+
classes.

Objects/typeobject.c

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1748,7 +1748,7 @@ type_get_mro(PyObject *tp, void *Py_UNUSED(closure))
17481748
static PyTypeObject *find_best_base(PyObject *);
17491749
static int mro_internal(PyTypeObject *, int, PyObject **);
17501750
static int type_is_subtype_base_chain(PyTypeObject *, PyTypeObject *);
1751-
static int compatible_for_assignment(PyTypeObject *, PyTypeObject *, const char *);
1751+
static int compatible_for_assignment(PyTypeObject *, PyTypeObject *, const char *, int);
17521752
static int add_subclass(PyTypeObject*, PyTypeObject*);
17531753
static int add_all_subclasses(PyTypeObject *type, PyObject *bases);
17541754
static void remove_subclass(PyTypeObject *, PyTypeObject *);
@@ -1886,7 +1886,7 @@ type_check_new_bases(PyTypeObject *type, PyObject *new_bases, PyTypeObject **bes
18861886
if (*best_base == NULL)
18871887
return -1;
18881888

1889-
if (!compatible_for_assignment(type->tp_base, *best_base, "__bases__")) {
1889+
if (!compatible_for_assignment(type, *best_base, "__bases__", 0)) {
18901890
return -1;
18911891
}
18921892

@@ -7263,10 +7263,6 @@ compatible_with_tp_base(PyTypeObject *child)
72637263
return (parent != NULL &&
72647264
child->tp_basicsize == parent->tp_basicsize &&
72657265
child->tp_itemsize == parent->tp_itemsize &&
7266-
child->tp_dictoffset == parent->tp_dictoffset &&
7267-
child->tp_weaklistoffset == parent->tp_weaklistoffset &&
7268-
((child->tp_flags & Py_TPFLAGS_HAVE_GC) ==
7269-
(parent->tp_flags & Py_TPFLAGS_HAVE_GC)) &&
72707266
(child->tp_dealloc == subtype_dealloc ||
72717267
child->tp_dealloc == parent->tp_dealloc));
72727268
}
@@ -7301,11 +7297,24 @@ same_slots_added(PyTypeObject *a, PyTypeObject *b)
73017297
}
73027298

73037299
static int
7304-
compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char* attr)
7300+
compatible_flags(int setclass, PyTypeObject *origto, PyTypeObject *newto, unsigned long flags)
7301+
{
7302+
/* For __class__ assignment, the flags should be the same.
7303+
For __bases__ assignment, the new base flags can only be set
7304+
if the original class flags are set.
7305+
*/
7306+
return setclass ? (origto->tp_flags & flags) == (newto->tp_flags & flags)
7307+
: !(~(origto->tp_flags & flags) & (newto->tp_flags & flags));
7308+
}
7309+
7310+
static int
7311+
compatible_for_assignment(PyTypeObject *origto, PyTypeObject *newto,
7312+
const char *attr, int setclass)
73057313
{
73067314
PyTypeObject *newbase, *oldbase;
7315+
PyTypeObject *oldto = setclass ? origto : origto->tp_base;
73077316

7308-
if (newto->tp_free != oldto->tp_free) {
7317+
if (setclass && newto->tp_free != oldto->tp_free) {
73097318
PyErr_Format(PyExc_TypeError,
73107319
"%s assignment: "
73117320
"'%s' deallocator differs from '%s'",
@@ -7314,6 +7323,28 @@ compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char*
73147323
oldto->tp_name);
73157324
return 0;
73167325
}
7326+
if (!compatible_flags(setclass, origto, newto,
7327+
Py_TPFLAGS_HAVE_GC |
7328+
Py_TPFLAGS_INLINE_VALUES |
7329+
Py_TPFLAGS_PREHEADER))
7330+
{
7331+
goto differs;
7332+
}
7333+
/* For __class__ assignment, tp_dictoffset and tp_weaklistoffset should
7334+
be the same for old and new types.
7335+
For __bases__ assignment, they can only be set in the new base
7336+
if they are set in the original class with the same value.
7337+
*/
7338+
if ((setclass || newto->tp_dictoffset)
7339+
&& origto->tp_dictoffset != newto->tp_dictoffset)
7340+
{
7341+
goto differs;
7342+
}
7343+
if ((setclass || newto->tp_weaklistoffset)
7344+
&& origto->tp_weaklistoffset != newto->tp_weaklistoffset)
7345+
{
7346+
goto differs;
7347+
}
73177348
/*
73187349
It's tricky to tell if two arbitrary types are sufficiently compatible as
73197350
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*
73357366
!same_slots_added(newbase, oldbase))) {
73367367
goto differs;
73377368
}
7338-
if ((oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) !=
7339-
((newto->tp_flags & Py_TPFLAGS_INLINE_VALUES)))
7340-
{
7341-
goto differs;
7342-
}
7343-
/* The above does not check for the preheader */
7344-
if ((oldto->tp_flags & Py_TPFLAGS_PREHEADER) ==
7345-
((newto->tp_flags & Py_TPFLAGS_PREHEADER)))
7346-
{
7347-
return 1;
7348-
}
7369+
return 1;
73497370
differs:
73507371
PyErr_Format(PyExc_TypeError,
73517372
"%s assignment: "
@@ -7422,7 +7443,7 @@ object_set_class_world_stopped(PyObject *self, PyTypeObject *newto)
74227443
return -1;
74237444
}
74247445

7425-
if (compatible_for_assignment(oldto, newto, "__class__")) {
7446+
if (compatible_for_assignment(oldto, newto, "__class__", 1)) {
74267447
/* Changing the class will change the implicit dict keys,
74277448
* so we must materialize the dictionary first. */
74287449
if (oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) {

0 commit comments

Comments
 (0)