Skip to content

gh-37817: Allow assignment to __bases__ of direct subclasses of builtin classes #137585

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 159 additions & 46 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -4077,42 +4077,167 @@ 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:
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
Expand Down Expand Up @@ -4165,26 +4290,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):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Allow assignment to :attr:`~type.__bases__` of direct subclasses of builtin
classes.
61 changes: 41 additions & 20 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 *);
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -7228,10 +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_flags & Py_TPFLAGS_HAVE_GC) ==
(parent->tp_flags & Py_TPFLAGS_HAVE_GC)) &&
(child->tp_dealloc == subtype_dealloc ||
child->tp_dealloc == parent->tp_dealloc));
}
Expand Down Expand Up @@ -7266,11 +7262,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'",
Expand All @@ -7279,6 +7288,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
Expand All @@ -7300,17 +7331,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: "
Expand Down Expand Up @@ -7387,7 +7408,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) {
Expand Down
Loading