Skip to content

Commit 8f83a0b

Browse files
gh-37817: Allow assignment to __bases__ of direct subclasses of builtin classes
1 parent 34d7351 commit 8f83a0b

File tree

3 files changed

+128
-75
lines changed

3 files changed

+128
-75
lines changed

Lib/test/test_descr.py

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4077,42 +4077,82 @@ 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:
4096-
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")
4102-
4103-
try:
4085+
with self.assertRaisesRegex(TypeError, 'can only assign tuple'):
4086+
D.__bases__ = [C]
4087+
with self.assertRaisesRegex(TypeError, 'duplicate base class'):
41044088
D.__bases__ = (C, C)
4105-
except TypeError:
4106-
pass
4107-
else:
4108-
self.fail("didn't detect repeated base classes")
4109-
4110-
try:
4089+
with self.assertRaisesRegex(TypeError, 'inheritance cycle'):
4090+
D.__bases__ = (D,)
4091+
with self.assertRaisesRegex(TypeError, 'inheritance cycle'):
41114092
D.__bases__ = (E,)
4112-
except TypeError:
4113-
pass
4114-
else:
4115-
self.fail("shouldn't be able to create inheritance cycles")
4093+
4094+
class A:
4095+
__slots__ = ()
4096+
def __repr__(self):
4097+
return '<A>'
4098+
class B:
4099+
__slots__ = ()
4100+
b = B()
4101+
r = repr(b)
4102+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4103+
B.__bases__ = (int,)
4104+
B.__bases__ = (A,)
4105+
self.assertNotHasAttr(b, '__dict__')
4106+
self.assertNotHasAttr(b, '__weakref__')
4107+
self.assertEqual(repr(b), '<A>')
4108+
B.__bases__ = (object,)
4109+
self.assertEqual(repr(b), r)
4110+
4111+
class A_with_dict:
4112+
pass
4113+
class B_with_dict:
4114+
pass
4115+
b = B_with_dict()
4116+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4117+
B_with_dict.__bases__ = (A_with_dict,)
4118+
B_with_dict.__bases__ = (A,)
4119+
self.assertHasAttr(b, '__dict__')
4120+
self.assertHasAttr(b, '__weakref__')
4121+
B_with_dict.__bases__ = (object,)
4122+
4123+
class A_int(int):
4124+
__slots__ = ()
4125+
def __repr__(self):
4126+
return '<A_int>'
4127+
class B_int(int):
4128+
__slots__ = ()
4129+
b = B_int(42)
4130+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4131+
B_int.__bases__ = (object,)
4132+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4133+
B_int.__bases__ = (tuple,)
4134+
with self.assertRaisesRegex(TypeError, 'is not an acceptable base type'):
4135+
B_int.__bases__ = (bool,)
4136+
B_int.__bases__ = (A_int,)
4137+
self.assertEqual(repr(b), '<A_int>')
4138+
B_int.__bases__ = (int,)
4139+
self.assertEqual(repr(b), '42')
4140+
4141+
class A_tuple(tuple):
4142+
__slots__ = ()
4143+
def __repr__(self):
4144+
return '<A_tuple>'
4145+
class B_tuple(tuple):
4146+
__slots__ = ()
4147+
b = B_tuple((1, 2))
4148+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4149+
B_tuple.__bases__ = (object,)
4150+
with self.assertRaisesRegex(TypeError, 'layout differs'):
4151+
B_tuple.__bases__ = (int,)
4152+
B_tuple.__bases__ = (A_tuple,)
4153+
self.assertEqual(repr(b), '<A_tuple>')
4154+
B_tuple.__bases__ = (tuple,)
4155+
self.assertEqual(repr(b), '(1, 2)')
41164156

41174157
def test_assign_bases_many_subclasses(self):
41184158
# This is intended to check that typeobject.c:queue_slot_update() can
@@ -4165,26 +4205,14 @@ class C(object):
41654205
class D(C):
41664206
pass
41674207

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

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

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

41894217
def test_unsubclassable_types(self):
41904218
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: 51 additions & 28 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

@@ -7230,8 +7230,6 @@ compatible_with_tp_base(PyTypeObject *child)
72307230
child->tp_itemsize == parent->tp_itemsize &&
72317231
child->tp_dictoffset == parent->tp_dictoffset &&
72327232
child->tp_weaklistoffset == parent->tp_weaklistoffset &&
7233-
((child->tp_flags & Py_TPFLAGS_HAVE_GC) ==
7234-
(parent->tp_flags & Py_TPFLAGS_HAVE_GC)) &&
72357233
(child->tp_dealloc == subtype_dealloc ||
72367234
child->tp_dealloc == parent->tp_dealloc));
72377235
}
@@ -7243,34 +7241,47 @@ same_slots_added(PyTypeObject *a, PyTypeObject *b)
72437241
Py_ssize_t size;
72447242
PyObject *slots_a, *slots_b;
72457243

7246-
assert(base == b->tp_base);
7244+
// assert(base == b->tp_base);
72477245
size = base->tp_basicsize;
72487246
if (a->tp_dictoffset == size && b->tp_dictoffset == size)
72497247
size += sizeof(PyObject *);
72507248
if (a->tp_weaklistoffset == size && b->tp_weaklistoffset == size)
72517249
size += sizeof(PyObject *);
72527250

