1use pyo3::prelude::*;
13use pyo3::types::{PyDict, PyModule, PyTuple};
14use quarb::Value;
15use quarb::library::{InputSlot, LibCall, LibFailure, Synopsis};
16use std::ffi::CString;
17use std::sync::OnceLock;
18
19pub struct PyGlue {
22 source: &'static str,
23 file: &'static str,
24 name: &'static str,
25 module: OnceLock<Py<PyModule>>,
26}
27
28impl PyGlue {
29 pub const fn new(source: &'static str, file: &'static str, name: &'static str) -> Self {
30 PyGlue {
31 source,
32 file,
33 name,
34 module: OnceLock::new(),
35 }
36 }
37
38 pub(crate) fn module<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyModule>> {
39 if let Some(m) = self.module.get() {
40 return Ok(m.bind(py).clone());
41 }
42 crate::foreign::register(py)?;
45 let m = PyModule::from_code(
46 py,
47 &CString::new(self.source).expect("glue has no NUL"),
48 &CString::new(self.file).expect("file name has no NUL"),
49 &CString::new(self.name).expect("module name has no NUL"),
50 )?;
51 let _ = self.module.set(m.clone().unbind());
52 Ok(m)
53 }
54
55 pub fn call(&self, syn: &Synopsis, name: &str, call: LibCall) -> Result<Value, LibFailure> {
61 let (positional, keyword) = arguments(syn, call);
62 Python::attach(|py| -> Result<Value, LibFailure> {
63 let failed = |e: PyErr| LibFailure::Failed(format!("{e}"));
64 let m = self.module(py).map_err(failed)?;
65 let (args, kwargs) = python_arguments(py, &positional, &keyword).map_err(failed)?;
66 let out = m
67 .getattr("_call")
68 .map_err(failed)?
69 .call1((name, args, kwargs))
70 .map_err(failed)?;
71 crate::py_to_value(&out).map_err(failed)
72 })
73 }
74}
75
76pub(crate) fn arguments(syn: &Synopsis, call: LibCall) -> (Vec<Value>, Vec<(String, Value)>) {
81 let (positional, keyword, _) = placed(syn, call);
82 (positional, keyword)
83}
84
85pub(crate) fn placed(
90 syn: &Synopsis,
91 call: LibCall,
92) -> (Vec<Value>, Vec<(String, Value)>, Option<usize>) {
93 let mut positional: Vec<Value> = call.operands;
94 let mut keyword: Vec<(String, Value)> = Vec::new();
95 let mut at = None;
96 if let Some(input) = call.input {
97 match syn.input {
98 InputSlot::Operand(i) => {
99 let i = i.min(positional.len());
100 positional.insert(i, input);
101 at = Some(i);
102 }
103 InputSlot::OperandOrSelf(i) => {
104 let fixed = syn.operands.iter().filter(|o| o.name != "...").count();
105 let i = if positional.len() >= fixed {
106 0
107 } else {
108 i.min(positional.len())
109 };
110 positional.insert(i, input);
111 at = Some(i);
112 }
113 InputSlot::Option(long) => keyword.push((long.to_string(), input)),
114 InputSlot::Context => {}
117 }
118 }
119 for (long, value) in call.options {
120 keyword.push((long.replace('-', "_"), value.unwrap_or(Value::Bool(true))));
121 }
122 (positional, keyword, at)
123}
124
125pub(crate) fn python_arguments<'py>(
126 py: Python<'py>,
127 positional: &[Value],
128 keyword: &[(String, Value)],
129) -> PyResult<(Bound<'py, PyTuple>, Bound<'py, PyDict>)> {
130 let args = PyTuple::new(
131 py,
132 positional
133 .iter()
134 .map(|v| crate::value_to_py(py, v))
135 .collect::<PyResult<Vec<_>>>()?,
136 )?;
137 let kwargs = PyDict::new(py);
138 for (k, v) in keyword {
139 kwargs.set_item(k, crate::value_to_py(py, v)?)?;
140 }
141 Ok((args, kwargs))
142}
143
144pub fn importable(modules: &[&str], pip: &str) -> Result<(), String> {
147 Python::attach(|py| {
148 crate::foreign::extend_sys_path(py).map_err(|e| e.to_string())?;
151 for m in modules {
152 py.import(*m).map_err(|e| {
153 format!(
154 "the Python package {m} is not importable ({e}); install it: pip install {pip}"
155 )
156 })?;
157 }
158 Ok(())
159 })
160}