outl_exec/runtimes/
python.rs1use std::time::Instant;
18
19use rustpython_vm::{compiler, scope::Scope, Interpreter, PyObjectRef, Settings, VirtualMachine};
20
21use crate::runtime::{ExecContext, ExecError, ExecOutput, ExitStatus, OutputFormat, Runtime};
22
23pub struct PythonRuntime;
25
26const PRELUDE: &str = r#"
27__outl_out = []
28def print(*args, sep=' ', end='\n', **_kw):
29 __outl_out.append(sep.join(str(a) for a in args) + end)
30"#;
31
32impl Runtime for PythonRuntime {
33 fn language(&self) -> &'static str {
34 "python"
35 }
36
37 fn execute(&self, source: &str, _ctx: &ExecContext) -> Result<ExecOutput, ExecError> {
38 let start = Instant::now();
39 let interpreter = Interpreter::without_stdlib(Settings::default());
40
41 let outcome = interpreter.enter(|vm| -> Result<String, PyErrString> {
42 let scope = vm.new_scope_with_builtins();
43
44 run_code(vm, &scope, PRELUDE, "<prelude>")?;
45 run_code(vm, &scope, source, "<block>")?;
46
47 let key = vm.ctx.new_str("__outl_out");
49 let captured: PyObjectRef = scope
50 .globals
51 .get_item(&*key, vm)
52 .map_err(|e| pyerr_string(vm, e))?;
53
54 let len = vm
56 .call_method(&captured, "__len__", ())
57 .and_then(|v| v.try_int(vm).map(|i| i.as_bigint().clone()));
58 let len = match len {
59 Ok(n) => n.to_string().parse::<usize>().unwrap_or(0),
60 Err(_) => 0,
61 };
62
63 let mut buf = String::new();
64 for i in 0..len {
65 let idx: PyObjectRef = vm.ctx.new_int(i).into();
66 let item = vm
67 .call_method(&captured, "__getitem__", (idx,))
68 .map_err(|e| pyerr_string(vm, e))?;
69 let s = item.str(vm).map_err(|e| pyerr_string(vm, e))?;
70 let chunk = s
76 .to_str()
77 .ok_or_else(|| PyErrString("python stdout has non-UTF-8 bytes".into()))?;
78 buf.push_str(chunk);
79 }
80 Ok(buf)
81 });
82
83 match outcome {
84 Ok(stdout) => Ok(ExecOutput {
85 stdout,
86 stderr: String::new(),
87 duration: start.elapsed(),
88 exit: ExitStatus::Ok,
89 format: OutputFormat::Text,
90 }),
91 Err(stderr) => Ok(ExecOutput {
92 stdout: String::new(),
93 stderr: stderr.0,
94 duration: start.elapsed(),
95 exit: ExitStatus::Trap("python-error".into()),
96 format: OutputFormat::Text,
97 }),
98 }
99 }
100}
101
102struct PyErrString(String);
103
104fn run_code(
105 vm: &VirtualMachine,
106 scope: &Scope,
107 source: &str,
108 label: &str,
109) -> Result<(), PyErrString> {
110 let code = vm
111 .compile(source, compiler::Mode::Exec, label.to_string())
112 .map_err(|e| PyErrString(format!("{e}")))?;
113 vm.run_code_obj(code, scope.clone())
114 .map_err(|e| pyerr_string(vm, e))?;
115 Ok(())
116}
117
118fn pyerr_string(
119 vm: &VirtualMachine,
120 e: rustpython_vm::PyRef<rustpython_vm::builtins::PyBaseException>,
121) -> PyErrString {
122 let mut buf = String::new();
125 let _ = vm.write_exception(&mut buf, &e);
126 PyErrString(buf)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 fn run(src: &str) -> String {
134 PythonRuntime
135 .execute(src, &ExecContext::default())
136 .unwrap()
137 .stdout
138 }
139
140 #[test]
141 fn print_writes_stdout() {
142 assert_eq!(run("print(1 + 2)"), "3\n");
143 }
144
145 #[test]
146 fn list_comprehension() {
147 assert_eq!(run("print([x*x for x in range(4)])"), "[0, 1, 4, 9]\n");
148 }
149
150 #[test]
151 fn fstrings_and_dicts() {
152 let out = run(r#"d={'a':1}; print(f"d={d}")"#);
153 assert!(out.contains("d={'a': 1}"));
154 }
155
156 #[test]
157 fn print_sep_end_kwargs() {
158 assert_eq!(run("print('a','b',sep='-',end='!')"), "a-b!");
159 }
160
161 #[test]
162 fn syntax_error_returns_trap() {
163 let out = PythonRuntime
164 .execute("def (", &ExecContext::default())
165 .unwrap();
166 assert!(matches!(out.exit, ExitStatus::Trap(_)));
167 }
168
169 #[test]
170 fn runtime_exception_returns_trap_with_traceback() {
171 let out = PythonRuntime
172 .execute("raise ValueError('boom')", &ExecContext::default())
173 .unwrap();
174 assert!(matches!(out.exit, ExitStatus::Trap(_)));
175 assert!(out.stderr.contains("ValueError"));
176 }
177}