diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 8c711207fa..88d120a427 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -4941,7 +4941,6 @@ def __init__(self): for o in gc.get_objects(): self.assertIsNot(type(o), X) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_object_new_and_init_with_parameters(self): # See issue #1683368 class OverrideNeither: diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index d0fec18250..e403ab7b22 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -409,6 +409,7 @@ class NotFuture: pass coro.close(); gen_coro.close() # silence warnings + @unittest.expectedFailure # TODO: RUSTPYTHON def test_isroutine(self): # method self.assertTrue(inspect.isroutine(git.argue)) @@ -1483,8 +1484,6 @@ def test_getfullargspec_definition_order_preserved_on_kwonly(self): l = list(signature.kwonlyargs) self.assertEqual(l, unsorted_keyword_only_parameters) - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_classify_newstyle(self): class A(object): diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index fdcb9060e8..7e9b28236c 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -597,6 +597,7 @@ def test_internal_sizes(self): self.assertGreater(object.__basicsize__, 0) self.assertGreater(tuple.__itemsize__, 0) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_slot_wrapper_types(self): self.assertIsInstance(object.__init__, types.WrapperDescriptorType) self.assertIsInstance(object.__str__, types.WrapperDescriptorType) @@ -611,6 +612,7 @@ def test_dunder_get_signature(self): # gh-93021: Second parameter is optional self.assertIs(sig.parameters["owner"].default, None) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_method_wrapper_types(self): self.assertIsInstance(object().__init__, types.MethodWrapperType) self.assertIsInstance(object().__str__, types.MethodWrapperType) diff --git a/Lib/test/test_weakref.py b/Lib/test/test_weakref.py index e7cd5962cf..f47c17b723 100644 --- a/Lib/test/test_weakref.py +++ b/Lib/test/test_weakref.py @@ -906,8 +906,6 @@ def __del__(self): w = Target() - # TODO: RUSTPYTHON - @unittest.expectedFailure def test_init(self): # Issue 3634 # .__init__() doesn't check errors correctly diff --git a/crates/vm/src/builtins/descriptor.rs b/crates/vm/src/builtins/descriptor.rs index cdcc456edf..2ccd1dcc0e 100644 --- a/crates/vm/src/builtins/descriptor.rs +++ b/crates/vm/src/builtins/descriptor.rs @@ -3,8 +3,11 @@ use crate::{ AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, builtins::{PyTypeRef, builtin_func::PyNativeMethod, type_}, class::PyClassImpl, + common::hash::PyHash, function::{FuncArgs, PyMethodDef, PyMethodFlags, PySetterValue}, - types::{Callable, GetDescriptor, Representable}, + types::{ + Callable, Comparable, GetDescriptor, Hashable, InitFunc, PyComparisonOp, Representable, + }, }; use rustpython_common::lock::PyRwLock; @@ -219,7 +222,7 @@ impl std::fmt::Debug for PyMemberDef { } } -// PyMemberDescrObject in CPython +// = PyMemberDescrObject #[pyclass(name = "member_descriptor", module = false)] #[derive(Debug)] pub struct PyMemberDescriptor { @@ -382,4 +385,201 @@ impl GetDescriptor for PyMemberDescriptor { pub fn init(ctx: &Context) { PyMemberDescriptor::extend_class(ctx, ctx.types.member_descriptor_type); PyMethodDescriptor::extend_class(ctx, ctx.types.method_descriptor_type); + PySlotWrapper::extend_class(ctx, ctx.types.wrapper_descriptor_type); + PyMethodWrapper::extend_class(ctx, ctx.types.method_wrapper_type); +} + +// PySlotWrapper - wrapper_descriptor + +/// wrapper_descriptor: wraps a slot function as a Python method +// = PyWrapperDescrObject +#[pyclass(name = "wrapper_descriptor", module = false)] +#[derive(Debug)] +pub struct PySlotWrapper { + pub typ: &'static Py, + pub name: &'static PyStrInterned, + pub wrapped: InitFunc, + pub doc: Option<&'static str>, +} + +impl PyPayload for PySlotWrapper { + fn class(ctx: &Context) -> &'static Py { + ctx.types.wrapper_descriptor_type + } +} + +impl GetDescriptor for PySlotWrapper { + fn descr_get( + zelf: PyObjectRef, + obj: Option, + _cls: Option, + vm: &VirtualMachine, + ) -> PyResult { + match obj { + None => Ok(zelf), + Some(obj) if vm.is_none(&obj) => Ok(zelf), + Some(obj) => { + let zelf = zelf.downcast::().unwrap(); + Ok(PyMethodWrapper { wrapper: zelf, obj }.into_pyobject(vm)) + } + } + } +} + +impl Callable for PySlotWrapper { + type Args = FuncArgs; + + fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + // list.__init__(l, [1,2,3]) form + let (obj, rest): (PyObjectRef, FuncArgs) = args.bind(vm)?; + + if !obj.fast_isinstance(zelf.typ) { + return Err(vm.new_type_error(format!( + "descriptor '{}' requires a '{}' object but received a '{}'", + zelf.name.as_str(), + zelf.typ.name(), + obj.class().name() + ))); + } + + (zelf.wrapped)(obj, rest, vm)?; + Ok(vm.ctx.none()) + } +} + +#[pyclass( + with(GetDescriptor, Callable, Representable), + flags(DISALLOW_INSTANTIATION) +)] +impl PySlotWrapper { + #[pygetset] + fn __name__(&self) -> &'static PyStrInterned { + self.name + } + + #[pygetset] + fn __qualname__(&self) -> String { + format!("{}.{}", self.typ.name(), self.name) + } + + #[pygetset] + fn __objclass__(&self) -> PyTypeRef { + self.typ.to_owned() + } + + #[pygetset] + fn __doc__(&self) -> Option<&'static str> { + self.doc + } +} + +impl Representable for PySlotWrapper { + #[inline] + fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok(format!( + "", + zelf.name.as_str(), + zelf.typ.name() + )) + } +} + +// PyMethodWrapper - method-wrapper + +/// method-wrapper: a slot wrapper bound to an instance +/// Returned when accessing l.__init__ on an instance +#[pyclass(name = "method-wrapper", module = false, traverse)] +#[derive(Debug)] +pub struct PyMethodWrapper { + pub wrapper: PyRef, + #[pytraverse(skip)] + pub obj: PyObjectRef, +} + +impl PyPayload for PyMethodWrapper { + fn class(ctx: &Context) -> &'static Py { + ctx.types.method_wrapper_type + } +} + +impl Callable for PyMethodWrapper { + type Args = FuncArgs; + + fn call(zelf: &Py, args: FuncArgs, vm: &VirtualMachine) -> PyResult { + (zelf.wrapper.wrapped)(zelf.obj.clone(), args, vm)?; + Ok(vm.ctx.none()) + } +} + +#[pyclass( + with(Callable, Representable, Hashable, Comparable), + flags(DISALLOW_INSTANTIATION) +)] +impl PyMethodWrapper { + #[pygetset] + fn __self__(&self) -> PyObjectRef { + self.obj.clone() + } + + #[pygetset] + fn __name__(&self) -> &'static PyStrInterned { + self.wrapper.name + } + + #[pygetset] + fn __objclass__(&self) -> PyTypeRef { + self.wrapper.typ.to_owned() + } + + #[pymethod] + fn __reduce__(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + let builtins_getattr = vm.builtins.get_attr("getattr", vm)?; + Ok(vm + .ctx + .new_tuple(vec![ + builtins_getattr, + vm.ctx + .new_tuple(vec![ + zelf.obj.clone(), + vm.ctx.new_str(zelf.wrapper.name.as_str()).into(), + ]) + .into(), + ]) + .into()) + } +} + +impl Representable for PyMethodWrapper { + #[inline] + fn repr_str(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + Ok(format!( + "", + zelf.wrapper.name.as_str(), + zelf.obj.class().name(), + zelf.obj.get_id() + )) + } +} + +impl Hashable for PyMethodWrapper { + fn hash(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let obj_hash = zelf.obj.hash(vm)?; + let wrapper_hash = zelf.wrapper.as_object().get_id() as PyHash; + Ok(obj_hash ^ wrapper_hash) + } +} + +impl Comparable for PyMethodWrapper { + fn cmp( + zelf: &Py, + other: &PyObject, + op: PyComparisonOp, + vm: &VirtualMachine, + ) -> PyResult { + op.eq_only(|| { + let other = class_or_notimplemented!(Self, other); + let eq = zelf.wrapper.is(&other.wrapper) && vm.bool_eq(&zelf.obj, &other.obj)?; + Ok(eq.into()) + }) + } } diff --git a/crates/vm/src/builtins/object.rs b/crates/vm/src/builtins/object.rs index 0970496c7b..854cb2701e 100644 --- a/crates/vm/src/builtins/object.rs +++ b/crates/vm/src/builtins/object.rs @@ -118,7 +118,46 @@ impl Constructor for PyBaseObject { impl Initializer for PyBaseObject { type Args = FuncArgs; - fn slot_init(_zelf: PyObjectRef, _args: FuncArgs, _vm: &VirtualMachine) -> PyResult<()> { + // object_init: excess_args validation + fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { + 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(()); + } + + // Otherwise, reject excess args + if !args.is_empty() { + return Err(vm.new_type_error(format!( + "{}.__init__() takes exactly one argument (the instance to initialize)", + typ.name() + ))); + } Ok(()) } diff --git a/crates/vm/src/builtins/weakref.rs b/crates/vm/src/builtins/weakref.rs index 88d6dbac3e..327c0fd148 100644 --- a/crates/vm/src/builtins/weakref.rs +++ b/crates/vm/src/builtins/weakref.rs @@ -4,10 +4,12 @@ use crate::common::{ hash::{self, PyHash}, }; use crate::{ - AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyResult, VirtualMachine, + AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::PyClassImpl, function::{FuncArgs, OptionalArg}, - types::{Callable, Comparable, Constructor, Hashable, PyComparisonOp, Representable}, + types::{ + Callable, Comparable, Constructor, Hashable, Initializer, PyComparisonOp, Representable, + }, }; pub use crate::object::PyWeak; @@ -49,8 +51,24 @@ impl Constructor for PyWeak { } } +impl Initializer for PyWeak { + type Args = WeakNewArgs; + + // weakref_tp_init: accepts args but does nothing (all init done in slot_new) + fn init(_zelf: PyRef, _args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> { + Ok(()) + } +} + #[pyclass( - with(Callable, Hashable, Comparable, Constructor, Representable), + with( + Callable, + Hashable, + Comparable, + Constructor, + Initializer, + Representable + ), flags(BASETYPE) )] impl PyWeak { diff --git a/crates/vm/src/class.rs b/crates/vm/src/class.rs index 6a36638570..92e9f6a15b 100644 --- a/crates/vm/src/class.rs +++ b/crates/vm/src/class.rs @@ -1,7 +1,8 @@ //! Utilities to define a new Python class use crate::{ - builtins::{PyBaseObject, PyType, PyTypeRef}, + PyPayload, + builtins::{PyBaseObject, PyType, PyTypeRef, descriptor::PySlotWrapper}, function::PyMethodDef, object::Py, types::{PyTypeFlags, PyTypeSlots, hash_not_implemented}, @@ -135,6 +136,20 @@ pub trait PyClassImpl: PyClassDef { } } + // Add __init__ slot wrapper if slot exists and not already in dict + if let Some(init_func) = class.slots.init.load() { + let init_name = identifier!(ctx, __init__); + if !class.attributes.read().contains_key(init_name) { + let wrapper = PySlotWrapper { + typ: class, + name: ctx.intern_str("__init__"), + wrapped: init_func, + doc: Some("Initialize self. See help(type(self)) for accurate signature."), + }; + class.set_attr(init_name, wrapper.into_ref(ctx).into()); + } + } + if class.slots.hash.load().map_or(0, |h| h as usize) == hash_not_implemented as usize { class.set_attr(ctx.names.__hash__, ctx.none.clone().into()); } diff --git a/crates/vm/src/stdlib/io.rs b/crates/vm/src/stdlib/io.rs index ba3576a176..2c3c02ec65 100644 --- a/crates/vm/src/stdlib/io.rs +++ b/crates/vm/src/stdlib/io.rs @@ -1464,17 +1464,12 @@ mod _io { #[pyslot] fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { let zelf: PyRef = zelf.try_into_value(vm)?; - zelf.__init__(args, vm) - } - - #[pymethod] - fn __init__(&self, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { let (raw, BufferSize { buffer_size }): (PyObjectRef, _) = args.bind(vm).map_err(|e| { let msg = format!("{}() {}", Self::CLASS_NAME, *e.__str__(vm)); vm.new_exception_msg(e.class().to_owned(), msg) })?; - self.init(raw, BufferSize { buffer_size }, vm) + zelf.init(raw, BufferSize { buffer_size }, vm) } fn init( diff --git a/crates/vm/src/types/slot.rs b/crates/vm/src/types/slot.rs index 5deb593818..d09f6925ee 100644 --- a/crates/vm/src/types/slot.rs +++ b/crates/vm/src/types/slot.rs @@ -465,7 +465,10 @@ fn descr_set_wrapper( fn init_wrapper(obj: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { let res = vm.call_special_method(&obj, identifier!(vm, __init__), args)?; if !vm.is_none(&res) { - return Err(vm.new_type_error("__init__ must return None")); + return Err(vm.new_type_error(format!( + "__init__ should return None, not '{:.200}'", + res.class().name() + ))); } Ok(()) } @@ -943,7 +946,6 @@ pub trait Initializer: PyPayload { #[inline] #[pyslot] - #[pymethod(name = "__init__")] fn slot_init(zelf: PyObjectRef, args: FuncArgs, vm: &VirtualMachine) -> PyResult<()> { #[cfg(debug_assertions)] let class_name_for_debug = zelf.class().name().to_string(); diff --git a/crates/vm/src/types/zoo.rs b/crates/vm/src/types/zoo.rs index d994ff6021..dd4631bc76 100644 --- a/crates/vm/src/types/zoo.rs +++ b/crates/vm/src/types/zoo.rs @@ -94,6 +94,8 @@ pub struct TypeZoo { pub generic_alias_type: &'static Py, pub union_type: &'static Py, pub member_descriptor_type: &'static Py, + pub wrapper_descriptor_type: &'static Py, + pub method_wrapper_type: &'static Py, // RustPython-original types pub method_def: &'static Py, @@ -187,6 +189,8 @@ impl TypeZoo { generic_alias_type: genericalias::PyGenericAlias::init_builtin_type(), union_type: union_::PyUnion::init_builtin_type(), member_descriptor_type: descriptor::PyMemberDescriptor::init_builtin_type(), + wrapper_descriptor_type: descriptor::PySlotWrapper::init_builtin_type(), + method_wrapper_type: descriptor::PyMethodWrapper::init_builtin_type(), method_def: crate::function::HeapMethodDef::init_builtin_type(), }