Skip to content

Commit d4a2177

Browse files
committed
auto-generate slot_str
1 parent 340df35 commit d4a2177

1 file changed

Lines changed: 96 additions & 0 deletions

File tree

crates/derive-impl/src/pyclass.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ struct ImplContext {
7070
member_items: MemberNursery,
7171
extend_slots_items: ItemNursery,
7272
class_extensions: Vec<TokenStream>,
73+
extra_impl_items: Vec<syn::ImplItem>,
7374
errors: Vec<syn::Error>,
7475
}
7576

@@ -196,6 +197,10 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul
196197
},
197198
];
198199
imp.items.extend(extra_methods);
200+
// Add extra impl items (like __slot_str__ for __str__)
201+
for item in context.extra_impl_items {
202+
imp.items.push(item);
203+
}
199204
let is_main_impl = impl_ty == payload_ty;
200205
if is_main_impl {
201206
let method_defs = if with_method_defs.is_empty() {
@@ -294,6 +299,8 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul
294299
},
295300
];
296301
trai.items.extend(extra_methods);
302+
// Note: extra_impl_items (like __slot_str__ for __str__) are not added to traits,
303+
// because traits define the method signature, not the slot wrapper implementation.
297304

298305
trai.into_token_stream()
299306
}
@@ -925,6 +932,94 @@ where
925932
args.attrs.push(allow_attr);
926933
}
927934

935+
// Special handling for __str__: generate slot wrapper instead of pymethod
936+
if py_name == "__str__" {
937+
// Validate __str__ signature
938+
let sig = func.sig();
939+
let params: Vec<_> = sig.inputs.iter().collect();
940+
941+
// Check parameter count (should be 2: zelf and vm)
942+
if params.len() != 2 {
943+
return Err(syn::Error::new(
944+
sig.inputs.span(),
945+
format!(
946+
"#[pymethod] __str__ must have exactly 2 parameters (zelf, vm), found {}.\n\
947+
Expected signature: fn __str__(zelf: &Py<Self>, vm: &VirtualMachine) -> PyResult<PyStrRef>",
948+
params.len()
949+
),
950+
));
951+
}
952+
953+
// Check first parameter is a reference (should be &Py<...> or &self for impl Py<T>)
954+
if let Some(syn::FnArg::Typed(pat_type)) = params.first() {
955+
let ty = &pat_type.ty;
956+
let is_reference = matches!(ty.as_ref(), syn::Type::Reference(_));
957+
if !is_reference {
958+
return Err(syn::Error::new_spanned(
959+
ty,
960+
"#[pymethod] __str__ first parameter must be a reference type.\n\
961+
Expected: &Py<Self> or &self (for impl Py<T>)\n\
962+
Hint: Use `zelf: &Py<Self>` instead of `PyRef<Self>`",
963+
));
964+
}
965+
} else if let Some(syn::FnArg::Receiver(recv)) = params.first() {
966+
// &self is allowed for impl Py<T> blocks (where &self == &Py<T>)
967+
// self by value is not allowed
968+
if recv.reference.is_none() {
969+
return Err(syn::Error::new_spanned(
970+
recv,
971+
"#[pymethod] __str__ cannot take `self` by value.\n\
972+
Expected: fn __str__(zelf: &Py<T>, vm: &VirtualMachine) -> PyResult<PyStrRef>",
973+
));
974+
}
975+
}
976+
977+
// Check return type (should be PyResult<PyStrRef> or PyResult<PyRef<PyStr>>)
978+
let valid_return_type = match &sig.output {
979+
syn::ReturnType::Type(_, ty) => {
980+
let ty_str = quote!(#ty).to_string().replace(' ', "");
981+
ty_str.contains("PyResult")
982+
&& (ty_str.contains("PyStrRef") || ty_str.contains("PyRef<PyStr>"))
983+
}
984+
syn::ReturnType::Default => false,
985+
};
986+
if !valid_return_type {
987+
return Err(syn::Error::new_spanned(
988+
&sig.output,
989+
"#[pymethod] __str__ must return PyResult<PyStrRef>.\n\
990+
Hint: Use `-> PyResult<PyStrRef>` instead of `-> String` or other types",
991+
));
992+
}
993+
994+
// 1. Generate wrapper function as impl item
995+
let wrapper_fn: syn::ImplItem = parse_quote! {
996+
fn slot_str(
997+
zelf: &::rustpython_vm::PyObject,
998+
vm: &::rustpython_vm::VirtualMachine,
999+
) -> ::rustpython_vm::PyResult<::rustpython_vm::builtins::PyStrRef> {
1000+
let zelf: &::rustpython_vm::Py<_> = zelf.downcast_ref()
1001+
.ok_or_else(|| vm.new_type_error("unexpected payload for __str__"))?;
1002+
Self::#ident(zelf, vm)
1003+
}
1004+
};
1005+
args.context.extra_impl_items.push(wrapper_fn);
1006+
1007+
// 2. Add slot assignment to extend_slots_items
1008+
let slot_tokens = quote_spanned! { ident.span() =>
1009+
slots.str.store(Some(Self::slot_str as _));
1010+
};
1011+
args.context.extend_slots_items.add_item(
1012+
ident.clone(),
1013+
vec!["(slot str)".to_string()],
1014+
args.cfgs.to_vec(),
1015+
slot_tokens,
1016+
2,
1017+
)?;
1018+
1019+
// 3. Don't add to method_items - PySlotWrapper handles dict entry
1020+
return Ok(());
1021+
}
1022+
9281023
let doc = args.attrs.doc().map(|doc| format_doc(&sig_doc, &doc));
9291024
args.context.method_items.add_item(MethodNurseryItem {
9301025
py_name,
@@ -934,6 +1029,7 @@ where
9341029
raw,
9351030
attr_name: self.inner.attr_name,
9361031
});
1032+
9371033
Ok(())
9381034
}
9391035
}

0 commit comments

Comments
 (0)