Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions crates/derive-impl/src/pyclass.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ struct ImplContext {
member_items: MemberNursery,
extend_slots_items: ItemNursery,
class_extensions: Vec<TokenStream>,
extra_impl_items: Vec<syn::ImplItem>,
errors: Vec<syn::Error>,
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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<Self>, vm: &VirtualMachine) -> PyResult<PyStrRef>",
params.len()
),
));
}

// Check first parameter is a reference (should be &Py<...> or &self for impl Py<T>)
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<Self> or &self (for impl Py<T>)\n\
Hint: Use `zelf: &Py<Self>` instead of `PyRef<Self>`",
));
}
} else if let Some(syn::FnArg::Receiver(recv)) = params.first() {
// &self is allowed for impl Py<T> blocks (where &self == &Py<T>)
// 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<T>, vm: &VirtualMachine) -> PyResult<PyStrRef>",
));
}
}

// Check return type (should be PyResult<PyStrRef> or PyResult<PyRef<PyStr>>)
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<PyStr>"))
}
syn::ReturnType::Default => false,
};
if !valid_return_type {
return Err(syn::Error::new_spanned(
&sig.output,
"#[pymethod] __str__ must return PyResult<PyStrRef>.\n\
Hint: Use `-> PyResult<PyStrRef>` 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,
Expand All @@ -934,6 +1029,7 @@ where
raw,
attr_name: self.inner.attr_name,
});

Ok(())
}
}
Expand Down
Loading