From 23fa1838194ee61caecc383b5681288cd7249b46 Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 24 Dec 2025 17:48:41 +0900 Subject: [PATCH 1/2] repr --- crates/vm/src/builtins/descriptor.rs | 14 ++++++++++++-- crates/vm/src/class.rs | 14 ++++++++++++++ crates/vm/src/types/slot.rs | 6 ------ 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/crates/vm/src/builtins/descriptor.rs b/crates/vm/src/builtins/descriptor.rs index 5cd54bb753d..c251ac45ee3 100644 --- a/crates/vm/src/builtins/descriptor.rs +++ b/crates/vm/src/builtins/descriptor.rs @@ -7,7 +7,7 @@ use crate::{ function::{FuncArgs, PyMethodDef, PyMethodFlags, PySetterValue}, types::{ Callable, Comparable, GetDescriptor, HashFunc, Hashable, InitFunc, PyComparisonOp, - Representable, + Representable, StringifyFunc, }, }; use rustpython_common::lock::PyRwLock; @@ -398,6 +398,7 @@ pub fn init(ctx: &Context) { pub enum SlotFunc { Init(InitFunc), Hash(HashFunc), + Repr(StringifyFunc), } impl std::fmt::Debug for SlotFunc { @@ -405,6 +406,7 @@ impl std::fmt::Debug for SlotFunc { match self { SlotFunc::Init(_) => write!(f, "SlotFunc::Init(...)"), SlotFunc::Hash(_) => write!(f, "SlotFunc::Hash(...)"), + SlotFunc::Repr(_) => write!(f, "SlotFunc::Repr(...)"), } } } @@ -426,6 +428,15 @@ impl SlotFunc { let hash = func(&obj, vm)?; Ok(vm.ctx.new_int(hash).into()) } + SlotFunc::Repr(func) => { + if !args.args.is_empty() || !args.kwargs.is_empty() { + return Err( + vm.new_type_error("__repr__() takes no arguments (1 given)".to_owned()) + ); + } + let s = func(&obj, vm)?; + Ok(s.into()) + } } } } @@ -456,7 +467,6 @@ impl GetDescriptor for PySlotWrapper { ) -> PyResult { match obj { None => Ok(zelf), - Some(obj) if vm.is_none(&obj) => Ok(zelf), Some(obj) => { let zelf = zelf.downcast::().unwrap(); Ok(PyMethodWrapper { wrapper: zelf, obj }.into_pyobject(vm)) diff --git a/crates/vm/src/class.rs b/crates/vm/src/class.rs index f258281712f..236967dd36e 100644 --- a/crates/vm/src/class.rs +++ b/crates/vm/src/class.rs @@ -174,6 +174,20 @@ pub trait PyClassImpl: PyClassDef { class.set_attr(ctx.names.__hash__, ctx.none.clone().into()); } + // Add __repr__ slot wrapper if slot exists and not already in dict + if let Some(repr_func) = class.slots.repr.load() { + let repr_name = identifier!(ctx, __repr__); + if !class.attributes.read().contains_key(repr_name) { + let wrapper = PySlotWrapper { + typ: class, + name: ctx.intern_str("__repr__"), + wrapped: SlotFunc::Repr(repr_func), + doc: Some("Return repr(self)."), + }; + class.set_attr(repr_name, wrapper.into_ref(ctx).into()); + } + } + class.extend_methods(class.slots.methods, ctx); } diff --git a/crates/vm/src/types/slot.rs b/crates/vm/src/types/slot.rs index 2954d08abf0..6e98da173a4 100644 --- a/crates/vm/src/types/slot.rs +++ b/crates/vm/src/types/slot.rs @@ -1114,12 +1114,6 @@ pub trait Representable: PyPayload { Self::repr(zelf, vm) } - #[inline] - #[pymethod] - fn __repr__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult> { - Self::slot_repr(&zelf, vm) - } - #[inline] fn repr(zelf: &Py, vm: &VirtualMachine) -> PyResult> { let repr = Self::repr_str(zelf, vm)?; From ebbbb4f12b88d87289fa1a16cfe87225dd8f9acd Mon Sep 17 00:00:00 2001 From: Jeong YunWon Date: Wed, 24 Dec 2025 21:36:42 +0900 Subject: [PATCH 2/2] uniform __str__ --- crates/stdlib/src/ssl/error.rs | 6 +- crates/vm/src/builtins/str.rs | 16 ++-- crates/vm/src/builtins/weakproxy.rs | 4 +- crates/vm/src/exception_group.rs | 6 +- crates/vm/src/exceptions.rs | 138 +++++++++++++++------------- crates/vm/src/stdlib/io.rs | 6 +- crates/vm/src/stdlib/winreg.rs | 16 ++-- 7 files changed, 104 insertions(+), 88 deletions(-) diff --git a/crates/stdlib/src/ssl/error.rs b/crates/stdlib/src/ssl/error.rs index 879275228ec..d77910f6aa1 100644 --- a/crates/stdlib/src/ssl/error.rs +++ b/crates/stdlib/src/ssl/error.rs @@ -5,8 +5,8 @@ pub(crate) use ssl_error::*; #[pymodule(sub)] pub(crate) mod ssl_error { use crate::vm::{ - PyPayload, PyRef, PyResult, VirtualMachine, - builtins::{PyBaseExceptionRef, PyOSError, PyStrRef}, + Py, PyPayload, PyRef, PyResult, VirtualMachine, + builtins::{PyBaseException, PyOSError, PyStrRef}, types::{Constructor, Initializer}, }; @@ -42,7 +42,7 @@ pub(crate) mod ssl_error { impl PySSLError { // Returns strerror attribute if available, otherwise str(args) #[pymethod] - fn __str__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult { + fn __str__(exc: &Py, vm: &VirtualMachine) -> PyResult { use crate::vm::AsObject; // Try to get strerror attribute first (OSError compatibility) if let Ok(strerror) = exc.as_object().get_attr("strerror", vm) diff --git a/crates/vm/src/builtins/str.rs b/crates/vm/src/builtins/str.rs index 8084c4d053e..95b41e6d55c 100644 --- a/crates/vm/src/builtins/str.rs +++ b/crates/vm/src/builtins/str.rs @@ -529,7 +529,6 @@ impl Py { #[pyclass( flags(BASETYPE, _MATCH_SELF), with( - PyRef, AsMapping, AsNumber, AsSequence, @@ -1448,15 +1447,16 @@ impl PyStr { fn __getnewargs__(zelf: PyRef, vm: &VirtualMachine) -> PyObjectRef { (zelf.as_str(),).to_pyobject(vm) } -} -#[pyclass] -impl PyRef { #[pymethod] - fn __str__(self, vm: &VirtualMachine) -> PyRefExact { - self.into_exact_or(&vm.ctx, |zelf| { - PyStr::from(zelf.data.clone()).into_exact_ref(&vm.ctx) - }) + fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + if zelf.class().is(vm.ctx.types.str_type) { + // Already exact str, just return a reference + Ok(zelf.to_owned()) + } else { + // Subclass, create a new exact str + Ok(PyStr::from(zelf.data.clone()).into_ref(&vm.ctx)) + } } } diff --git a/crates/vm/src/builtins/weakproxy.rs b/crates/vm/src/builtins/weakproxy.rs index a9221ec876f..6e0e8308dbc 100644 --- a/crates/vm/src/builtins/weakproxy.rs +++ b/crates/vm/src/builtins/weakproxy.rs @@ -79,8 +79,8 @@ impl PyWeakProxy { } #[pymethod] - fn __str__(&self, vm: &VirtualMachine) -> PyResult { - self.try_upgrade(vm)?.str(vm) + fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + zelf.try_upgrade(vm)?.str(vm) } fn len(&self, vm: &VirtualMachine) -> PyResult { diff --git a/crates/vm/src/exception_group.rs b/crates/vm/src/exception_group.rs index e19dbceb8da..645a3e779ff 100644 --- a/crates/vm/src/exception_group.rs +++ b/crates/vm/src/exception_group.rs @@ -182,7 +182,7 @@ pub(super) mod types { } #[pymethod] - fn __str__(zelf: PyRef, vm: &VirtualMachine) -> PyResult { + fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { let message = zelf .get_arg(0) .map(|m| m.str(vm)) @@ -196,10 +196,10 @@ pub(super) mod types { .unwrap_or(0); let suffix = if num_excs == 1 { "" } else { "s" }; - Ok(format!( + Ok(vm.ctx.new_str(format!( "{} ({} sub-exception{})", message, num_excs, suffix - )) + ))) } #[pymethod] diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index 2c36aa13bd5..3a932a5df54 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -560,7 +560,7 @@ impl PyBaseException { } #[pyclass( - with(PyRef, Constructor, Initializer, Representable), + with(Py, PyRef, Constructor, Initializer, Representable), flags(BASETYPE, HAS_DICT) )] impl PyBaseException { @@ -633,15 +633,18 @@ impl PyBaseException { fn set_suppress_context(&self, suppress_context: bool) { self.suppress_context.store(suppress_context); } +} +#[pyclass] +impl Py { #[pymethod] - pub(super) fn __str__(&self, vm: &VirtualMachine) -> PyStrRef { + pub(super) fn __str__(&self, vm: &VirtualMachine) -> PyResult { let str_args = vm.exception_args_as_string(self.args(), true); - match str_args.into_iter().exactly_one() { + Ok(match str_args.into_iter().exactly_one() { Err(i) if i.len() == 0 => vm.ctx.empty_str.to_owned(), Ok(s) => s, Err(i) => PyStr::from(format!("({})", i.format(", "))).into_ref(&vm.ctx), - } + }) } } @@ -1527,16 +1530,16 @@ pub(super) mod types { #[pyexception] impl PyKeyError { #[pymethod] - fn __str__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyStrRef { - let args = exc.args(); - if args.len() == 1 { + fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let args = zelf.args(); + Ok(if args.len() == 1 { vm.exception_args_as_string(args, false) .into_iter() .exactly_one() .unwrap() } else { - exc.__str__(vm) - } + zelf.__str__(vm)? + }) } } @@ -1731,8 +1734,8 @@ pub(super) mod types { #[pyexception(with(Constructor, Initializer))] impl PyOSError { #[pymethod] - fn __str__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult { - let obj = exc.as_object().to_owned(); + fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let obj = zelf.as_object(); // Get OSError fields directly let errno_field = obj.get_attr("errno", vm).ok().filter(|v| !vm.is_none(v)); @@ -1819,7 +1822,7 @@ pub(super) mod types { } // fallback to BaseException.__str__ - Ok(exc.__str__(vm)) + zelf.__str__(vm) } #[pymethod] @@ -2026,7 +2029,7 @@ pub(super) mod types { #[pyexception(with(Initializer))] impl PySyntaxError { #[pymethod] - fn __str__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyStrRef { + fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { fn basename(filename: &str) -> &str { let splitted = if cfg!(windows) { filename.rsplit(&['/', '\\']).next() @@ -2036,16 +2039,16 @@ pub(super) mod types { splitted.unwrap_or(filename) } - let maybe_lineno = exc.as_object().get_attr("lineno", vm).ok().map(|obj| { + let maybe_lineno = zelf.as_object().get_attr("lineno", vm).ok().map(|obj| { obj.str(vm) .unwrap_or_else(|_| vm.ctx.new_str("")) }); - let maybe_filename = exc.as_object().get_attr("filename", vm).ok().map(|obj| { + let maybe_filename = zelf.as_object().get_attr("filename", vm).ok().map(|obj| { obj.str(vm) .unwrap_or_else(|_| vm.ctx.new_str("")) }); - let args = exc.args(); + let args = zelf.args(); let msg = if args.len() == 1 { vm.exception_args_as_string(args, false) @@ -2053,7 +2056,7 @@ pub(super) mod types { .exactly_one() .unwrap() } else { - return exc.__str__(vm); + return zelf.__str__(vm); }; let msg_with_location_info: String = match (maybe_lineno, maybe_filename) { @@ -2069,7 +2072,7 @@ pub(super) mod types { (None, None) => msg.to_string(), }; - vm.ctx.new_str(msg_with_location_info) + Ok(vm.ctx.new_str(msg_with_location_info)) } } @@ -2178,29 +2181,32 @@ pub(super) mod types { #[pyexception(with(Initializer))] impl PyUnicodeDecodeError { #[pymethod] - fn __str__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult { - let Ok(object) = exc.as_object().get_attr("object", vm) else { - return Ok("".to_owned()); + fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let Ok(object) = zelf.as_object().get_attr("object", vm) else { + return Ok(vm.ctx.empty_str.to_owned()); }; let object: ArgBytesLike = object.try_into_value(vm)?; - let encoding: PyStrRef = exc + let encoding: PyStrRef = zelf .as_object() .get_attr("encoding", vm)? .try_into_value(vm)?; - let start: usize = exc.as_object().get_attr("start", vm)?.try_into_value(vm)?; - let end: usize = exc.as_object().get_attr("end", vm)?.try_into_value(vm)?; - let reason: PyStrRef = exc.as_object().get_attr("reason", vm)?.try_into_value(vm)?; - if start < object.len() && end <= object.len() && end == start + 1 { + let start: usize = zelf.as_object().get_attr("start", vm)?.try_into_value(vm)?; + let end: usize = zelf.as_object().get_attr("end", vm)?.try_into_value(vm)?; + let reason: PyStrRef = zelf + .as_object() + .get_attr("reason", vm)? + .try_into_value(vm)?; + Ok(vm.ctx.new_str(if start < object.len() && end <= object.len() && end == start + 1 { let b = object.borrow_buf()[start]; - Ok(format!( + format!( "'{encoding}' codec can't decode byte {b:#02x} in position {start}: {reason}" - )) + ) } else { - Ok(format!( + format!( "'{encoding}' codec can't decode bytes in position {start}-{}: {reason}", end - 1, - )) - } + ) + })) } } @@ -2232,30 +2238,33 @@ pub(super) mod types { #[pyexception(with(Initializer))] impl PyUnicodeEncodeError { #[pymethod] - fn __str__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult { - let Ok(object) = exc.as_object().get_attr("object", vm) else { - return Ok("".to_owned()); + fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let Ok(object) = zelf.as_object().get_attr("object", vm) else { + return Ok(vm.ctx.empty_str.to_owned()); }; let object: PyStrRef = object.try_into_value(vm)?; - let encoding: PyStrRef = exc + let encoding: PyStrRef = zelf .as_object() .get_attr("encoding", vm)? .try_into_value(vm)?; - let start: usize = exc.as_object().get_attr("start", vm)?.try_into_value(vm)?; - let end: usize = exc.as_object().get_attr("end", vm)?.try_into_value(vm)?; - let reason: PyStrRef = exc.as_object().get_attr("reason", vm)?.try_into_value(vm)?; - if start < object.char_len() && end <= object.char_len() && end == start + 1 { + let start: usize = zelf.as_object().get_attr("start", vm)?.try_into_value(vm)?; + let end: usize = zelf.as_object().get_attr("end", vm)?.try_into_value(vm)?; + let reason: PyStrRef = zelf + .as_object() + .get_attr("reason", vm)? + .try_into_value(vm)?; + Ok(vm.ctx.new_str(if start < object.char_len() && end <= object.char_len() && end == start + 1 { let ch = object.as_wtf8().code_points().nth(start).unwrap(); - Ok(format!( + format!( "'{encoding}' codec can't encode character '{}' in position {start}: {reason}", UnicodeEscapeCodepoint(ch) - )) + ) } else { - Ok(format!( + format!( "'{encoding}' codec can't encode characters in position {start}-{}: {reason}", end - 1, - )) - } + ) + })) } } @@ -2286,26 +2295,31 @@ pub(super) mod types { #[pyexception(with(Initializer))] impl PyUnicodeTranslateError { #[pymethod] - fn __str__(exc: PyBaseExceptionRef, vm: &VirtualMachine) -> PyResult { - let Ok(object) = exc.as_object().get_attr("object", vm) else { - return Ok("".to_owned()); + fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let Ok(object) = zelf.as_object().get_attr("object", vm) else { + return Ok(vm.ctx.empty_str.to_owned()); }; let object: PyStrRef = object.try_into_value(vm)?; - let start: usize = exc.as_object().get_attr("start", vm)?.try_into_value(vm)?; - let end: usize = exc.as_object().get_attr("end", vm)?.try_into_value(vm)?; - let reason: PyStrRef = exc.as_object().get_attr("reason", vm)?.try_into_value(vm)?; - if start < object.char_len() && end <= object.char_len() && end == start + 1 { - let ch = object.as_wtf8().code_points().nth(start).unwrap(); - Ok(format!( - "can't translate character '{}' in position {start}: {reason}", - UnicodeEscapeCodepoint(ch) - )) - } else { - Ok(format!( - "can't translate characters in position {start}-{}: {reason}", - end - 1, - )) - } + let start: usize = zelf.as_object().get_attr("start", vm)?.try_into_value(vm)?; + let end: usize = zelf.as_object().get_attr("end", vm)?.try_into_value(vm)?; + let reason: PyStrRef = zelf + .as_object() + .get_attr("reason", vm)? + .try_into_value(vm)?; + Ok(vm.ctx.new_str( + if start < object.char_len() && end <= object.char_len() && end == start + 1 { + let ch = object.as_wtf8().code_points().nth(start).unwrap(); + format!( + "can't translate character '{}' in position {start}: {reason}", + UnicodeEscapeCodepoint(ch) + ) + } else { + format!( + "can't translate characters in position {start}-{}: {reason}", + end - 1, + ) + }, + )) } } diff --git a/crates/vm/src/stdlib/io.rs b/crates/vm/src/stdlib/io.rs index 2c3c02ec65c..9402660a86e 100644 --- a/crates/vm/src/stdlib/io.rs +++ b/crates/vm/src/stdlib/io.rs @@ -1466,7 +1466,11 @@ mod _io { let zelf: PyRef = zelf.try_into_value(vm)?; let (raw, BufferSize { buffer_size }): (PyObjectRef, _) = args.bind(vm).map_err(|e| { - let msg = format!("{}() {}", Self::CLASS_NAME, *e.__str__(vm)); + let str_repr = e + .__str__(vm) + .map(|s| s.as_str().to_owned()) + .unwrap_or_else(|_| "".to_owned()); + let msg = format!("{}() {}", Self::CLASS_NAME, str_repr); vm.new_exception_msg(e.class().to_owned(), msg) })?; zelf.init(raw, BufferSize { buffer_size }, vm) diff --git a/crates/vm/src/stdlib/winreg.rs b/crates/vm/src/stdlib/winreg.rs index a7619025866..b5e568fce6d 100644 --- a/crates/vm/src/stdlib/winreg.rs +++ b/crates/vm/src/stdlib/winreg.rs @@ -9,7 +9,7 @@ pub(crate) fn make_module(vm: &VirtualMachine) -> PyRef { #[pymodule] mod winreg { - use crate::builtins::{PyInt, PyTuple, PyTypeRef}; + use crate::builtins::{PyInt, PyStr, PyTuple, PyTypeRef}; use crate::common::hash::PyHash; use crate::common::windows::ToWideString; use crate::convert::TryFromObject; @@ -233,8 +233,8 @@ mod winreg { } #[pymethod] - fn __str__(&self) -> String { - format!("", self.hkey.load()) + fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult> { + Ok(vm.ctx.new_str(format!("", zelf.hkey.load()))) } } @@ -1029,7 +1029,7 @@ mod winreg { return Ok(Some(vec![0u8, 0u8])); } let s = value - .downcast::() + .downcast::() .map_err(|_| vm.new_type_error("value must be a string".to_string()))?; let wide = s.as_str().to_wide_with_nul(); // Convert Vec to Vec @@ -1047,11 +1047,9 @@ mod winreg { let mut bytes: Vec = Vec::new(); for item in list.borrow_vec().iter() { - let s = item - .downcast_ref::() - .ok_or_else(|| { - vm.new_type_error("list items must be strings".to_string()) - })?; + let s = item.downcast_ref::().ok_or_else(|| { + vm.new_type_error("list items must be strings".to_string()) + })?; let wide = s.as_str().to_wide_with_nul(); bytes.extend(wide.iter().flat_map(|&c| c.to_le_bytes())); }