Fix -m argv[0] and file SyntaxError display (#8684)

* Fix -m argv[0] and file SyntaxError display

Set sys.argv[0] to -m for module execution. Convert an escalated
invalid-escape SyntaxWarning into a SyntaxError that spans just
the `\X` pair. Report null bytes in source files with filename,
lineno, and the truncated line.

Enable the four corresponding test_cmd_line_script cases.

Assisted-by: Grok:4.6

* Pass SyntaxError text when escalating escape warnings

Keep the warning and SyntaxError messages as separate strings at
the invalid-escape site. CompileWarningError carries an optional
replacement instead of rewriting the warning text later.

Assisted-by: Grok:4.6

* Remove expectedFailure from bytes invalid-escape test

The invalid-escape SyntaxError path now satisfies
test_eval_bytes_invalid_escape.

Assisted-by: Grok:4.6
This commit is contained in:
Jeong, YunWon authored and GitHub committed 2026-09-11 09:54:59 +09:00
1 parent 1345a82443
commit cfee8a7ffa
5 files changed
+76 -24

No files matched your search

-4
View File
@@ -387,7 +387,6 @@ class CmdLineTest(unittest.TestCase):
"be directly executed")
self._check_import_error(["-m", "test_pkg"], msg, cwd=script_dir)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_issue8202(self):
# Make sure package __init__ modules see "-m" in sys.argv0 while
# searching for the module to execute
@@ -656,7 +655,6 @@ class CmdLineTest(unittest.TestCase):
],
)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_syntaxerror_invalid_escape_sequence_multi_line(self):
script = 'foo = """\\q"""\n'
with os_helper.temp_dir() as script_dir:
@@ -673,7 +671,6 @@ class CmdLineTest(unittest.TestCase):
],
)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_syntaxerror_null_bytes(self):
script = "x = '\0' nothing to see here\n';import os;os.system('echo pwnd')\n"
with os_helper.temp_dir() as script_dir:
@@ -686,7 +683,6 @@ class CmdLineTest(unittest.TestCase):
],
)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_syntaxerror_null_bytes_in_multiline_string(self):
scripts = ["\n'''\nmultilinestring\0\n'''", "\nf'''\nmultilinestring\0\n'''"] # Both normal and f-strings
with os_helper.temp_dir() as script_dir:
-1
View File
@@ -225,7 +225,6 @@ class TestLiterals(unittest.TestCase):
self.assertRaises(SyntaxError, eval, r""" b'\x' """)
self.assertRaises(SyntaxError, eval, r""" b'\x0' """)
@unittest.expectedFailure # TODO: RUSTPYTHON
def test_eval_bytes_invalid_escape(self):
for b in range(1, 128):
if b in b"""\n\r"'01234567\\abfnrtvx""":
+46 -11
View File
@@ -26,6 +26,13 @@ pub struct CompileWarningError {
filename: String,
lineno: usize,
offset: usize,
replacement: Option<SyntaxErrorReplacement>,
}
#[derive(Debug)]
struct SyntaxErrorReplacement {
message: String,
end_offset: usize,
}
impl From<CompileError> for VmCompileError {
@@ -71,13 +78,16 @@ impl CompileWarningError {
{
return self.exception;
}
let Ok(message) = self.exception.as_object().str(vm) else {
return self.exception;
let (message, end_offset) = if let Some(replacement) = self.replacement {
(replacement.message, Some(replacement.end_offset))
} else {
let Ok(message) = self.exception.as_object().str(vm) else {
return self.exception;
};
(message.to_string_lossy().into_owned(), None)
};
let syntax_error = vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
message.as_wtf8().to_owned(),
);
let syntax_error =
vm.new_exception_msg(vm.ctx.exceptions.syntax_error.to_owned(), message.into());
syntax_error
.as_object()
.set_attr("lineno", vm.ctx.new_int(self.lineno), vm)
@@ -86,6 +96,16 @@ impl CompileWarningError {
.as_object()
.set_attr("offset", vm.ctx.new_int(self.offset), vm)
.unwrap();
if let Some(end_offset) = end_offset {
syntax_error
.as_object()
.set_attr("end_lineno", vm.ctx.new_int(self.lineno), vm)
.unwrap();
syntax_error
.as_object()
.set_attr("end_offset", vm.ctx.new_int(end_offset), vm)
.unwrap();
}
syntax_error
.as_object()
.set_attr("filename", vm.ctx.new_str(self.filename), vm)
@@ -484,6 +504,7 @@ mod escape_warnings {
filename: filename.to_owned(),
lineno,
offset,
replacement: None,
}
}
@@ -609,16 +630,20 @@ mod escape_warnings {
filename: &str,
vm: &VirtualMachine,
) -> Result<(), CompileWarningError> {
let lineno = line_number_at(source, offset);
let message = vm.ctx.new_str(format!(
let (lineno, column) = line_offset_at(source, offset);
let warning = format!(
"\"\\{ch}\" is an invalid escape sequence. \
Such sequences will not work in the future. \
Did you mean \"\\\\{ch}\"? A raw string is also an option."
));
);
let syntax_error = format!(
"\"\\{ch}\" is an invalid escape sequence. \
Did you mean \"\\\\{ch}\"? A raw string is also an option."
);
let fname = vm.ctx.new_str(filename);
warn::warn_explicit(
Some(vm.ctx.exceptions.syntax_warning.to_owned()),
message.into(),
vm.ctx.new_str(warning).into(),
fname,
lineno,
None,
@@ -627,7 +652,16 @@ mod escape_warnings {
None,
vm,
)
.map_err(|err| compile_warning_error(err, source, filename, offset))
.map_err(|exception| CompileWarningError {
exception,
filename: filename.to_owned(),
lineno,
offset: column,
replacement: Some(SyntaxErrorReplacement {
message: syntax_error,
end_offset: column + 2,
}),
})
}
fn warn_syntax_at_offset(
@@ -678,6 +712,7 @@ mod escape_warnings {
filename: filename.to_owned(),
lineno: location.line.get(),
offset: location.character_offset.get(),
replacement: None,
})
}
+29 -7
View File
@@ -55,8 +55,8 @@ impl VirtualMachine {
#[cfg(feature = "host_env")]
mod file_run {
use crate::{
Py, PyResult, VirtualMachine,
builtins::{PyCode, PyDict},
AsObject, Py, PyResult, VirtualMachine,
builtins::{PyBaseExceptionRef, PyCode, PyDict},
compiler::{self},
scope::Scope,
};
@@ -107,11 +107,8 @@ mod file_run {
}
match crate::host_env::fs::read(path) {
Ok(source_bytes) => {
if source_bytes.contains(&0) {
return Err(self.new_exception_msg(
self.ctx.exceptions.syntax_error.to_owned(),
"source code cannot contain null bytes".into(),
));
if let Some(null_at) = source_bytes.iter().position(|&b| b == 0) {
return Err(null_byte_syntax_error(self, path, &source_bytes, null_at));
}
#[cfg(feature = "parser")]
// Match compile() by honoring BOMs and encoding cookies in files.
@@ -138,6 +135,31 @@ mod file_run {
}
}
fn null_byte_syntax_error(
vm: &VirtualMachine,
path: &str,
source_bytes: &[u8],
null_at: usize,
) -> PyBaseExceptionRef {
let before = &source_bytes[..null_at];
let lineno = before.iter().filter(|&&b| b == b'\n').count() + 1;
let line_start = before
.iter()
.rposition(|&b| b == b'\n')
.map_or(0, |i| i + 1);
let line = String::from_utf8_lossy(&source_bytes[line_start..null_at]);
let syntax_error = vm.new_exception_msg(
vm.ctx.exceptions.syntax_error.to_owned(),
"source code cannot contain null bytes".into(),
);
let obj = syntax_error.as_object();
obj.set_attr("filename", vm.ctx.new_str(path), vm).unwrap();
obj.set_attr("lineno", vm.ctx.new_int(lineno), vm).unwrap();
obj.set_attr("text", vm.ctx.new_str(format!("{line}\n")), vm)
.unwrap();
syntax_error
}
fn set_main_loader(
module_dict: &Py<PyDict>,
filename: &str,
+1 -1
View File
@@ -136,7 +136,7 @@ fn parse_args() -> Result<(CliArgs, RunMode, Vec<String>), lexopt::Error> {
Short('I') => args.isolate = true,
Short('m') => {
let module = parser.value()?.string()?;
let argv = argv("PLACEHOLDER".to_owned(), parser)?;
let argv = argv("-m".to_owned(), parser)?;
return Ok((args, RunMode::Module(module), argv));
}
Short('O') => args.optimize += 1,