From fdbf65c4c8c02727f4b8389f6f30314dd68e3d62 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 24 Dec 2025 21:04:42 +0900 Subject: [PATCH] auto-generate slot_str --- crates/derive-impl/src/pyclass.rs | 96 +++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/crates/derive-impl/src/pyclass.rs b/crates/derive-impl/src/pyclass.rs index d1fe539863..8347e23459 100644 --- a/crates/derive-impl/src/pyclass.rs +++ b/crates/derive-impl/src/pyclass.rs @@ -70,6 +70,7 @@ struct ImplContext { member_items: MemberNursery, extend_slots_items: ItemNursery, class_extensions: Vec, + extra_impl_items: Vec, errors: Vec, } @@ -196,6 +197,10 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul }, ]; imp.items.extend(extra_methods); + // Add extra impl items (like __slot_str__ for __str__) + for item in context.extra_impl_items { + imp.items.push(item); + } let is_main_impl = impl_ty == payload_ty; if is_main_impl { let method_defs = if with_method_defs.is_empty() { @@ -294,6 +299,8 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul }, ]; trai.items.extend(extra_methods); + // Note: extra_impl_items (like __slot_str__ for __str__) are not added to traits, + // because traits define the method signature, not the slot wrapper implementation. trai.into_token_stream() } @@ -925,6 +932,94 @@ where args.attrs.push(allow_attr); } + // Special handling for __str__: generate slot wrapper instead of pymethod + if py_name == "__str__" { + // Validate __str__ signature + let sig = func.sig(); + let params: Vec<_> = sig.inputs.iter().collect(); + + // Check parameter count (should be 2: zelf and vm) + if params.len() != 2 { + return Err(syn::Error::new( + sig.inputs.span(), + format!( + "#[pymethod] __str__ must have exactly 2 parameters (zelf, vm), found {}.\n\ + Expected signature: fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult", + params.len() + ), + )); + } + + // Check first parameter is a reference (should be &Py<...> or &self for impl Py) + if let Some(syn::FnArg::Typed(pat_type)) = params.first() { + let ty = &pat_type.ty; + let is_reference = matches!(ty.as_ref(), syn::Type::Reference(_)); + if !is_reference { + return Err(syn::Error::new_spanned( + ty, + "#[pymethod] __str__ first parameter must be a reference type.\n\ + Expected: &Py or &self (for impl Py)\n\ + Hint: Use `zelf: &Py` instead of `PyRef`", + )); + } + } else if let Some(syn::FnArg::Receiver(recv)) = params.first() { + // &self is allowed for impl Py blocks (where &self == &Py) + // self by value is not allowed + if recv.reference.is_none() { + return Err(syn::Error::new_spanned( + recv, + "#[pymethod] __str__ cannot take `self` by value.\n\ + Expected: fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult", + )); + } + } + + // Check return type (should be PyResult or PyResult>) + let valid_return_type = match &sig.output { + syn::ReturnType::Type(_, ty) => { + let ty_str = quote!(#ty).to_string().replace(' ', ""); + ty_str.contains("PyResult") + && (ty_str.contains("PyStrRef") || ty_str.contains("PyRef")) + } + syn::ReturnType::Default => false, + }; + if !valid_return_type { + return Err(syn::Error::new_spanned( + &sig.output, + "#[pymethod] __str__ must return PyResult.\n\ + Hint: Use `-> PyResult` instead of `-> String` or other types", + )); + } + + // 1. Generate wrapper function as impl item + let wrapper_fn: syn::ImplItem = parse_quote! { + fn slot_str( + zelf: &::rustpython_vm::PyObject, + vm: &::rustpython_vm::VirtualMachine, + ) -> ::rustpython_vm::PyResult<::rustpython_vm::builtins::PyStrRef> { + let zelf: &::rustpython_vm::Py<_> = zelf.downcast_ref() + .ok_or_else(|| vm.new_type_error("unexpected payload for __str__"))?; + Self::#ident(zelf, vm) + } + }; + args.context.extra_impl_items.push(wrapper_fn); + + // 2. Add slot assignment to extend_slots_items + let slot_tokens = quote_spanned! { ident.span() => + slots.str.store(Some(Self::slot_str as _)); + }; + args.context.extend_slots_items.add_item( + ident.clone(), + vec!["(slot str)".to_string()], + args.cfgs.to_vec(), + slot_tokens, + 2, + )?; + + // 3. Don't add to method_items - PySlotWrapper handles dict entry + return Ok(()); + } + let doc = args.attrs.doc().map(|doc| format_doc(&sig_doc, &doc)); args.context.method_items.add_item(MethodNurseryItem { py_name, @@ -934,6 +1029,7 @@ where raw, attr_name: self.inner.attr_name, }); + Ok(()) } }