72537251
/* Check slots compliance */
7254-
if (!(a->tp_flags & Py_TPFLAGS_HEAPTYPE) ||
7255-
!(b->tp_flags & Py_TPFLAGS_HEAPTYPE)) {
7256-
return 0;
7252+
slots_a = (a->tp_flags & Py_TPFLAGS_HEAPTYPE) ? ((PyHeapTypeObject *)a)->ht_slots : NULL;
7253+
slots_b = (b->tp_flags & Py_TPFLAGS_HEAPTYPE) ? ((PyHeapTypeObject *)b)->ht_slots : NULL;
7254+
if (!slots_a) {
7255+
slots_a = (PyObject *)&_Py_SINGLETON(tuple_empty);
72577256
}
7258-
slots_a = ((PyHeapTypeObject *)a)->ht_slots;
7259-
slots_b = ((PyHeapTypeObject *)b)->ht_slots;
7260-
if (slots_a && slots_b) {
7261-
if (PyObject_RichCompareBool(slots_a, slots_b, Py_EQ) != 1)
7262-
return 0;
7263-
size += sizeof(PyObject *) * PyTuple_GET_SIZE(slots_a);
7257+
if (!slots_b) {
7258+
slots_b = (PyObject *)&_Py_SINGLETON(tuple_empty);
72647259
}
7260+
if (PyObject_RichCompareBool(slots_a, slots_b, Py_EQ) != 1)
7261+
return 0;
7262+
size += sizeof(PyObject *) * PyTuple_GET_SIZE(slots_a);
72657263
return size == a->tp_basicsize && size == b->tp_basicsize;
72667264
}
72677265

72687266
static int
7269-
compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char* attr)
7267+
compatible_flags(int setclass, PyTypeObject *origto, PyTypeObject *newto, unsigned long flags)
7268+
{
7269+
/* For __class__ assignment, the flags should be the same.
7270+
For __bases__ assignment, the new base flags can only be set
7271+
if the original class flags are set.
7272+
*/
7273+
return setclass ? (origto->tp_flags & flags) == (newto->tp_flags & flags)
7274+
: !(~(origto->tp_flags & flags) & (newto->tp_flags & flags));
7275+
}
7276+
7277+
static int
7278+
compatible_for_assignment(PyTypeObject *origto, PyTypeObject *newto,
7279+
const char *attr, int setclass)
72707280
{
72717281
PyTypeObject *newbase, *oldbase;
7282+
PyTypeObject *oldto = setclass ? origto : origto->tp_base;
72727283

7273-
if (newto->tp_free != oldto->tp_free) {
7284+
if (setclass && newto->tp_free != oldto->tp_free) {
72747285
PyErr_Format(PyExc_TypeError,
72757286
"%s assignment: "
72767287
"'%s' deallocator differs from '%s'",
@@ -7279,6 +7290,28 @@ compatible_for_assignment(PyTypeObject* oldto, PyTypeObject* newto, const char*
72797290
oldto->tp_name);
72807291
return 0;
72817292
}
7293+
if (!compatible_flags(setclass, origto, newto,
7294+
Py_TPFLAGS_HAVE_GC |
7295+
Py_TPFLAGS_INLINE_VALUES |
7296+
Py_TPFLAGS_PREHEADER))
7297+
{
7298+
goto differs;
7299+
}
7300+
/* For __class__ assignment, tp_dictoffset and tp_weaklistoffset should
7301+
be the same for old and new types.
7302+
For __bases__ assignment, they can only be set in the new base
7303+
if they are set in the original class with the same value.
7304+
*/
7305+
if ((setclass || newto->tp_dictoffset)
7306+
&& origto->tp_dictoffset != newto->tp_dictoffset)
7307+
{
7308+
goto differs;
7309+
}
7310+
if ((setclass || newto->tp_weaklistoffset)
7311+
&& origto->tp_weaklistoffset != newto->tp_weaklistoffset)
7312+
{
7313+
goto differs;
7314+
}
72827315
/*
72837316
It's tricky to tell if two arbitrary types are sufficiently compatible as
72847317
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*
73007333
!same_slots_added(newbase, oldbase))) {
73017334
goto differs;
73027335
}
7303-
if ((oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) !=
7304-
((newto->tp_flags & Py_TPFLAGS_INLINE_VALUES)))
7305-
{
7306-
goto differs;
7307-
}
7308-
/* The above does not check for the preheader */
7309-
if ((oldto->tp_flags & Py_TPFLAGS_PREHEADER) ==
7310-
((newto->tp_flags & Py_TPFLAGS_PREHEADER)))
7311-
{
7312-
return 1;
7313-
}
7336+
return 1;
73147337
differs:
73157338
PyErr_Format(PyExc_TypeError,
73167339
"%s assignment: "
@@ -7387,7 +7410,7 @@ object_set_class_world_stopped(PyObject *self, PyTypeObject *newto)
73877410
return -1;
73887411
}
73897412

7390-
if (compatible_for_assignment(oldto, newto, "__class__")) {
7413+
if (compatible_for_assignment(oldto, newto, "__class__", 1)) {
73917414
/* Changing the class will change the implicit dict keys,
73927415
* so we must materialize the dictionary first. */
73937416
if (oldto->tp_flags & Py_TPFLAGS_INLINE_VALUES) {

0 commit comments

Comments
 (0)