Skip to content

Commit 577dcb1

Browse files
committed
Generate repr slots from methods and remove Representable
Migrate native representations across the VM, standard library and WASM to pymethod __repr__. Preserve direct helper calls, recursion guards and WTF-8 results, and let infallible methods return strings directly. Verify actual slot descriptors and dispatch through Python extra_tests. Assisted-by: OpenAI Codex:model version not exposed
1 parent 282aafe commit 577dcb1

80 files changed

Lines changed: 1945 additions & 2281 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎crates/derive-impl/src/pyclass.rs‎

Lines changed: 130 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,8 +1216,6 @@ where
12161216
// Constructor/Initializer traits
12171217
("__new__", "Constructor"),
12181218
("__init__", "Initializer"),
1219-
// Representable trait
1220-
("__repr__", "Representable"),
12211219
// ("__str__", "???"), // allow __str__
12221220
// Hashable trait
12231221
("__hash__", "Hashable"),
@@ -1342,37 +1340,45 @@ where
13421340
args.attrs.push(allow_attr);
13431341
}
13441342

1345-
// __str__ is a single-method protocol: register its implementation as a
1343+
// __str__ and __repr__ are single-method protocols: register its implementation as a
13461344
// slot and let add_operators expose the usual wrapper_descriptor. Other
13471345
// protocols still use their traits. Reuse native argument and result
13481346
// conversion so this keeps the same Rust signatures as a normal method.
1349-
if py_name == "__str__" && self.inner.attr_name == AttrName::Method && !coexist {
1347+
if matches!(py_name.as_str(), "__str__" | "__repr__")
1348+
&& self.inner.attr_name == AttrName::Method
1349+
&& !coexist
1350+
{
1351+
let slot_name = py_name.trim_matches('_');
1352+
let slot_ident = Ident::new(slot_name, ident.span());
1353+
let return_error = format!("{py_name} returned non-string (type {{}})");
13501354
if raw {
13511355
return Err(syn::Error::new_spanned(
13521356
item_attr,
1353-
"#[pymethod(raw)] cannot define '__str__'; use #[pyslot(str)] instead",
1357+
format!(
1358+
"#[pymethod(raw)] cannot define '{py_name}'; use #[pyslot({slot_name})] instead"
1359+
),
13541360
));
13551361
}
13561362
let slot_tokens = quote_spanned! { ident.span() =>
1357-
slots.str.store(Some(|zelf, vm| {
1363+
slots.#slot_ident.store(Some(|zelf, vm| {
13581364
use ::rustpython_vm::{AsObject as _, function::IntoPyNativeFn};
13591365
let result = IntoPyNativeFn::call(
13601366
&Self::#ident,
13611367
vm,
13621368
vec![zelf.to_owned()].into(),
1363-
::rustpython_vm::function::Callee::named("__str__").with_instance_arg(true),
1369+
::rustpython_vm::function::Callee::named(#py_name).with_instance_arg(true),
13641370
)?;
13651371
result.downcast::<::rustpython_vm::builtins::PyStr>().map_err(|obj| {
13661372
vm.new_type_error(format!(
1367-
"__str__ returned non-string (type {})",
1373+
#return_error,
13681374
obj.class().slot_name()
13691375
))
13701376
})
13711377
}));
13721378
};
13731379
args.context.extend_slots_items.add_item(
13741380
ident.clone(),
1375-
vec!["(slot str)".to_string()],
1381+
vec![format!("(slot {slot_name})")],
13761382
args.cfgs.to_vec(),
13771383
slot_tokens,
13781384
2,
@@ -2883,7 +2889,7 @@ fn parse_vec_ident(
28832889
}
28842890

28852891
#[cfg(test)]
2886-
mod str_slot_tests {
2892+
mod string_slot_tests {
28872893
use super::*;
28882894

28892895
fn expand(item: Item) -> String {
@@ -2892,48 +2898,62 @@ mod str_slot_tests {
28922898
.to_string()
28932899
}
28942900

2901+
fn protocols() -> [(Ident, &'static str); 2] {
2902+
[("__str__", "str"), ("__repr__", "repr")]
2903+
.map(|(method, slot)| (Ident::new(method, Span::call_site()), slot))
2904+
}
2905+
28952906
#[test]
2896-
fn str_alias_uses_native_conversion_without_method_entry() {
2897-
let tokens = expand(parse_quote! {
2898-
impl Py<Example> {
2899-
#[pymethod(name = "__str__")]
2900-
fn display(&self) -> String { String::new() }
2901-
}
2902-
});
2903-
assert!(tokens.contains("slots . str . store"));
2904-
assert!(tokens.contains("& Self :: display"));
2905-
assert!(tokens.contains("IntoPyNativeFn"));
2906-
assert!(!tokens.contains("PyMethodDef :: new_const"));
2907-
assert!(!tokens.contains("fn slot_str"));
2907+
fn alias_uses_native_conversion_without_method_entry() {
2908+
for (method, slot) in protocols() {
2909+
let method = method.to_string();
2910+
let tokens = expand(parse_quote! {
2911+
impl Py<Example> {
2912+
#[pymethod(name = #method)]
2913+
fn display(&self) -> String { String::new() }
2914+
}
2915+
});
2916+
assert!(tokens.contains(&format!("slots . {slot} . store")));
2917+
assert!(tokens.contains("& Self :: display"));
2918+
assert!(tokens.contains("IntoPyNativeFn"));
2919+
assert!(!tokens.contains("PyMethodDef :: new_const"));
2920+
assert!(!tokens.contains(&format!("fn slot_{slot}")));
2921+
}
29082922
}
29092923

29102924
#[test]
2911-
fn str_slot_preserves_cfg_on_assignment() {
2912-
let tokens = expand(parse_quote! {
2913-
impl Py<Example> {
2914-
#[cfg(feature = "example")]
2915-
#[pymethod]
2916-
fn __str__(&self) -> String { String::new() }
2917-
}
2918-
});
2919-
assert!(tokens.contains("# [cfg (feature = \"example\")] { slots . str . store"));
2920-
assert_eq!(tokens.matches("cfg (feature = \"example\")").count(), 2);
2921-
assert!(!tokens.contains("fn slot_str"));
2925+
fn slot_preserves_cfg_on_assignment() {
2926+
for (method, slot) in protocols() {
2927+
let tokens = expand(parse_quote! {
2928+
impl Py<Example> {
2929+
#[cfg(feature = "example")]
2930+
#[pymethod]
2931+
fn #method(&self) -> String { String::new() }
2932+
}
2933+
});
2934+
assert!(tokens.contains(&format!(
2935+
"# [cfg (feature = \"example\")] {{ slots . {slot} . store"
2936+
)));
2937+
assert_eq!(tokens.matches("cfg (feature = \"example\")").count(), 2);
2938+
assert!(!tokens.contains(&format!("fn slot_{slot}")));
2939+
}
29222940
}
29232941

29242942
#[test]
2925-
fn trait_str_is_registered_inside_extend_slots() {
2926-
let tokens = expand(parse_quote! {
2927-
trait Display: PyPayload {
2928-
#[pymethod]
2929-
fn __str__(zelf: &Py<Self>) -> String { String::new() }
2930-
}
2931-
});
2932-
assert!(tokens.contains("fn __extend_slots"));
2933-
assert!(tokens.contains("slots . str . store"));
2934-
assert!(tokens.contains("& Self :: __str__"));
2935-
assert!(!tokens.contains("fn slot_str"));
2936-
assert!(!tokens.contains("PyMethodDef :: new_const"));
2943+
fn trait_method_is_registered_inside_extend_slots() {
2944+
for (method, slot) in protocols() {
2945+
let tokens = expand(parse_quote! {
2946+
trait Display: PyPayload {
2947+
#[pymethod]
2948+
fn #method(zelf: &Py<Self>) -> String { String::new() }
2949+
}
2950+
});
2951+
assert!(tokens.contains("fn __extend_slots"));
2952+
assert!(tokens.contains(&format!("slots . {slot} . store")));
2953+
assert!(tokens.contains(&format!("& Self :: {method}")));
2954+
assert!(!tokens.contains(&format!("fn slot_{slot}")));
2955+
assert!(!tokens.contains("PyMethodDef :: new_const"));
2956+
}
29372957

29382958
let consumer = impl_pyclass_impl(
29392959
parse_quote!(with(Display)),
@@ -2945,76 +2965,86 @@ mod str_slot_tests {
29452965
}
29462966

29472967
#[test]
2948-
fn repr_still_requires_protocol_trait() {
2949-
let tokens = expand(parse_quote! {
2950-
impl Py<Example> {
2951-
#[pymethod]
2952-
fn __repr__(&self) -> String { String::new() }
2968+
fn method_is_registered_as_slot() {
2969+
for (method, slot) in protocols() {
2970+
let tokens = expand(parse_quote! {
2971+
impl Py<Example> {
2972+
#[pymethod]
2973+
fn #method(&self) -> String { String::new() }
2974+
}
2975+
});
2976+
assert!(!tokens.contains("compile_error"));
2977+
assert!(tokens.contains(&format!("slots . {slot} . store")));
2978+
assert!(tokens.contains(&format!("{method} returned non-string (type {{}})")));
2979+
assert!(!tokens.contains("PyMethodDef :: new_const"));
2980+
}
2981+
}
2982+
2983+
#[test]
2984+
fn class_and_static_methods_remain_methods() {
2985+
for (method, slot) in protocols() {
2986+
for attribute in [quote!(pyclassmethod), quote!(pystaticmethod)] {
2987+
let tokens = expand(parse_quote! {
2988+
impl Py<Example> {
2989+
#[#attribute]
2990+
fn #method() -> String { String::new() }
2991+
}
2992+
});
2993+
assert!(tokens.contains("PyMethodDef :: new_const"));
2994+
assert!(!tokens.contains(&format!("slots . {slot} . store")));
29532995
}
2954-
});
2955-
assert!(tokens.contains("compile_error"));
2956-
assert!(tokens.contains("Representable"));
2957-
assert!(!tokens.contains("slots . str . store"));
2996+
}
29582997
}
29592998

29602999
#[test]
2961-
fn class_and_static_str_remain_methods() {
2962-
for attribute in [quote!(pyclassmethod), quote!(pystaticmethod)] {
2963-
let item = syn::parse2(quote! {
3000+
fn coexist_remains_an_ordinary_method() {
3001+
for (method, slot) in protocols() {
3002+
let tokens = expand(parse_quote! {
29643003
impl Py<Example> {
2965-
#[#attribute]
2966-
fn __str__() -> String { String::new() }
3004+
#[pymethod(coexist)]
3005+
fn #method(&self) -> String { String::new() }
29673006
}
2968-
})
2969-
.unwrap();
2970-
let tokens = expand(item);
3007+
});
29713008
assert!(tokens.contains("PyMethodDef :: new_const"));
2972-
assert!(!tokens.contains("slots . str . store"));
3009+
assert!(tokens.contains("COEXIST"));
3010+
assert!(!tokens.contains(&format!("slots . {slot} . store")));
29733011
}
29743012
}
29753013

29763014
#[test]
2977-
fn coexist_str_remains_an_ordinary_method() {
2978-
let tokens = expand(parse_quote! {
2979-
impl Py<Example> {
2980-
#[pymethod(coexist)]
2981-
fn __str__(&self) -> String { String::new() }
2982-
}
2983-
});
2984-
assert!(tokens.contains("PyMethodDef :: new_const"));
2985-
assert!(tokens.contains("COEXIST"));
2986-
assert!(!tokens.contains("slots . str . store"));
3015+
fn explicit_and_method_slots_conflict() {
3016+
for (method, slot) in protocols() {
3017+
let slot_ident = Ident::new(slot, Span::call_site());
3018+
let error = impl_pyclass_impl(
3019+
parse_quote!(),
3020+
parse_quote! {
3021+
impl Py<Example> {
3022+
#[pymethod]
3023+
fn #method(&self) -> String { String::new() }
3024+
#[pyslot(#slot_ident)]
3025+
fn stringify(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyStrRef> {
3026+
todo!()
3027+
}
3028+
}
3029+
},
3030+
)
3031+
.expect_err("two implementations of the same slot must be rejected");
3032+
assert!(error.to_string().contains(&format!("(slot {slot})")));
3033+
}
29873034
}
29883035

29893036
#[test]
2990-
fn explicit_and_method_str_slots_conflict() {
2991-
let error = impl_pyclass_impl(
2992-
parse_quote!(),
2993-
parse_quote! {
3037+
fn raw_method_reports_an_explicit_error() {
3038+
for (method, slot) in protocols() {
3039+
let tokens = expand(parse_quote! {
29943040
impl Py<Example> {
2995-
#[pymethod]
2996-
fn __str__(&self) -> String { String::new() }
2997-
#[pyslot(str)]
2998-
fn stringify(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyStrRef> {
2999-
todo!()
3000-
}
3041+
#[pymethod(raw)]
3042+
fn #method(vm: &VirtualMachine, args: FuncArgs) -> PyResult { todo!() }
30013043
}
3002-
},
3003-
)
3004-
.expect_err("two str slots must be rejected");
3005-
assert!(error.to_string().contains("(slot str)"));
3006-
}
3007-
3008-
#[test]
3009-
fn raw_str_reports_an_explicit_error() {
3010-
let tokens = expand(parse_quote! {
3011-
impl Py<Example> {
3012-
#[pymethod(raw)]
3013-
fn __str__(vm: &VirtualMachine, args: FuncArgs) -> PyResult { todo!() }
3014-
}
3015-
});
3016-
assert!(tokens.contains("compile_error"));
3017-
assert!(tokens.contains("#[pymethod(raw)] cannot define '__str__'"));
3018-
assert!(!tokens.contains("slots . str . store"));
3044+
});
3045+
assert!(tokens.contains("compile_error"));
3046+
assert!(tokens.contains(&format!("#[pymethod(raw)] cannot define '{method}'")));
3047+
assert!(!tokens.contains(&format!("slots . {slot} . store")));
3048+
}
30193049
}
30203050
}

0 commit comments

Comments
 (0)