@@ -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