mirror of
https://github.com/python/cpython.git
synced 2025-12-23 09:19:18 +00:00
gh-37817: Allow assignment to __bases__ of direct subclasses of builtin classes (GH-137585)
This commit is contained in:
parent
811acc85d5
commit
29d026f93e
3 changed files with 202 additions and 66 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
Allow assignment to :attr:`~type.__bases__` of direct subclasses of builtin
|
||||
classes.
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue