use pyo3::prelude::*;
use pyo3::types::PyAnyMethods;
use fidius_core::PluginError;
use crate::error::pyerr_to_plugin_error;
use crate::value_bridge::pyobject_to_value;
pub enum PyStreamStep {
Item(serde_json::Value),
End,
Error(PluginError),
}
pub struct PythonStream {
iter: Py<PyAny>,
}
impl PythonStream {
pub(crate) fn new(iter: Py<PyAny>) -> Self {
Self { iter }
}
pub fn next(&self) -> PyStreamStep {
Python::with_gil(|py| {
let it = self.iter.bind(py);
match it.call_method0("__next__") {
Ok(item) => match pyobject_to_value(&item) {
Ok(v) => PyStreamStep::Item(v),
Err(e) => PyStreamStep::Error(PluginError::new("OutputEncode", e.to_string())),
},
Err(e) if e.is_instance_of::<pyo3::exceptions::PyStopIteration>(py) => {
PyStreamStep::End
}
Err(e) => PyStreamStep::Error(pyerr_to_plugin_error(e)),
}
})
}
pub fn cancel(&self) {
Python::with_gil(|py| {
let it = self.iter.bind(py);
if let Ok(close) = it.getattr("close") {
let _ = close.call0();
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::interpreter::ensure_initialized;
fn stream_from(code: &str) -> PythonStream {
ensure_initialized();
Python::with_gil(|py| {
let obj = py
.eval(&std::ffi::CString::new(code).unwrap(), None, None)
.expect("eval");
let iter = obj.try_iter().expect("iterable");
PythonStream::new(iter.into_any().unbind())
})
}
fn item_i64(step: PyStreamStep) -> i64 {
match step {
PyStreamStep::Item(v) => v.as_i64().expect("int item"),
other => panic!("expected item, got {}", step_name(&other)),
}
}
fn step_name(s: &PyStreamStep) -> &'static str {
match s {
PyStreamStep::Item(_) => "Item",
PyStreamStep::End => "End",
PyStreamStep::Error(_) => "Error",
}
}
#[test]
fn yields_items_then_end() {
let s = stream_from("iter([1, 2, 3])");
assert_eq!(item_i64(s.next()), 1);
assert_eq!(item_i64(s.next()), 2);
assert_eq!(item_i64(s.next()), 3);
assert!(matches!(s.next(), PyStreamStep::End));
assert!(matches!(s.next(), PyStreamStep::End));
}
#[test]
fn generator_exception_becomes_error() {
let s = gen_from_def("def g():\n yield 7\n raise ValueError('boom')\nit = g()");
assert_eq!(item_i64(s.next()), 7);
match s.next() {
PyStreamStep::Error(pe) => {
assert!(pe.message.contains("boom"), "message was: {}", pe.message)
}
other => panic!("expected error, got {}", step_name(&other)),
}
assert!(matches!(s.next(), PyStreamStep::End));
}
fn gen_from_def(code: &str) -> PythonStream {
ensure_initialized();
Python::with_gil(|py| {
let globals = pyo3::types::PyDict::new(py);
py.run(&std::ffi::CString::new(code).unwrap(), Some(&globals), None)
.unwrap();
let it = globals.get_item("it").unwrap().unwrap();
PythonStream::new(it.unbind())
})
}
#[test]
fn cancel_runs_generator_finally() {
ensure_initialized();
let (stream, globals) = Python::with_gil(|py| {
let globals = pyo3::types::PyDict::new(py);
py.run(
&std::ffi::CString::new(
"ran = {'cleanup': False}\n\
def g():\n \
try:\n \
yield 1\n \
yield 2\n \
finally:\n \
ran['cleanup'] = True\n\
it = g()",
)
.unwrap(),
Some(&globals),
None,
)
.unwrap();
let it = globals.get_item("it").unwrap().unwrap();
(PythonStream::new(it.unbind()), globals.unbind())
});
assert_eq!(item_i64(stream.next()), 1);
stream.cancel();
Python::with_gil(|py| {
let g = globals.bind(py);
let ran = g.get_item("ran").unwrap().unwrap();
let cleanup: bool = ran.get_item("cleanup").unwrap().extract().unwrap();
assert!(cleanup, "generator `finally` should run on cancel()");
});
}
}