From 544fb61e34ea10ad1bb52a2d29ad5bc5ad722173 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:32:09 +0000 Subject: [PATCH 1/4] Initial plan From e31907fcf87884ac6dc4ba809d47e83054c85221 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 14:47:12 +0000 Subject: [PATCH 2/4] fix: allow SyntaxError msg to be set Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com> --- Lib/test/test_exceptions.py | 8 +++++++ crates/vm/src/exceptions.rs | 42 ++++++++++++++++++++++++++----------- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 3db9203602..5a499fc876 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2453,6 +2453,14 @@ def test_attributes_old_constructor(self): self.assertEqual(error, the_exception.text) self.assertEqual("bad bad", the_exception.msg) + def test_msg_attribute_writable(self): + err = SyntaxError("bad bad", ("bad.py", 1, 2, "abcdefg")) + err.msg = "changed" + self.assertEqual(err.msg, "changed") + self.assertEqual(str(err), "changed (bad.py, line 1)") + del err.msg + self.assertIsNone(err.msg) + @unittest.expectedFailure # TODO: RUSTPYTHON def test_incorrect_constructor(self): args = ("bad.py", 1, 2) diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index 3a932a5df5..ff392260bb 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -9,7 +9,7 @@ use crate::{ }, class::{PyClassImpl, StaticType}, convert::{ToPyException, ToPyObject}, - function::{ArgIterable, FuncArgs, IntoFuncArgs}, + function::{ArgIterable, FuncArgs, IntoFuncArgs, PySetterValue}, py_io::{self, Write}, stdlib::sys, suggestion::offer_suggestions, @@ -994,7 +994,12 @@ impl ExceptionZoo { extend_exception!(PyRecursionError, ctx, excs.recursion_error); extend_exception!(PySyntaxError, ctx, excs.syntax_error, { - "msg" => ctx.new_readonly_getset("msg", excs.syntax_error, make_arg_getter(0)), + "msg" => ctx.new_static_getset( + "msg", + excs.syntax_error, + make_arg_getter(0), + syntax_error_set_msg, + ), // TODO: members "filename" => ctx.none(), "lineno" => ctx.none(), @@ -1041,6 +1046,23 @@ fn make_arg_getter(idx: usize) -> impl Fn(PyBaseExceptionRef) -> Option PyResult<()> { + let value = value.unwrap_or_none(vm); + let mut args = exc.args.write(); + let mut new_args = args.as_slice().to_vec(); + if new_args.is_empty() { + new_args.push(value); + } else { + new_args[0] = value; + } + *args = PyTuple::new_ref(new_args, &vm.ctx); + Ok(()) +} + fn system_exit_code(exc: PyBaseExceptionRef) -> Option { exc.args.read().first().map(|code| { match_class!(match code { @@ -2045,18 +2067,14 @@ pub(super) mod types { }); let maybe_filename = zelf.as_object().get_attr("filename", vm).ok().map(|obj| { obj.str(vm) - .unwrap_or_else(|_| vm.ctx.new_str("")) + .unwrap_or_else(|_| vm.ctx.new_str("")) }); - let args = zelf.args(); - - let msg = if args.len() == 1 { - vm.exception_args_as_string(args, false) - .into_iter() - .exactly_one() - .unwrap() - } else { - return zelf.__str__(vm); + let msg = match zelf.as_object().get_attr("msg", vm) { + Ok(obj) => obj + .str(vm) + .unwrap_or_else(|_| vm.ctx.new_str("")), + Err(_) => return zelf.__str__(vm), }; let msg_with_location_info: String = match (maybe_lineno, maybe_filename) { From 11e9e9c0485a9d554093d63b4aff0e4aa6500da8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:00:07 +0000 Subject: [PATCH 3/4] chore: refine SyntaxError msg setter Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com> --- crates/vm/src/exceptions.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/crates/vm/src/exceptions.rs b/crates/vm/src/exceptions.rs index ff392260bb..8d6ce6142b 100644 --- a/crates/vm/src/exceptions.rs +++ b/crates/vm/src/exceptions.rs @@ -1051,13 +1051,15 @@ fn syntax_error_set_msg( value: PySetterValue, vm: &VirtualMachine, ) -> PyResult<()> { - let value = value.unwrap_or_none(vm); let mut args = exc.args.write(); let mut new_args = args.as_slice().to_vec(); + // Ensure the message slot at index 0 always exists for SyntaxError.args. if new_args.is_empty() { - new_args.push(value); - } else { - new_args[0] = value; + new_args.push(vm.ctx.none()); + } + match value { + PySetterValue::Assign(value) => new_args[0] = value, + PySetterValue::Delete => new_args[0] = vm.ctx.none(), } *args = PyTuple::new_ref(new_args, &vm.ctx); Ok(()) @@ -2067,14 +2069,17 @@ pub(super) mod types { }); let maybe_filename = zelf.as_object().get_attr("filename", vm).ok().map(|obj| { obj.str(vm) - .unwrap_or_else(|_| vm.ctx.new_str("")) + .unwrap_or_else(|_| vm.ctx.new_str("")) }); let msg = match zelf.as_object().get_attr("msg", vm) { Ok(obj) => obj .str(vm) .unwrap_or_else(|_| vm.ctx.new_str("")), - Err(_) => return zelf.__str__(vm), + Err(_) => { + // Fallback to the base formatting if the msg attribute was deleted or attribute lookup fails for any reason. + return Py::::__str__(zelf, vm); + } }; let msg_with_location_info: String = match (maybe_lineno, maybe_filename) { From 9ba97e52ef70e2c54024a6b51de8a2a456d727bf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Dec 2025 23:33:19 +0000 Subject: [PATCH 4/4] chore: move SyntaxError regression test to extra_tests Co-authored-by: youknowone <69878+youknowone@users.noreply.github.com> --- DEVELOPMENT.md | 3 ++- Lib/test/test_exceptions.py | 8 -------- extra_tests/snippets/builtin_exceptions.py | 7 +++++++ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index aa7d99eef3..d5c675faca 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -130,7 +130,8 @@ repository's structure: - `stdlib`: Standard library parts implemented in rust. - `src`: using the other subcrates to bring rustpython to life. - `wasm`: Binary crate and resources for WebAssembly build -- `extra_tests`: extra integration test snippets as a supplement to `Lib/test` +- `extra_tests`: extra integration test snippets as a supplement to `Lib/test`. + Add new RustPython-only regression tests here; do not place new tests under `Lib/test`. ## Understanding Internals diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index 5a499fc876..3db9203602 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -2453,14 +2453,6 @@ def test_attributes_old_constructor(self): self.assertEqual(error, the_exception.text) self.assertEqual("bad bad", the_exception.msg) - def test_msg_attribute_writable(self): - err = SyntaxError("bad bad", ("bad.py", 1, 2, "abcdefg")) - err.msg = "changed" - self.assertEqual(err.msg, "changed") - self.assertEqual(str(err), "changed (bad.py, line 1)") - del err.msg - self.assertIsNone(err.msg) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_incorrect_constructor(self): args = ("bad.py", 1, 2) diff --git a/extra_tests/snippets/builtin_exceptions.py b/extra_tests/snippets/builtin_exceptions.py index 490f831f52..0af5cf05ea 100644 --- a/extra_tests/snippets/builtin_exceptions.py +++ b/extra_tests/snippets/builtin_exceptions.py @@ -85,6 +85,13 @@ def __init__(self, value): assert exc.offset is None assert exc.text is None +err = SyntaxError("bad bad", ("bad.py", 1, 2, "abcdefg")) +err.msg = "changed" +assert err.msg == "changed" +assert str(err) == "changed (bad.py, line 1)" +del err.msg +assert err.msg is None + # Regression to: # https://github.com/RustPython/RustPython/issues/2779