1#![doc = include_str!("../README.md")]
2
3use std::{io::Write, path::PathBuf};
4
5#[derive(Debug)]
7pub struct Error(Box<dyn std::error::Error + Send + Sync>);
8
9impl std::fmt::Display for Error {
10 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11 self.0.fmt(f)
12 }
13}
14
15impl std::error::Error for Error {
16 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
17 self.0.source()
18 }
19
20 fn description(&self) -> &str {
21 #![allow(deprecated)]
22 self.0.description()
23 }
24
25 fn cause(&self) -> Option<&dyn std::error::Error> {
26 #![allow(deprecated)]
27 self.0.cause()
28 }
29}
30
31pub type PyResult<T> = Result<T, Error>;
33
34pub struct PyEnv {
36 path: PathBuf,
37 std_out: Box<dyn Fn(&str)>,
38 std_err: Box<dyn Fn(&str)>,
39 persistent: bool,
40}
41
42impl Drop for PyEnv {
43 fn drop(&mut self) {
44 if !self.persistent {
45 if let Err(e) = std::fs::remove_dir_all(&self.path) {
46 eprintln!("Error deleting PyEnv at {}, cause: {}", self.path.display(), e);
47 }
48 }
49 }
50}
51
52impl PyEnv {
53 pub fn new(
56 path: impl Into<PathBuf>,
57 std_out: impl Fn(&str) + 'static,
58 std_err: impl Fn(&str) + 'static,
59 ) -> Self {
60 let path = path.into();
61 let persistent = true;
62 let std_out = Box::new(std_out) as Box<dyn Fn(&str)>;
63 let std_err = Box::new(std_err) as Box<dyn Fn(&str)>;
64 Self { path, std_out, std_err, persistent }
65 }
66
67 pub fn at(path: impl Into<PathBuf>) -> Self {
69 let std_out = |line: &str| std::io::stdout().write_all((line.to_string() + "\n").as_bytes())
70 .expect("Error writing line to stdout");
71 let std_err = |line: &str| std::io::stdout().write_all((line.to_string() + "\n").as_bytes())
72 .expect("Error writing line to stderr");
73 Self::new(path, std_out, std_err)
74 }
75
76 fn stream_command(&self, command: &mut std::process::Command) -> PyResult<bool> {
77 use std::io::{BufReader, BufRead};
78
79 let mut command = command
80 .stdout(std::process::Stdio::piped())
81 .stderr(std::process::Stdio::piped())
82 .spawn()
83 .map_err(|e| Error(Box::new(e)))?;
84
85 command.stdout.as_mut().map(|stdout| {
86 let reader = BufReader::new(stdout);
87 reader.lines().for_each(|line| {
88 if let Ok(line) = line {
89 (self.std_out)(&line);
90 }
91 });
92 });
93 command.stderr.as_mut().map(|stderr| {
94 let reader = BufReader::new(stderr);
95 reader.lines().for_each(|line| {
96 if let Ok(line) = line {
97 (self.std_err)(&line);
98 }
99 });
100 });
101
102 let status = command.wait().map_err(|e| Error(Box::new(e)))?;
103 Ok(status.success())
104 }
105
106 pub fn install(&self, package_name: &str) -> PyResult<&Self> {
108 self.stream_command(std::process::Command::new("python")
109 .args([
110 "-m",
111 "pip",
112 "install",
113 package_name,
114 "--target",
115 self.path
116 .join("site-packages")
117 .as_os_str()
118 .to_str()
119 .ok_or_else(|| Error("Invalid path".into()))?])
120 )?;
121 Ok(&self)
122 }
123
124 pub fn try_install(&self, package_name: &str) -> &Self {
136 self.stream_command(std::process::Command::new("python")
137 .args([
138 "-m",
139 "pip",
140 "install",
141 package_name,
142 "--target",
143 self.path
144 .join("site-packages")
145 .as_os_str()
146 .to_str()
147 .expect("Invalid path")])
148 ).unwrap();
149 &self
150 }
151
152 pub fn execute(&self, code: &str) -> PyResult<&Self> {
154 std::env::set_var("PYTHONPATH", self.path.join("site-packages"));
155 self.stream_command(
156 std::process::Command::new("python")
157 .args(["-c", code])
158 )?;
159 Ok(&self)
160 }
161
162 pub fn try_execute(&self, code: &str) -> &Self {
165 std::env::set_var("PYTHONPATH", self.path.join("site-packages"));
166 self.stream_command(
167 std::process::Command::new("python")
168 .args(["-c", code])
169 ).expect("Error executing code");
170 &self
171 }
172
173 pub fn persistent(&mut self, persistent: bool) -> &Self {
175 self.persistent = persistent;
176 self
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn test_install() -> PyResult<()> {
186 PyEnv::at("./py_test/install")
187 .install("faker")?;
188 Ok(())
189 }
190
191 #[test]
192 fn test_run() -> PyResult<()> {
193 PyEnv::at("./py_test/run")
194 .execute("print('hello world')")?;
195 Ok(())
196 }
197
198 #[test]
199 fn test_install_run() -> PyResult<()> {
200 PyEnv::at("./py_test/install_run")
201 .install("faker")?
202 .execute("import faker; print(faker.Faker().name())")?;
203 Ok(())
204 }
205
206 #[test]
207 fn test_impersistence() -> PyResult<()> {
208 PyEnv::at("./py_test/impersistence")
209 .persistent(false)
210 .install("faker")?;
211 Ok(())
212 }
213
214 #[test]
215 fn test_unwrapped_funcs() {
216 PyEnv::at("./py_test/unwrapped_funcs")
217 .try_install("faker")
218 .try_execute("import faker; print(faker.Faker().name())");
219 }
220
221 #[test]
222 fn test_fail_unwrapped_funcs() {
223 PyEnv::at("./py_test/unwrapped_funcs")
226 .try_install(". .'] / .")
227 .try_execute("qb fesaf af vv");
228 }
229}