diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py index 0d3d8c9e2d..736022599e 100644 --- a/Lib/test/test_codecs.py +++ b/Lib/test/test_codecs.py @@ -3189,7 +3189,6 @@ def test_readline(self): sout = reader.readline() self.assertEqual(sout, b"\x80") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_buffer_api_usage(self): # We check all the transform codecs accept memoryview input # for encoding and decoding diff --git a/Lib/test/test_memoryio.py b/Lib/test/test_memoryio.py index 61d9b180e2..07d9d38d6e 100644 --- a/Lib/test/test_memoryio.py +++ b/Lib/test/test_memoryio.py @@ -724,8 +724,6 @@ class CBytesIOTest(PyBytesIOTest): ioclass = io.BytesIO UnsupportedOperation = io.UnsupportedOperation - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_bytes_array(self): super().test_bytes_array() @@ -739,8 +737,6 @@ def test_flags(self): def test_getbuffer(self): super().test_getbuffer() - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_init(self): super().test_init() @@ -770,8 +766,6 @@ def test_relative_seek(self): def test_seek(self): super().test_seek() - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_subclassing(self): super().test_subclassing() @@ -884,8 +878,6 @@ def test_detach(self): def test_flags(self): super().test_flags() - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_init(self): super().test_init() diff --git a/Lib/test/test_pickle.py b/Lib/test/test_pickle.py index 070e277c2a..ea51b9d091 100644 --- a/Lib/test/test_pickle.py +++ b/Lib/test/test_pickle.py @@ -180,11 +180,6 @@ def test_oob_buffers(self): # TODO(RUSTPYTHON): Remove this test when it passes def test_oob_buffers_writable_to_readonly(self): # TODO(RUSTPYTHON): Remove this test when it passes return super().test_oob_buffers_writable_to_readonly() - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_optional_frames(self): # TODO(RUSTPYTHON): Remove this test when it passes - return super().test_optional_frames() - # TODO: RUSTPYTHON @unittest.expectedFailure def test_buffers_error(self): # TODO(RUSTPYTHON): Remove this test when it passes @@ -220,11 +215,6 @@ def test_oob_buffers(self): # TODO(RUSTPYTHON): Remove this test when it passes def test_oob_buffers_writable_to_readonly(self): # TODO(RUSTPYTHON): Remove this test when it passes return super().test_oob_buffers_writable_to_readonly() - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_optional_frames(self): # TODO(RUSTPYTHON): Remove this test when it passes - return super().test_optional_frames() - class InMemoryPickleTests(AbstractPickleTests, AbstractUnpickleTests, BigmemPickleTests, unittest.TestCase): @@ -309,11 +299,6 @@ def test_in_band_buffers(self): # TODO(RUSTPYTHON): Remove this test when it pas def test_oob_buffers(self): # TODO(RUSTPYTHON): Remove this test when it passes return super().test_oob_buffers() - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_optional_frames(self): # TODO(RUSTPYTHON): Remove this test when it passes - return super().test_optional_frames() - class PersistentPicklerUnpicklerMixin(object): def dumps(self, arg, proto=None): diff --git a/Lib/test/test_pickletools.py b/Lib/test/test_pickletools.py index 6c38bef3d3..492f57cce2 100644 --- a/Lib/test/test_pickletools.py +++ b/Lib/test/test_pickletools.py @@ -97,11 +97,6 @@ def test_oob_buffers(self): # TODO(RUSTPYTHON): Remove this test when it passes def test_oob_buffers_writable_to_readonly(self): # TODO(RUSTPYTHON): Remove this test when it passes return super().test_oob_buffers_writable_to_readonly() - # TODO: RUSTPYTHON - @unittest.expectedFailure - def test_optional_frames(self): # TODO(RUSTPYTHON): Remove this test when it passes - return super().test_optional_frames() - # TODO: RUSTPYTHON @unittest.expectedFailure def test_py_methods(self): # TODO(RUSTPYTHON): Remove this test when it passes diff --git a/Lib/test/test_urllib2.py b/Lib/test/test_urllib2.py index 5a02b5db8e..399c94213a 100644 --- a/Lib/test/test_urllib2.py +++ b/Lib/test/test_urllib2.py @@ -641,8 +641,6 @@ def test_raise(self): self.assertRaises(urllib.error.URLError, o.open, req) self.assertEqual(o.calls, [(handlers[0], "http_open", (req,), {})]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_http_error(self): # XXX http_error_default # http errors are a special case @@ -666,8 +664,6 @@ def test_http_error(self): self.assertEqual((handler, method_name), got[:2]) self.assertEqual(args, got[2]) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_processors(self): # *_request / *_response methods get called appropriately o = OpenerDirector() @@ -874,8 +870,6 @@ def test_file(self): self.assertEqual(req.type, "ftp") self.assertEqual(req.type == "ftp", ftp) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_http(self): h = urllib.request.AbstractHTTPHandler() @@ -1136,8 +1130,6 @@ def test_fixpath_in_weirdurls(self): self.assertEqual(newreq.host, 'www.python.org') self.assertEqual(newreq.selector, '') - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_errors(self): h = urllib.request.HTTPErrorProcessor() o = h.parent = MockOpener() @@ -1163,8 +1155,6 @@ def test_errors(self): self.assertEqual(o.proto, "http") # o.error called self.assertEqual(o.args, (req, r, 502, "Bad gateway", {})) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_cookies(self): cj = MockCookieJar() h = urllib.request.HTTPCookieProcessor(cj) @@ -1291,8 +1281,6 @@ def test_relative_redirect(self): MockHeaders({"location": valid_url})) self.assertEqual(o.req.get_full_url(), valid_url) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_cookie_redirect(self): # cookies shouldn't leak into redirected requests from http.cookiejar import CookieJar @@ -1308,8 +1296,6 @@ def test_cookie_redirect(self): o.open("http://www.example.com/") self.assertFalse(hh.req.has_header("Cookie")) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_redirect_fragment(self): redirected_url = 'http://www.example.com/index.html#OK\r\n\r\n' hh = MockHTTPHandler(302, 'Location: ' + redirected_url) @@ -1374,8 +1360,6 @@ def http_open(self, req): request = handler.last_buf self.assertTrue(request.startswith(expected), repr(request)) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_proxy(self): u = "proxy.example.com:3128" for d in dict(http=u), dict(HTTP=u): @@ -1420,8 +1404,6 @@ def test_proxy_no_proxy_all(self): self.assertEqual(req.host, "www.python.org") del os.environ['no_proxy'] - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_proxy_https(self): o = OpenerDirector() ph = urllib.request.ProxyHandler(dict(https="proxy.example.com:3128")) @@ -1509,8 +1491,6 @@ def check_basic_auth(self, headers, realm): "http://acme.example.com/protected", "http://acme.example.com/protected") - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_basic_auth(self): realm = "realm2@example.com" realm2 = "realm2@example.com" @@ -1556,8 +1536,6 @@ def test_basic_auth(self): for challenge in challenges] self.check_basic_auth(headers, realm) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_proxy_basic_auth(self): opener = OpenerDirector() ph = urllib.request.ProxyHandler(dict(http="proxy.example.com:3128")) @@ -1575,8 +1553,6 @@ def test_proxy_basic_auth(self): "proxy.example.com:3128", ) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_basic_and_digest_auth_handlers(self): # HTTPDigestAuthHandler raised an exception if it couldn't handle a 40* # response (http://python.org/sf/1479302), where it should instead @@ -1684,8 +1660,6 @@ def _test_basic_auth(self, opener, auth_handler, auth_header, self.assertEqual(len(http_handler.requests), 1) self.assertFalse(http_handler.requests[0].has_header(auth_header)) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_basic_prior_auth_auto_send(self): # Assume already authenticated if is_authenticated=True # for APIs like Github that don't return 401 @@ -1713,8 +1687,6 @@ def test_basic_prior_auth_auto_send(self): # expect request to be sent with auth header self.assertTrue(http_handler.has_auth_header) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_basic_prior_auth_send_after_first_success(self): # Auto send auth header after authentication is successful once diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 0a50f03604..e7705590cd 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -1746,8 +1746,6 @@ def test_empty_file_raises_BadZipFile(self): fp.write("short file") self.assertRaises(zipfile.BadZipFile, zipfile.ZipFile, TESTFN) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_negative_central_directory_offset_raises_BadZipFile(self): # Zip file containing an empty EOCD record buffer = bytearray(b'PK\x05\x06' + b'\0'*18) diff --git a/crates/derive-impl/src/pyclass.rs b/crates/derive-impl/src/pyclass.rs index d1fe539863..1a01721391 100644 --- a/crates/derive-impl/src/pyclass.rs +++ b/crates/derive-impl/src/pyclass.rs @@ -223,8 +223,8 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul const METHOD_DEFS: &'static [::rustpython_vm::function::PyMethodDef] = &#method_defs; fn extend_slots(slots: &mut ::rustpython_vm::types::PyTypeSlots) { - #impl_ty::__extend_slots(slots); #with_slots + #impl_ty::__extend_slots(slots); } } } @@ -1672,9 +1672,24 @@ fn extract_impl_attrs(attr: PunctuatedNestedMeta, item: &Ident) -> Result - #extend_slots(slots); - }); + // For Initializer and Constructor traits, directly set the slot + // instead of calling __extend_slots. This ensures that the trait + // impl's override (e.g., slot_init in impl Initializer) is used, + // not the trait's default implementation. + let slot_code = if path.is_ident("Initializer") { + quote_spanned! { item_span => + slots.init.store(Some(::slot_init as _)); + } + } else if path.is_ident("Constructor") { + quote_spanned! { item_span => + slots.new.store(Some(::slot_new as _)); + } + } else { + quote_spanned! { item_span => + #extend_slots(slots); + } + }; + with_slots.push(slot_code); } } else if path.is_ident("flags") { for meta in nested { diff --git a/crates/vm/src/builtins/object.rs b/crates/vm/src/builtins/object.rs index ca208790f4..77a1eff6c7 100644 --- a/crates/vm/src/builtins/object.rs +++ b/crates/vm/src/builtins/object.rs @@ -120,44 +120,36 @@ impl Initializer for PyBaseObject { // object_init: excess_args validation fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + if args.is_empty() { + return Ok(()); + } + let typ = zelf.class(); let object_type = &vm.ctx.types.object_type; let typ_init = typ.slots.init.load().map(|f| f as usize); let object_init = object_type.slots.init.load().map(|f| f as usize); - let typ_new = typ.slots.new.load().map(|f| f as usize); - let object_new = object_type.slots.new.load().map(|f| f as usize); - // For heap types (Python classes), check if __new__ is defined anywhere in MRO - // (before object) because heap types always have slots.new = new_wrapper via MRO - let is_heap_type = typ - .slots - .flags - .contains(crate::types::PyTypeFlags::HEAPTYPE); - let new_overridden = if is_heap_type { - // Check if __new__ is defined in any base class (excluding object) - let new_id = identifier!(vm, __new__); - typ.mro_collect() - .into_iter() - .take_while(|t| !std::ptr::eq(t.as_ref(), *object_type)) - .any(|t| t.attributes.read().contains_key(new_id)) - } else { - // For built-in types, use slot comparison - typ_new != object_new - }; - - // If both __init__ and __new__ are overridden, allow excess args - if typ_init != object_init && new_overridden { - return Ok(()); + // if (type->tp_init != object_init) → first error + if typ_init != object_init { + return Err(vm.new_type_error( + "object.__init__() takes exactly one argument (the instance to initialize)" + .to_owned(), + )); } - // Otherwise, reject excess args - if !args.is_empty() { + let typ_new = typ.slots.new.load().map(|f| f as usize); + let object_new = object_type.slots.new.load().map(|f| f as usize); + + // if (type->tp_new == object_new) → second error + if typ_new == object_new { return Err(vm.new_type_error(format!( "{}.__init__() takes exactly one argument (the instance to initialize)", typ.name() ))); } + + // Both conditions false → OK (e.g., tuple, dict with custom __new__) Ok(()) } @@ -591,6 +583,13 @@ pub fn object_set_dict(obj: PyObjectRef, dict: PyDictRef, vm: &VirtualMachine) - } pub fn init(ctx: &Context) { + // Manually set init slot - derive macro doesn't generate extend_slots + // for trait impl that overrides #[pyslot] method + ctx.types + .object_type + .slots + .init + .store(Some(::slot_init)); PyBaseObject::extend_class(ctx, ctx.types.object_type); } diff --git a/crates/vm/src/builtins/type.rs b/crates/vm/src/builtins/type.rs index ffaad32468..4f150d9258 100644 --- a/crates/vm/src/builtins/type.rs +++ b/crates/vm/src/builtins/type.rs @@ -25,8 +25,8 @@ use crate::{ object::{Traverse, TraverseFn}, protocol::{PyIterReturn, PyNumberMethods}, types::{ - AsNumber, Callable, Constructor, GetAttr, PyTypeFlags, PyTypeSlots, Representable, SetAttr, - TypeDataRef, TypeDataRefMut, TypeDataSlot, + AsNumber, Callable, Constructor, GetAttr, Initializer, PyTypeFlags, PyTypeSlots, + Representable, SetAttr, TypeDataRef, TypeDataRefMut, TypeDataSlot, }, }; use indexmap::{IndexMap, map::Entry}; @@ -412,8 +412,10 @@ impl PyType { } pub(crate) fn init_slots(&self, ctx: &Context) { - // Inherit slots from direct bases (not MRO) - for base in self.bases.read().iter() { + // Inherit slots from MRO + // Note: self.mro does NOT include self, so we iterate all elements + let mro: Vec<_> = self.mro.read().iter().cloned().collect(); + for base in mro.iter() { self.inherit_slots(base); } @@ -460,7 +462,7 @@ impl PyType { } } - /// Inherit slots from base type. typeobject.c: inherit_slots + /// Inherit slots from base type. inherit_slots pub(crate) fn inherit_slots(&self, base: &Self) { macro_rules! copyslot { ($slot:ident) => { @@ -472,6 +474,28 @@ impl PyType { }; } + // Copy init slot only if base actually defines it (not just inherited) + // This is needed for multiple inheritance where a later base might + // have a more specific init slot + macro_rules! copyslot_defined { + ($slot:ident) => { + if self.slots.$slot.load().is_none() { + if let Some(base_val) = base.slots.$slot.load() { + // SLOTDEFINED: base->SLOT && (basebase == NULL || base->SLOT != basebase->SLOT) + let basebase = base.base.as_ref(); + let slot_defined = match basebase { + None => true, + Some(bb) => bb.slots.$slot.load().map(|v| v as usize) + != Some(base_val as usize), + }; + if slot_defined { + self.slots.$slot.store(Some(base_val)); + } + } + } + }; + } + // Core slots copyslot!(hash); copyslot!(call); @@ -484,9 +508,8 @@ impl PyType { copyslot!(iternext); copyslot!(descr_get); copyslot!(descr_set); - // Note: init is NOT inherited here because object_init has special - // handling in CPython (checks if type->tp_init != object_init). - // TODO: implement proper init inheritance with object_init check + // init uses SLOTDEFINED check for multiple inheritance support + copyslot_defined!(init); copyslot!(del); // new is handled by set_new() // as_buffer is inherited at type creation time (not AtomicCell) @@ -832,7 +855,16 @@ impl Py { } #[pyclass( - with(Py, Constructor, GetAttr, SetAttr, Callable, AsNumber, Representable), + with( + Py, + Constructor, + Initializer, + GetAttr, + SetAttr, + Callable, + AsNumber, + Representable + ), flags(BASETYPE) )] impl PyType { @@ -1532,6 +1564,26 @@ fn get_doc_from_internal_doc<'a>(name: &str, internal_doc: &'a str) -> &'a str { internal_doc } +impl Initializer for PyType { + type Args = FuncArgs; + + // type_init + fn slot_init(_zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + // type.__init__() takes 1 or 3 arguments + if args.args.len() == 1 && !args.kwargs.is_empty() { + return Err(vm.new_type_error("type.__init__() takes no keyword arguments".to_owned())); + } + if args.args.len() != 1 && args.args.len() != 3 { + return Err(vm.new_type_error("type.__init__() takes 1 or 3 arguments".to_owned())); + } + Ok(()) + } + + fn init(_zelf: PyRef, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> { + unreachable!("slot_init is defined") + } +} + impl GetAttr for PyType { fn getattro(zelf: &Py, name_str: &Py, vm: &VirtualMachine) -> PyResult { #[cold] diff --git a/crates/vm/src/exception_group.rs b/crates/vm/src/exception_group.rs index 645a3e779f..b2dee20620 100644 --- a/crates/vm/src/exception_group.rs +++ b/crates/vm/src/exception_group.rs @@ -353,21 +353,13 @@ pub(super) mod types { impl Initializer for PyBaseExceptionGroup { type Args = FuncArgs; - fn slot_init( - _zelf: PyObjectRef, - _args: ::rustpython_vm::function::FuncArgs, - _vm: &::rustpython_vm::VirtualMachine, - ) -> ::rustpython_vm::PyResult<()> { - // CPython's BaseExceptionGroup.__init__ just calls BaseException.__init__ - // which stores args as-is. Since __new__ already set up the correct args - // (message, exceptions_tuple), we don't need to do anything here. - // This also allows subclasses to pass extra arguments to __new__ without - // __init__ complaining about argument count. + fn slot_init(_zelf: PyObjectRef, _args: FuncArgs, _vm: &VirtualMachine) -> PyResult<()> { + // No-op: __new__ already set up the correct args (message, exceptions_tuple) Ok(()) } fn init(_zelf: PyRef, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> { - unreachable!("slot_init is defined") + unreachable!("slot_init is overridden") } } diff --git a/crates/vm/src/stdlib/io.rs b/crates/vm/src/stdlib/io.rs index 9402660a86..a02965797b 100644 --- a/crates/vm/src/stdlib/io.rs +++ b/crates/vm/src/stdlib/io.rs @@ -3679,23 +3679,31 @@ mod _io { } impl Constructor for StringIO { + type Args = FuncArgs; + + fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { + Ok(Self { + _base: Default::default(), + buffer: PyRwLock::new(BufferedIO::new(Cursor::new(Vec::new()))), + closed: AtomicCell::new(false), + }) + } + } + + impl Initializer for StringIO { type Args = StringIONewArgs; #[allow(unused_variables)] - fn py_new( - _cls: &Py, + fn init( + zelf: PyRef, Self::Args { object, newline }: Self::Args, _vm: &VirtualMachine, - ) -> PyResult { + ) -> PyResult<()> { let raw_bytes = object .flatten() .map_or_else(Vec::new, |v| v.as_bytes().to_vec()); - - Ok(Self { - _base: Default::default(), - buffer: PyRwLock::new(BufferedIO::new(Cursor::new(raw_bytes))), - closed: AtomicCell::new(false), - }) + *zelf.buffer.write() = BufferedIO::new(Cursor::new(raw_bytes)); + Ok(()) } } @@ -3709,7 +3717,7 @@ mod _io { } } - #[pyclass(flags(BASETYPE, HAS_DICT), with(Constructor))] + #[pyclass(flags(BASETYPE, HAS_DICT), with(Constructor, Initializer))] impl StringIO { #[pymethod] const fn readable(&self) -> bool { @@ -3814,22 +3822,35 @@ mod _io { } impl Constructor for BytesIO { - type Args = OptionalArg>; - - fn py_new(_cls: &Py, object: Self::Args, _vm: &VirtualMachine) -> PyResult { - let raw_bytes = object - .flatten() - .map_or_else(Vec::new, |input| input.as_bytes().to_vec()); + type Args = FuncArgs; + fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { Ok(Self { _base: Default::default(), - buffer: PyRwLock::new(BufferedIO::new(Cursor::new(raw_bytes))), + buffer: PyRwLock::new(BufferedIO::new(Cursor::new(Vec::new()))), closed: AtomicCell::new(false), exports: AtomicCell::new(0), }) } } + impl Initializer for BytesIO { + type Args = OptionalArg>; + + fn init(zelf: PyRef, object: Self::Args, vm: &VirtualMachine) -> PyResult<()> { + if zelf.exports.load() > 0 { + return Err(vm.new_buffer_error( + "Existing exports of data: object cannot be re-sized".to_owned(), + )); + } + let raw_bytes = object + .flatten() + .map_or_else(Vec::new, |input| input.borrow_buf().to_vec()); + *zelf.buffer.write() = BufferedIO::new(Cursor::new(raw_bytes)); + Ok(()) + } + } + impl BytesIO { fn buffer(&self, vm: &VirtualMachine) -> PyResult> { if !self.closed.load() { @@ -3840,7 +3861,7 @@ mod _io { } } - #[pyclass(flags(BASETYPE, HAS_DICT), with(PyRef, Constructor))] + #[pyclass(flags(BASETYPE, HAS_DICT), with(PyRef, Constructor, Initializer))] impl BytesIO { #[pymethod] const fn readable(&self) -> bool { diff --git a/crates/vm/src/types/slot.rs b/crates/vm/src/types/slot.rs index bfe6e04762..13a43ed9c9 100644 --- a/crates/vm/src/types/slot.rs +++ b/crates/vm/src/types/slot.rs @@ -632,13 +632,48 @@ impl PyType { update_slot!(descr_set, descr_set_wrapper); } _ if name == identifier!(ctx, __init__) => { - toggle_slot!(init, init_wrapper); + // Special handling: check if this type or any base has a real __init__ method + // If only slot wrappers exist, use the inherited slot from copyslot! + if ADD { + // First check if this type has __init__ in its own dict + let has_own_init = self.attributes.read().contains_key(name); + if has_own_init { + // This type defines __init__ - use wrapper + self.slots.init.store(Some(init_wrapper)); + } else if self.has_real_method_in_mro(name, ctx) { + // A base class defines a real __init__ method - use wrapper + self.slots.init.store(Some(init_wrapper)); + } + // else: keep inherited slot from copyslot! + } else { + let inherited = self + .mro + .read() + .iter() + .skip(1) + .find_map(|cls| cls.slots.init.load()); + self.slots.init.store(inherited); + } } _ if name == identifier!(ctx, __new__) => { toggle_slot!(new, new_wrapper); } _ if name == identifier!(ctx, __del__) => { - toggle_slot!(del, del_wrapper); + // Same special handling as __init__ + if ADD { + let has_own_del = self.attributes.read().contains_key(name); + if has_own_del || self.has_real_method_in_mro(name, ctx) { + self.slots.del.store(Some(del_wrapper)); + } + } else { + let inherited = self + .mro + .read() + .iter() + .skip(1) + .find_map(|cls| cls.slots.del.load()); + self.slots.del.store(inherited); + } } _ if name == identifier!(ctx, __bool__) => { toggle_sub_slot!(as_number, boolean, bool_wrapper); @@ -890,6 +925,36 @@ impl PyType { _ => {} } } + + /// Check if there's a real method (not a slot wrapper) for `name` anywhere in the MRO. + /// If a real method exists, we should use a wrapper function. Otherwise, use the inherited slot. + fn has_real_method_in_mro(&self, name: &'static PyStrInterned, ctx: &Context) -> bool { + use crate::builtins::descriptor::PySlotWrapper; + + // Check the entire MRO (including self) for the method + for cls in self.mro.read().iter() { + if let Some(attr) = cls.attributes.read().get(name).cloned() { + // Found the method - check if it's a slot wrapper + if attr.class().is(ctx.types.wrapper_descriptor_type) { + if let Some(wrapper) = attr.downcast_ref::() { + // It's a slot wrapper - check if it belongs to this class + let wrapper_typ: *const _ = wrapper.typ; + let cls_ptr: *const _ = cls.as_ref(); + if wrapper_typ == cls_ptr { + // Slot wrapper defined on this exact class - use inherited slot + return false; + } + } + // Inherited slot wrapper - continue checking MRO + } else { + // Real method found - use wrapper function + return true; + } + } + } + // No real method found in MRO - use inherited slot + false + } } /// Trait for types that can be constructed via Python's `__new__` method. diff --git a/crates/vm/src/types/zoo.rs b/crates/vm/src/types/zoo.rs index 0cd04a0ac1..9d16424167 100644 --- a/crates/vm/src/types/zoo.rs +++ b/crates/vm/src/types/zoo.rs @@ -199,8 +199,10 @@ impl TypeZoo { /// Fill attributes of builtin types. #[cold] pub(crate) fn extend(context: &Context) { - type_::init(context); + // object must be initialized before type to set object.slots.init, + // which type will inherit via inherit_slots() object::init(context); + type_::init(context); list::init(context); set::init(context); tuple::init(context);