From c0eae2caff6b4dc36602aad4a4f8240cad7577f7 Mon Sep 17 00:00:00 2001 From: youknowdot Date: Sat, 17 Oct 2026 21:30:10 +0900 Subject: [PATCH] Align type layouts and generic descriptors with Python 3.15 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 --- Lib/test/test_descr.py | 468 +++++++++++++++++++++++++------ crates/vm/src/builtins/object.rs | 8 +- crates/vm/src/builtins/type.rs | 185 +++++++----- crates/vm/src/vm/interpreter.rs | 1 + crates/vm/src/vm/mod.rs | 2 + 5 files changed, 506 insertions(+), 158 deletions(-) diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 3923412b9a0..ca9861e1994 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -1329,6 +1329,18 @@ class X(object): 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 D(object): 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 C1(W, D): 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 C2(D, W): 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 @@ def annotated(cls) -> int: pass 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 @@ def __new__(cls, x, y): 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 D(C): 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 E(D): 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: + 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 '' + class A_with_dict: + __slots__ = ('__dict__',) + def __repr__(self): + return '' + class A_with_dict_weakref: + def __repr__(self): + return '' + class A_with_slots: + __slots__ = ('x',) + def __repr__(self): + return '' + class A_with_slots_dict: + __slots__ = ('x', '__dict__') + def __repr__(self): + return '' - try: - D.__bases__ = (E,) - except TypeError: + 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), '') + 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), '') + B_with_dict_weakref.__bases__ = (A_with_dict,) + self.assertEqual(repr(b), '') + B_with_dict_weakref.__bases__ = (A,) + self.assertEqual(repr(b), '') + 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), '') + + 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), '') + B_with_slots_dict.__bases__ = (A,) + self.assertEqual(repr(b), '') + + 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), '') + B_with_slots_dict_weakref.__bases__ = (A_with_dict,) + self.assertEqual(repr(b), '') + B_with_slots_dict_weakref.__bases__ = (A,) + self.assertEqual(repr(b), '') + + 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), '') + + 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), '') + C_with_slots_dict.__bases__ = (A_with_slots,) + self.assertEqual(repr(c), '') + + class A_int(int): + __slots__ = () + def __repr__(self): + return '' + 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), '') + B_int.__bases__ = (int,) + self.assertEqual(repr(b), '42') + + class A_tuple(tuple): + __slots__ = () + def __repr__(self): + return '' + 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), '') + 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 - else: - self.fail("shouldn't be able to create inheritance cycles") + + 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 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): @@ -4956,7 +5179,7 @@ class X: 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 @@ def foo(self): 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), '') - self.assertIsNone(sm.__func__) - self.assertIsNone(sm.__wrapped__) - - def test_classmethod_new(self): - class MyClassMethod(classmethod): - def __init__(self, func): - pass - def func(): pass - cm = MyClassMethod(func) - self.assertEqual(repr(cm), '') - self.assertIsNone(cm.__func__) - self.assertIsNone(cm.__wrapped__) + def test_gh146587(self): + # See https://github.com/python/cpython/issues/146587 + + 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 A(metaclass=M): 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() diff --git a/crates/vm/src/builtins/object.rs b/crates/vm/src/builtins/object.rs index 2e832c571d5..40be5c16406 100644 --- a/crates/vm/src/builtins/object.rs +++ b/crates/vm/src/builtins/object.rs @@ -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(()) diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index 8f1636d032a..d75a937bb27 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -47,6 +47,32 @@ use std::collections::HashSet; pub(crate) type PyTypeTupleRef = PyRef>; +pub(crate) struct CommonTypeDescriptors { + dict: PyRef, + weakref: PyRef, +} + +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 { // 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::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::().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 { // 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) -> 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) -> bool { +fn compatible_with_base(child: &Py, 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) -> &Py { - while compatible_with_base(typ) { +fn layout_solid_base(mut typ: &Py, set_class: bool) -> &Py { + while compatible_with_base(typ, set_class) { typ = typ.base.deref().unwrap(); } typ @@ -3759,12 +3787,12 @@ fn layout_solid_base(mut typ: &Py) -> &Py { /// 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, b: &Py) -> bool { +fn same_slots_added(a: &Py, b: &Py, 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, b: &Py) -> 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, + original: &Py, new_to: &Py, 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(()); } diff --git a/crates/vm/src/vm/interpreter.rs b/crates/vm/src/vm/interpreter.rs index 75f31574d5c..afebf052a1f 100644 --- a/crates/vm/src/vm/interpreter.rs +++ b/crates/vm/src/vm/interpreter.rs @@ -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")] diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 7ae959a3b75..e6d530abf4a 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -886,6 +886,8 @@ pub struct PyGlobalState { pub global_profile_func: PyMutex>, /// 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,