Align type layouts and generic descriptors with Python 3.15 (#9175)

Import canonical descriptor tests through update_lib, preserving existing guards. Support tuple member slots, distinguish base reassignment from class layout compatibility, and share object-owned generic descriptors.

Assisted-by: OpenAI Codex:model version unavailable
This commit is contained in:
youknowdot authored and GitHub committed 2026-10-17 14:13:50 +00:00
1 parent fc9f4a9541
commit 75a2417043
5 files changed
+506 -158

No files matched your search

+380 -88
View File
@@ -1329,6 +1329,18 @@ class ClassPropertiesAndMethods(unittest.TestCase):
with self.assertRaisesRegex(AttributeError, "'X' object has no attribute 'a'"):
X().a
def test_slots_after_items(self):
class C(tuple):
__slots__ = ['a']
x = C((1, 2, 3))
self.assertNotHasAttr(x, "__dict__")
self.assertNotHasAttr(x, "a")
x.a = 42
self.assertEqual(x.a, 42)
del x.a
self.assertNotHasAttr(x, "a")
self.assertEqual(x, (1, 2, 3))
def test_slots_special(self):
# Testing __dict__ and __weakref__ in __slots__...
class D(object):
@@ -1338,18 +1350,17 @@ class ClassPropertiesAndMethods(unittest.TestCase):
self.assertNotHasAttr(a, "__weakref__")
a.foo = 42
self.assertEqual(a.__dict__, {"foo": 42})
with self.assertRaises(TypeError):
weakref.ref(a)
class W(object):
__slots__ = ["__weakref__"]
a = W()
self.assertHasAttr(a, "__weakref__")
self.assertNotHasAttr(a, "__dict__")
try:
with self.assertRaises(AttributeError):
a.foo = 42
except AttributeError:
pass
else:
self.fail("shouldn't be allowed to set a.foo")
self.assertIs(weakref.ref(a)(), a)
class C1(W, D):
__slots__ = []
@@ -1358,6 +1369,7 @@ class ClassPropertiesAndMethods(unittest.TestCase):
self.assertHasAttr(a, "__weakref__")
a.foo = 42
self.assertEqual(a.__dict__, {"foo": 42})
self.assertIs(weakref.ref(a)(), a)
class C2(D, W):
__slots__ = []
@@ -1366,6 +1378,80 @@ class ClassPropertiesAndMethods(unittest.TestCase):
self.assertHasAttr(a, "__weakref__")
a.foo = 42
self.assertEqual(a.__dict__, {"foo": 42})
self.assertIs(weakref.ref(a)(), a)
@unittest.skipIf(_testcapi is None, 'need the _testcapi module')
def test_slots_special_before_items(self):
class D(_testcapi.HeapCCollection):
__slots__ = ["__dict__"]
a = D(1, 2, 3)
self.assertHasAttr(a, "__dict__")
self.assertNotHasAttr(a, "__weakref__")
a.foo = 42
self.assertEqual(a.__dict__, {"foo": 42})
with self.assertRaises(TypeError):
weakref.ref(a)
del a.__dict__
self.assertNotHasAttr(a, "foo")
self.assertEqual(a.__dict__, {})
self.assertEqual(list(a), [1, 2, 3])
class W(_testcapi.HeapCCollection):
__slots__ = ["__weakref__"]
a = W(1, 2, 3)
self.assertHasAttr(a, "__weakref__")
self.assertNotHasAttr(a, "__dict__")
with self.assertRaises(AttributeError):
a.foo = 42
self.assertIs(weakref.ref(a)(), a)
with self.assertRaises(TypeError):
class X(_testcapi.HeapCCollection):
__slots__ = ['x']
with self.assertRaises(TypeError):
class X(_testcapi.HeapCCollection):
__slots__ = ['__dict__', 'x']
@support.subTests(('base', 'arg'), [
(tuple, (1, 2, 3)),
(int, 9876543210**2),
(bytes, b'ab'),
])
def test_slots_special_after_items(self, base, arg):
class D(base):
__slots__ = ["__dict__"]
a = D(arg)
self.assertHasAttr(a, "__dict__")
self.assertNotHasAttr(a, "__weakref__")
a.foo = 42
self.assertEqual(a.__dict__, {"foo": 42})
with self.assertRaises(TypeError):
weakref.ref(a)
del a.__dict__
self.assertNotHasAttr(a, "foo")
self.assertEqual(a.__dict__, {})
self.assertEqual(a, base(arg))
class W(base):
__slots__ = ["__weakref__"]
a = W(arg)
self.assertHasAttr(a, "__weakref__")
self.assertNotHasAttr(a, "__dict__")
with self.assertRaises(AttributeError):
a.foo = 42
self.assertIs(weakref.ref(a)(), a)
self.assertEqual(a, base(arg))
@support.subTests('base', [int, bytes] +
([_testcapi.HeapCCollection] if _testcapi else []))
def test_unsupported_slots(self, base):
with self.assertRaises(TypeError):
class X(base):
__slots__ = ['x']
with self.assertRaises(TypeError):
class X(base):
__slots__ = ['__dict__', 'x']
def test_slots_special2(self):
# Testing __qualname__ and __classcell__ in __slots__
@@ -1650,6 +1736,18 @@ class ClassPropertiesAndMethods(unittest.TestCase):
del method.__annotate__
self.assertIs(method.__annotate__, original_annotate)
def test_classmethod_without_dict_access(self):
class Spam:
@classmethod
def method(cls, x, y):
pass
obj = Spam.__dict__['method']
self.assertIsInstance(obj, classmethod)
self.assertEqual(obj.__annotations__, {})
self.assertEqual(obj.__name__, 'method')
self.assertEqual(obj.__module__, __name__)
def test_staticmethod_annotations_without_dict_access(self):
# gh-125017: this used to crash
class Spam:
@@ -1660,15 +1758,8 @@ class ClassPropertiesAndMethods(unittest.TestCase):
obj = Spam.__dict__['__new__']
self.assertIsInstance(obj, staticmethod)
self.assertEqual(obj.__annotations__, {})
@support.refcount_test
def test_refleaks_in_classmethod___init__(self):
gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
cm = classmethod(None)
refs_before = gettotalrefcount()
for i in range(100):
cm.__init__(None)
self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)
self.assertEqual(obj.__name__, '__new__')
self.assertEqual(obj.__module__, __name__)
@support.impl_detail("the module 'xxsubtype' is internal")
@unittest.skipIf(xxsubtype is None, "requires xxsubtype module")
@@ -1767,15 +1858,6 @@ class ClassPropertiesAndMethods(unittest.TestCase):
del sm.x
self.assertNotHasAttr(sm, "x")
@support.refcount_test
def test_refleaks_in_staticmethod___init__(self):
gettotalrefcount = support.get_attribute(sys, 'gettotalrefcount')
sm = staticmethod(None)
refs_before = gettotalrefcount()
for i in range(100):
sm.__init__(None)
self.assertAlmostEqual(gettotalrefcount() - refs_before, 0, delta=10)
@support.impl_detail("the module 'xxsubtype' is internal")
@unittest.skipIf(xxsubtype is None, "requires xxsubtype module")
def test_staticmethods_in_c(self):
@@ -4111,42 +4193,195 @@ class ClassPropertiesAndMethods(unittest.TestCase):
self.assertEqual(e.a, 2)
self.assertEqual(C2.__subclasses__(), [D])
try:
with self.assertRaisesRegex(TypeError,
"cannot delete '__bases__' attribute of type 'D'"):
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
# handle updating many subclasses when a slot method is re-assigned.
class A:
x = 'hello'
def __call__(self):
return 123
def __getitem__(self, index):
return None
class X:
x = 'bye'
class B(A):
pass
subclasses = []
for i in range(1000):
sc = type(f'Sub{i}', (B,), {})
subclasses.append(sc)
self.assertEqual(subclasses[0]()(), 123)
self.assertEqual(subclasses[0]().x, 'hello')
B.__bases__ = (X,)
with self.assertRaises(TypeError):
subclasses[0]()()
self.assertEqual(subclasses[0]().x, 'bye')
def test_builtin_bases(self):
# Make sure all the builtin types can have their base queried without
@@ -4171,26 +4406,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):
@@ -4956,7 +5179,7 @@ class ClassPropertiesAndMethods(unittest.TestCase):
with self.assertRaises(TypeError) as cm:
type(X).__dict__["__doc__"].__delete__(X)
self.assertIn("cannot delete '__doc__' attribute of immutable type 'X'", str(cm.exception))
self.assertIn("cannot delete '__doc__' attribute of type 'X'", str(cm.exception))
self.assertEqual(X.__doc__, "banana")
def test_qualname(self):
@@ -5173,25 +5396,30 @@ class ClassPropertiesAndMethods(unittest.TestCase):
with self.assertRaisesRegex(NotImplementedError, "BAR"):
B().foo
def test_staticmethod_new(self):
class MyStaticMethod(staticmethod):
def __init__(self, func):
pass
def func(): pass
sm = MyStaticMethod(func)
self.assertEqual(repr(sm), '<staticmethod(None)>')
self.assertIsNone(sm.__func__)
self.assertIsNone(sm.__wrapped__)
def test_gh146587(self):
# See https://github.com/python/cpython/issues/146587
def test_classmethod_new(self):
class MyClassMethod(classmethod):
def __init__(self, func):
pass
def func(): pass
cm = MyClassMethod(func)
self.assertEqual(repr(cm), '<classmethod(None)>')
self.assertIsNone(cm.__func__)
self.assertIsNone(cm.__wrapped__)
class A:
def __radd__(self, other): ...
class B(tuple): ...
self.assertIsNone(() + A())
self.assertIsNone(B() + A())
from typing import NamedTuple
class T(NamedTuple):
x: int
class A:
def __init__(self, *args):
self.lst = list(args)
def __radd__(self, other):
return A(*self.lst, other)
self.assertEqual(((1,)+A()).lst, [(1,)])
self.assertEqual((T(x=1)+A()).lst, [T(x=1)])
class DictProxyTests(unittest.TestCase):
@@ -6040,5 +6268,69 @@ class MroTest(unittest.TestCase):
pass
class TestGenericDescriptors(unittest.TestCase):
def test___dict__(self):
class CustomClass:
pass
class SlotClass:
__slots__ = ['foo']
class SlotSubClass(SlotClass):
pass
class IntSubclass(int):
pass
dict_descriptor = CustomClass.__dict__['__dict__']
self.assertEqual(dict_descriptor.__objclass__, object)
for cls in CustomClass, SlotSubClass, IntSubclass:
with self.subTest(cls=cls):
self.assertIs(cls.__dict__['__dict__'], dict_descriptor)
instance = cls()
instance.attr = 123
self.assertEqual(
dict_descriptor.__get__(instance, cls),
{'attr': 123},
)
with self.assertRaises(AttributeError):
print(dict_descriptor.__get__(True, bool))
with self.assertRaises(AttributeError):
print(dict_descriptor.__get__(SlotClass(), SlotClass))
# delegation to type.__dict__
self.assertIsInstance(
dict_descriptor.__get__(type, type),
types.MappingProxyType,
)
def test___weakref__(self):
class CustomClass:
pass
class SlotClass:
__slots__ = ['foo']
class SlotSubClass(SlotClass):
pass
class IntSubclass(int):
pass
weakref_descriptor = CustomClass.__dict__['__weakref__']
self.assertEqual(weakref_descriptor.__objclass__, object)
for cls in CustomClass, SlotSubClass:
with self.subTest(cls=cls):
self.assertIs(cls.__dict__['__weakref__'], weakref_descriptor)
instance = cls()
instance.attr = 123
self.assertEqual(
weakref_descriptor.__get__(instance, cls),
None,
)
with self.assertRaises(AttributeError):
weakref_descriptor.__get__(True, bool)
with self.assertRaises(AttributeError):
weakref_descriptor.__get__(SlotClass(), SlotClass)
with self.assertRaises(AttributeError):
weakref_descriptor.__get__(IntSubclass(), IntSubclass)
if __name__ == "__main__":
unittest.main()
+7 -1
View File
@@ -460,7 +460,13 @@ impl PyBaseObject {
&& !cls.slots.flags.has_feature(PyTypeFlags::IMMUTABLETYPE);
// FIXME(#1979) cls instances might have a payload
if both_mutable || both_module {
super::type_::compatible_for_assignment(current_cls, &cls, "__class__", vm)?;
super::type_::compatible_for_assignment(
current_cls,
&cls,
"__class__",
true,
vm,
)?;
instance.set_class(cls, vm);
crate::stdlib::_testinternalcapi::note_set_class();
Ok(())
+116 -69
View File
@@ -47,6 +47,32 @@ use std::collections::HashSet;
pub(crate) type PyTypeTupleRef = PyRef<PyTuple<PyTypeRef>>;
pub(crate) struct CommonTypeDescriptors {
dict: PyRef<super::PyGetSet>,
weakref: PyRef<super::PyGetSet>,
}
impl CommonTypeDescriptors {
pub(crate) fn new(ctx: &Context) -> Self {
// These descriptors operate on the instance's layout, not the class
// that first exposes them. Keeping object as their owner also lets an
// annotation closure retain a class namespace without retaining the class.
let dict = super::PyGetSet::new("__dict__", ctx.types.object_type, ctx)
.with_get(subtype_get_dict)
.with_set(subtype_set_dict)
.with_doc(ItemDoc::static_text("dictionary for instance variables"));
let weakref = super::PyGetSet::new("__weakref__", ctx.types.object_type, ctx)
.with_get(subtype_get_weakref)
.with_doc(ItemDoc::static_text(
"list of weak references to the object",
));
Self {
dict: ctx.new_pyref(dict),
weakref: ctx.new_pyref(weakref),
}
}
}
#[pyclass(module = false, name = "type", traverse = "manual")]
pub struct PyType {
// tp_base. Written under the type lock (see `set_bases`); read lock-free.
@@ -1841,11 +1867,11 @@ impl Constructor for PyType {
// Search the bases for the proper metatype to deal with this:
let winner = calculate_meta_class(metatype.clone(), bases.as_slice(), vm)?;
let metatype = if !winner.is(&metatype) {
if let Some(ref slot_new) = winner.slots.new.load() {
// Pass it to the winner
return slot_new(winner, args, vm);
}
winner
let slot_new = winner.slots.new.load().ok_or_else(|| {
vm.new_type_error(format!("cannot create '{}' instances", winner.slot_name()))
})?;
// Pass it to the winner.
return slot_new(winner, args, vm);
} else {
metatype
};
@@ -1974,16 +2000,6 @@ impl Constructor for PyType {
tuple.try_into_typed(vm)?
};
// Any nonempty __slots__ is rejected when the base has a variable
// item size, including a tuple of only `__dict__` or `__weakref__`.
// Types like weakref.ref have itemsize 0 and do allow slots.
if !slots.as_slice().is_empty() && base.slots.itemsize > 0 {
return Err(vm.new_type_error(format!(
"nonempty __slots__ not supported for subtype of '{}'",
base.name()
)));
}
// Validate slot names and track duplicates
let mut seen_dict = false;
let mut seen_weakref = false;
@@ -2055,6 +2071,19 @@ impl Constructor for PyType {
.cloned()
.collect();
filtered.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
// CPython 3.15 permits named slots on tuple subclasses. Our
// member cells live in the object prefix, independently of the
// tuple's elements. Other variable-size bases still only accept
// the managed __dict__ and __weakref__ slots filtered above.
if !filtered.is_empty()
&& base.slots.itemsize > 0
&& !base.fast_issubclass(vm.ctx.types.tuple_type)
{
return Err(vm.new_type_error(format!(
"arbitrary __slots__ not supported for subtype of '{}'",
base.name()
)));
}
let filtered_slots = PyTuple::new_ref_typed(filtered, &vm.ctx);
(Some(filtered_slots), has_dict, add_weakref)
@@ -2222,7 +2251,7 @@ impl Constructor for PyType {
// Also, type subclasses don't need their own __dict__ descriptor
// since they inherit it from type
// Add __dict__ descriptor after type creation to ensure correct __objclass__
// Share the interpreter's object-owned __dict__ descriptor.
// Only add if:
// 1. base is not type (type subclasses inherit __dict__ from type)
// 2. the class has HAS_DICT flag (i.e., __slots__ was not defined or __dict__ is in __slots__)
@@ -2235,12 +2264,10 @@ impl Constructor for PyType {
.iter()
.any(|base| base.attributes.contains(__dict__));
if !typ.attributes.contains(__dict__) && !has_inherited_dict {
let getset = super::PyGetSet::new("__dict__", &typ, &vm.ctx)
.with_get(subtype_get_dict)
.with_set(subtype_set_dict)
.with_doc(ItemDoc::static_text("dictionary for instance variables"));
let descriptor = PyRef::new_ref(getset, vm.ctx.types.getset_type.to_owned(), None);
typ.attributes.set(__dict__, descriptor.into());
typ.attributes.set(
__dict__,
vm.state.common_type_descriptors.dict.clone().into(),
);
}
}
@@ -2253,14 +2280,10 @@ impl Constructor for PyType {
.iter()
.any(|base| base.attributes.contains(__weakref__));
if !typ.attributes.contains(__weakref__) && !has_inherited_weakref {
let getset = super::PyGetSet::new("__weakref__", &typ, &vm.ctx)
.with_get(subtype_get_weakref)
.with_set(subtype_set_weakref)
.with_doc(ItemDoc::static_text(
"list of weak references to the object",
));
let descriptor = PyRef::new_ref(getset, vm.ctx.types.getset_type.to_owned(), None);
typ.attributes.set(__weakref__, descriptor.into());
typ.attributes.set(
__weakref__,
vm.state.common_type_descriptors.weakref.clone().into(),
);
}
}
@@ -2488,8 +2511,8 @@ impl Py<PyType> {
// Similar to CPython's type_set_doc
let value = value.ok_or_else(|| {
vm.new_type_error(format!(
"cannot delete '__doc__' attribute of immutable type '{}'",
self.name()
"cannot delete '__doc__' attribute of type '{}'",
self.slot_name()
))
})?;
@@ -2545,15 +2568,23 @@ impl Py<PyType> {
PyType::with_type_lock(vm, || self.bases.read().clone().into_untyped())
}
#[pygetset(setter, name = "__bases__")]
fn set_bases(zelf: &Self, bases_tuple: PyTupleRef, vm: &VirtualMachine) -> PyResult<()> {
fn set_bases(zelf: &Self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> {
// TODO: Assigning to __bases__ is only used in typing.NamedTupleMeta.__new__
// Rather than correctly re-initializing the class, we are skipping a few steps for now
if zelf.slots().flags.has_feature(PyTypeFlags::IMMUTABLETYPE) {
return Err(vm.new_type_error(format!(
"cannot set '__bases__' attribute of immutable type '{}'",
zelf.name()
)));
}
let value = value.ok_or_else(|| {
vm.new_type_error(format!(
"cannot delete '__bases__' attribute of type '{}'",
zelf.slot_name()
))
})?;
zelf.check_set_special_type_attr(identifier!(vm, __bases__), vm)?;
let bases_tuple = value.downcast::<PyTuple>().map_err(|value| {
vm.new_type_error(format!(
"can only assign tuple to {}.__bases__, not {}",
zelf.slot_name(),
value.class().slot_name()
))
})?;
if bases_tuple.as_slice().is_empty() {
return Err(vm.new_type_error(format!(
"can only assign non-empty tuple to {}.__bases__, not ()",
@@ -2578,11 +2609,7 @@ impl Py<PyType> {
// Reject reparenting onto a base whose instances have an incompatible
// object layout.
let old_base_owned = zelf.base.load_owned();
let old_base = old_base_owned
.as_deref()
.unwrap_or(vm.ctx.types.object_type);
compatible_for_assignment(old_base, &new_base, "__bases__", vm)?;
compatible_for_assignment(zelf, &new_base, "__bases__", false, vm)?;
// References released inside the critical section are collected here
// and dropped after the lock: dropping them inside can run arbitrary
@@ -3382,18 +3409,18 @@ fn subtype_set_dict(obj: PyObjectRef, value: PySetterValue, vm: &VirtualMachine)
}
// subtype_get_weakref
fn subtype_get_weakref(obj: PyObjectRef, vm: &VirtualMachine) -> PyObjectRef {
fn subtype_get_weakref(obj: PyObjectRef, vm: &VirtualMachine) -> PyResult {
if !obj
.class()
.slots
.flags
.has_feature(PyTypeFlags::HAS_WEAKREF)
{
return Err(vm.new_attribute_error("This object has no __weakref__"));
}
// Return the first weakref in the weakref list, or None
let weakref = obj.get_weakrefs();
weakref.unwrap_or_else(|| vm.ctx.none())
}
// subtype_set_weakref: __weakref__ is read-only
fn subtype_set_weakref(obj: PyObjectRef, _value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> {
Err(vm.new_attribute_error(format!(
"attribute '__weakref__' of '{}' objects is not writable",
obj.class().name()
)))
Ok(weakref.unwrap_or_else(|| vm.ctx.none()))
}
/*
@@ -3738,20 +3765,21 @@ fn type_has_weakref(typ: &Py<PyType>) -> bool {
/// Returns true if `child` adds no instance layout of its own beyond its base,
/// so the base can stand in for it when comparing object layouts.
fn compatible_with_base(child: &Py<PyType>) -> bool {
fn compatible_with_base(child: &Py<PyType>, set_class: bool) -> bool {
let Some(parent) = child.base.deref() else {
return false;
};
child.slots.basicsize == parent.slots.basicsize
&& child.slots.itemsize == parent.slots.itemsize
&& child.slots.member_count == parent.slots.member_count
&& type_has_dict(child) == type_has_dict(parent)
&& type_has_weakref(child) == type_has_weakref(parent)
&& (!set_class
|| (type_has_dict(child) == type_has_dict(parent)
&& type_has_weakref(child) == type_has_weakref(parent)))
}
/// Walk up to the most derived base that actually fixes the instance layout.
fn layout_solid_base(mut typ: &Py<PyType>) -> &Py<PyType> {
while compatible_with_base(typ) {
fn layout_solid_base(mut typ: &Py<PyType>, set_class: bool) -> &Py<PyType> {
while compatible_with_base(typ, set_class) {
typ = typ.base.deref().unwrap();
}
typ
@@ -3759,12 +3787,12 @@ fn layout_solid_base(mut typ: &Py<PyType>) -> &Py<PyType> {
/// Returns true if `a` and `b`, which share the same base, added the same
/// instance layout (`__dict__`, `__weakref__`, and `__slots__`).
fn same_slots_added(a: &Py<PyType>, b: &Py<PyType>) -> bool {
fn same_slots_added(a: &Py<PyType>, b: &Py<PyType>, set_class: bool) -> bool {
if a.slots.basicsize != b.slots.basicsize
|| a.slots.itemsize != b.slots.itemsize
|| a.slots.member_count != b.slots.member_count
|| type_has_dict(a) != type_has_dict(b)
|| type_has_weakref(a) != type_has_weakref(b)
|| (set_class
&& (type_has_dict(a) != type_has_dict(b) || type_has_weakref(a) != type_has_weakref(b)))
{
return false;
}
@@ -3783,25 +3811,44 @@ fn same_slots_added(a: &Py<PyType>, b: &Py<PyType>) -> bool {
}
}
/// Validates that instances of `old_to` and `new_to` share an interchangeable
/// object layout, the check `__class__` and `__bases__` assignment perform.
/// Validates the layout for `__class__` or `__bases__` assignment. For a base
/// change, `original` is the existing class whose allocation must remain valid.
///
/// `attr` names the attribute being assigned for the error message; the
/// message reports `new_to` first and `old_to` second.
/// message reports the new type first and the replaced type or base second.
pub(crate) fn compatible_for_assignment(
old_to: &Py<PyType>,
original: &Py<PyType>,
new_to: &Py<PyType>,
attr: &str,
set_class: bool,
vm: &VirtualMachine,
) -> PyResult<()> {
let newbase = layout_solid_base(new_to);
let oldbase = layout_solid_base(old_to);
let old_base = if set_class {
None
} else {
original.base.load_owned()
};
let old_to = old_base.as_deref().unwrap_or(original);
// Reparenting leaves the class's prefix allocation unchanged. A new base
// may use its existing dict/weakref storage, but cannot require storage
// the class never allocated. Payload size and member slots are checked
// separately against the old base, not against the class's own additions.
let storage_compatible = set_class
|| ((!type_has_dict(new_to) || type_has_dict(original))
&& (!type_has_weakref(new_to) || type_has_weakref(original)));
let newbase = layout_solid_base(new_to, set_class);
let oldbase = layout_solid_base(old_to, set_class);
let bases_equal = match (newbase.base.deref(), oldbase.base.deref()) {
(Some(x), Some(y)) => x.is(y),
(None, None) => true,
_ => false,
};
let compatible = newbase.is(oldbase) || (bases_equal && same_slots_added(newbase, oldbase));
let compatible = storage_compatible
&& (newbase.is(oldbase)
|| (bases_equal
&& (set_class
|| (newbase.heaptype_ext.is_some() && oldbase.heaptype_ext.is_some()))
&& same_slots_added(newbase, oldbase, set_class)));
if compatible {
return Ok(());
}
+1
View File
@@ -175,6 +175,7 @@ where
global_trace_func: PyMutex::default(),
global_profile_func: PyMutex::default(),
type_mutex: PyMutex::default(),
common_type_descriptors: crate::builtins::type_::CommonTypeDescriptors::new(&ctx),
#[cfg(feature = "threading")]
main_thread_ident,
#[cfg(feature = "threading")]
+2
View File
@@ -886,6 +886,8 @@ pub struct PyGlobalState {
pub global_profile_func: PyMutex<Option<PyObjectRef>>,
/// Global type mutation/versioning mutex for CPython-style FT type operations.
pub type_mutex: PyMutex<()>,
/// Object-owned descriptors shared by heap types in this interpreter.
pub(crate) common_type_descriptors: builtins::type_::CommonTypeDescriptors,
/// Main thread identifier (pthread_self on Unix)
#[cfg(feature = "threading")]
pub main_thread_ident: AtomicCell<u64>,