Skip to main content

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}