quarb_pyvalue/module.rs
1//! A Python module mounted as an environment (spec: Environment
2//! Mounts, `py=python:./helpers.py`; Environment Commands): the
3//! module's functions as commands under the mount's alias, with the
4//! native framing. The input is the first positional parameter
5//! unless `$_` places it among the operands or in an option's value
6//! (`py:round(2)` is `round(x, 2)`, `py:sub("^get_"; ""; $_)` is
7//! `sub("^get_", "", x)`, `py:f(--data=$_)` is `f(data=x)`); the
8//! operands are the positional arguments in order, the options the
9//! keyword arguments; a raised exception is a failed call, null
10//! after `|` and a refusal after `@|`, where the one call over the
11//! stream answers one thread per returned value. The module is
12//! imported once at mount time, from a file or by its import path,
13//! and its synopses are read from the functions' signatures as a
14//! reflected library's are. An environment reaches outside the
15//! query by definition, so nothing in the module is withheld for
16//! reading a file.
17
18use crate::reflect::Reflective;
19use pyo3::prelude::*;
20use quarb::Value;
21use quarb::library::{CommandLibrary, LibCall, LibFailure, Synopsis};
22
23/// One mounted module: a command library under the mount's alias.
24pub struct ModuleEnvironment {
25 alias: String,
26 /// The target as mounted, for messages.
27 target: String,
28 /// The module's name in `sys.modules`: the import path, or a
29 /// name of the mount's own for a module file.
30 root: &'static str,
31 reflect: Reflective,
32}
33
34impl ModuleEnvironment {
35 /// Mount `target` under `alias`: a module file (`./helpers.py`,
36 /// imported from its path) or an import path (`pkg.helpers`,
37 /// imported from the Python path, the active virtualenv's
38 /// included). `Err` names what did not import, the module's own
39 /// error in it.
40 pub fn mount(alias: &str, target: &str) -> Result<ModuleEnvironment, String> {
41 let is_file = target.ends_with(".py");
42 // A module file is registered under a name of the mount's
43 // own, derived from its path, so two mounts of one file (a
44 // session remounting, two aliases) share the loaded module
45 // and two files never collide.
46 let root: &'static str = if is_file {
47 use std::hash::{Hash, Hasher};
48 let mut h = std::collections::hash_map::DefaultHasher::new();
49 target.hash(&mut h);
50 Box::leak(format!("quarb_env_{:016x}", h.finish()).into_boxed_str())
51 } else {
52 Box::leak(target.to_string().into_boxed_str())
53 };
54 // One mount at a time, the lock taken outside the
55 // interpreter so a mount that yields the GIL while its
56 // module runs cannot wait on a thread that holds both.
57 static MOUNTING: std::sync::Mutex<()> = std::sync::Mutex::new(());
58 let _one = MOUNTING.lock().unwrap_or_else(|e| e.into_inner());
59 Python::attach(|py| -> Result<(), String> {
60 crate::foreign::extend_sys_path(py).map_err(|e| e.to_string())?;
61 let fail = |e: PyErr| format!("'{alias}=python:{target}': {}", e.value(py));
62 if is_file {
63 let modules = py
64 .import("sys")
65 .map_err(fail)?
66 .getattr("modules")
67 .map_err(fail)?;
68 if modules.contains(root).map_err(fail)? {
69 // Loaded by an earlier mount of the same file.
70 return Ok(());
71 }
72 let util = py.import("importlib.util").map_err(fail)?;
73 let spec = util
74 .call_method1("spec_from_file_location", (root, target))
75 .map_err(fail)?;
76 if spec.is_none() {
77 return Err(format!("'{alias}=python:{target}': not a module file"));
78 }
79 let module = util
80 .call_method1("module_from_spec", (&spec,))
81 .map_err(fail)?;
82 // Run first, register after: a reader never meets
83 // the module half-made; the module's own relative
84 // imports do not need the entry (a file mount is a
85 // top-level module).
86 spec.getattr("loader")
87 .map_err(fail)?
88 .call_method1("exec_module", (&module,))
89 .map_err(fail)?;
90 modules.set_item(root, &module).map_err(fail)?;
91 } else {
92 py.import(root).map_err(fail)?;
93 }
94 Ok(())
95 })?;
96 Ok(ModuleEnvironment {
97 alias: alias.to_string(),
98 target: target.to_string(),
99 root,
100 reflect: Reflective::module(root),
101 })
102 }
103
104 /// The module's name in a message reads as the mount.
105 fn relabel(&self, m: String) -> String {
106 let m = m.replace(self.root, &format!("python:{}", self.target));
107 format!("environment '{}': {m}", self.alias)
108 }
109}
110
111impl std::fmt::Debug for ModuleEnvironment {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 f.debug_struct("ModuleEnvironment")
114 .field("alias", &self.alias)
115 .field("target", &self.target)
116 .finish()
117 }
118}
119
120impl CommandLibrary for ModuleEnvironment {
121 fn prefix(&self) -> &str {
122 &self.alias
123 }
124
125 /// Read from the module as the names are met: none declared.
126 fn commands(&self) -> &[Synopsis] {
127 &[]
128 }
129
130 fn resolve(&self, name: &str) -> Result<&Synopsis, String> {
131 self.reflect.resolve(name).map_err(|m| self.relabel(m))
132 }
133
134 fn call(&self, name: &str, call: LibCall) -> Result<Value, LibFailure> {
135 let syn = self.resolve(name).map_err(LibFailure::Unknown)?;
136 self.reflect.call(syn, name, call)
137 }
138}