Skip to content

Commit 4381488

Browse files
committed
fix: improve support for rust interpreter python imports from venv
Signed-off-by: Nick Mitchell <[email protected]>
1 parent 5e516bf commit 4381488

File tree

6 files changed

+122
-19
lines changed

6 files changed

+122
-19
lines changed

.github/workflows/rust-interpreter.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@ jobs:
3131
(curl -fsSL https://ollama.com/install.sh | sudo -E sh && sleep 2)
3232
wait
3333
- name: Run interpreter tests
34-
run: npm run test:interpreter
34+
run: |
35+
python3.12 -mvenv venv
36+
source venv/bin/activate
37+
pip install nested-diff
38+
npm run test:interpreter

pdl-live-react/src-tauri/Cargo.lock

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pdl-live-react/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ minijinja = { version = "2.9.0", features = ["custom_syntax"] }
3838
#ollama-rs = { version = "0.3.0", features = ["stream"] }
3939
ollama-rs = { git = "https://github.com/starpit/ollama-rs.git", branch = "tools-pub-7", features = ["stream"] }
4040
owo-colors = "4.2.0"
41-
rustpython-vm = { git="https://github.com/RustPython/RustPython.git" } # "0.4.0"
41+
rustpython-vm = { git="https://github.com/RustPython/RustPython.git", features= ["importlib", "threading", "encodings"] } # "0.4.0"
4242
async-recursion = "1.1.1"
4343
tokio-stream = "0.1.17"
4444
tokio = { version = "1.44.1", features = ["io-std"] }

pdl-live-react/src-tauri/src/pdl/interpreter.rs

Lines changed: 67 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -759,28 +759,55 @@ impl<'a> Interpreter<'a> {
759759
_state: &mut State,
760760
) -> BodyInterpretation {
761761
use rustpython_vm as vm;
762-
let interp = vm::Interpreter::with_init(vm::Settings::default(), |vm| {
763-
//vm.add_native_modules(rustpython_stdlib::get_module_inits());
762+
763+
let mut settings = rustpython_vm::Settings::default();
764+
765+
// add PYTHONPATH to sys.path
766+
settings.path_list.extend(get_paths("PDLPYTHONPATH"));
767+
settings.path_list.extend(get_paths("PYTHONPATH"));
768+
769+
if let Ok(venv) = ::std::env::var("VIRTUAL_ENV") {
770+
let path = ::std::path::PathBuf::from(venv).join(if cfg!(windows) {
771+
"lib/site-packages"
772+
} else {
773+
// TODO generalize this!
774+
"lib/python3.12/site-packages"
775+
});
776+
settings = settings.with_path(path.display().to_string());
777+
}
778+
779+
let interp = vm::Interpreter::with_init(settings, |vm| {
780+
vm.add_native_modules(rustpython_stdlib::get_module_inits());
764781
vm.add_frozen(rustpython_pylib::FROZEN_STDLIB);
765782
});
766783
interp.enter(|vm| -> BodyInterpretation {
767784
let scope = vm.new_scope_with_builtins();
768785

769-
// TODO vm.new_syntax_error(&err, Some(block.code.as_str()))
770-
let code_obj = match vm.compile(
771-
block.code.as_str(),
772-
vm::compiler::Mode::Exec,
786+
// Sigh, this is copy-pasted from RustPython/src/lib.rs
787+
// `run_rustpython` as of 20250416 commit hash
788+
// a917da3b1. Without this (and also: importlib and
789+
// encodings features on rustpython-vm crate), then
790+
// pulling in venvs does not work.
791+
match vm.run_code_string(
792+
vm.new_scope_with_builtins(),
793+
"import sys; sys.path.insert(0, '')",
773794
"<embedded>".to_owned(),
774795
) {
775-
Ok(x) => Ok(x),
776-
Err(exc) => Err(PdlError::from(format!(
777-
"Syntax error in Python code {:?}",
778-
exc
779-
))),
796+
Ok(_) => Ok(()),
797+
Err(exc) => {
798+
vm.print_exception(exc);
799+
Err(PdlError::from("Error setting up Python site path"))
800+
}
780801
}?;
802+
let site_result = vm.import("site", 0);
803+
if site_result.is_err() {
804+
println!(
805+
"Failed to import site, consider adding the Lib directory to your RUSTPYTHONPATH \
806+
environment variable",
807+
);
808+
}
781809

782-
// TODO vm.print_exception(exc);
783-
match vm.run_code_obj(code_obj, scope.clone()) {
810+
match vm.run_code_string(scope.clone(), block.code.as_str(), "<embedded>".to_owned()) {
784811
Ok(_) => Ok(()),
785812
Err(exc) => {
786813
vm.print_exception(exc);
@@ -1500,3 +1527,30 @@ pub fn load_scope(
15001527

15011528
Ok(scope)
15021529
}
1530+
1531+
/// Helper function to retrieve a sequence of paths from an environment variable.
1532+
fn get_paths(env_variable_name: &str) -> impl Iterator<Item = String> + '_ {
1533+
::std::env::var_os(env_variable_name)
1534+
.into_iter()
1535+
.flat_map(move |paths| {
1536+
split_paths(&paths)
1537+
.map(|path| {
1538+
path.into_os_string()
1539+
.into_string()
1540+
.unwrap_or_else(|_| panic!("{env_variable_name} isn't valid unicode"))
1541+
})
1542+
.collect::<Vec<_>>()
1543+
})
1544+
}
1545+
1546+
#[cfg(not(target_os = "wasi"))]
1547+
pub(crate) use ::std::env::split_paths;
1548+
#[cfg(target_os = "wasi")]
1549+
pub(crate) fn split_paths<T: AsRef<std::ffi::OsStr> + ?Sized>(
1550+
s: &T,
1551+
) -> impl Iterator<Item = std::path::PathBuf> + '_ {
1552+
use std::os::wasi::ffi::OsStrExt;
1553+
let s = s.as_ref().as_bytes();
1554+
s.split(|b| *b == b':')
1555+
.map(|x| std::ffi::OsStr::from_bytes(x).to_owned().into())
1556+
}

pdl-live-react/src-tauri/src/pdl/interpreter_tests.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,45 @@ mod tests {
318318
Ok(())
319319
}
320320

321+
#[test]
322+
fn text_python_two_code_result_dict() -> Result<(), Box<dyn Error>> {
323+
let program = json!({
324+
"text": [
325+
{ "lang": "python",
326+
"code":"print('hi ho'); result = {\"foo\": 3}"
327+
},
328+
{ "lang": "python",
329+
"code":"import os; print('hi ho'); result = {\"foo\": 4}"
330+
}
331+
]
332+
});
333+
334+
let (_, messages, _) = run_json(program, streaming(), initial_scope())?;
335+
assert_eq!(messages.len(), 2);
336+
assert_eq!(messages[0].role, MessageRole::User);
337+
assert_eq!(messages[0].content, "{'foo': 3}");
338+
assert_eq!(messages[1].role, MessageRole::User);
339+
assert_eq!(messages[1].content, "{'foo': 4}");
340+
Ok(())
341+
}
342+
343+
// TODO: illegal instruction, but only during tests
344+
#[test]
345+
fn text_python_code_import_venv() -> Result<(), Box<dyn Error>> {
346+
let program = json!({
347+
"include": "./tests/cli/code-python.pdl"
348+
});
349+
350+
let (_, messages, _) = run_json(program, streaming(), initial_scope())?;
351+
assert_eq!(messages.len(), 1);
352+
assert_eq!(messages[0].role, MessageRole::User);
353+
assert_eq!(
354+
messages[0].content,
355+
"{'foo': None, 'diff': {'D': {'two': {'N': 3}}}}"
356+
);
357+
Ok(())
358+
}
359+
321360
#[test]
322361
fn text_read_file_text() -> Result<(), Box<dyn Error>> {
323362
let program = json!({
Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
lang: python
22
code: |
3-
import os
4-
print(f"hi ho {os.getcwd()}")
5-
result = {"foo": 3}
3+
import sys # test import stdlib
4+
import os # test import stdlib
5+
print(f"!!! {sys.path}")
6+
#import textdistance # test import from venv
7+
from nested_diff import diff # test import from venv
8+
a = {'one': 1, 'two': 2, 'three': 3}
9+
b = {'one': 1, 'two': 3, 'three': 3}
10+
result = {"foo": os.getenv("FOO999999999999999999999999"), "diff": diff(a, b, O=False, U=False)}

0 commit comments

Comments
 (0)