#![cfg(feature = "pyo3")]
use std::collections::BTreeMap;
use std::sync::Arc;
use semver::Version;
use uni_plugin::manifest::{AbiRange, PluginManifest, ProvidedSurfaces};
use uni_plugin::{
Capability, Determinism, Plugin, PluginError, PluginRegistrar, Scope, SideEffects,
};
use crate::loader::LoadOutcome;
use crate::runtime::PyPluginRuntime;
#[derive(Debug)]
pub struct PyPluginHandle {
manifest: PluginManifest,
#[allow(
dead_code,
reason = "carried to keep the runtime alive across the host's plugin lifetime"
)]
runtime: Arc<PyPluginRuntime>,
}
impl PyPluginHandle {
#[must_use]
pub fn new(outcome: LoadOutcome) -> Self {
let manifest = build_manifest(&outcome);
Self {
manifest,
runtime: outcome.runtime,
}
}
#[must_use]
pub fn with_determinism(outcome: LoadOutcome, determinism: Determinism) -> Self {
let mut h = Self::new(outcome);
h.manifest.determinism = determinism;
h
}
#[must_use]
pub fn manifest_ref(&self) -> &PluginManifest {
&self.manifest
}
#[must_use]
pub fn runtime(&self) -> &Arc<PyPluginRuntime> {
&self.runtime
}
}
fn build_manifest(outcome: &LoadOutcome) -> PluginManifest {
let version = outcome
.version
.parse::<Version>()
.unwrap_or_else(|_| Version::new(0, 0, 0));
let mut declared = outcome.effective_capabilities.clone();
if !outcome.scalars_registered.is_empty() {
declared.insert(Capability::ScalarFn);
}
if !outcome.aggregates_registered.is_empty() {
declared.insert(Capability::AggregateFn);
}
if !outcome.procedures_registered.is_empty() {
declared.insert(Capability::Procedure);
}
PluginManifest {
id: outcome.plugin_id.clone(),
version,
abi: AbiRange::parse("^1").expect("static"),
depends_on: vec![],
capabilities: declared,
determinism: Determinism::SessionScoped,
side_effects: SideEffects::ReadOnly,
scope: Scope::Session,
hash: None,
signature: None,
provides: ProvidedSurfaces::default(),
docs: format!(
"PyO3 plugin `{id}` v{ver} — session-scoped Python callables registered via decorators.",
id = outcome.plugin_id,
ver = outcome.version
),
metadata: BTreeMap::new(),
}
}
impl Plugin for PyPluginHandle {
fn manifest(&self) -> &PluginManifest {
&self.manifest
}
fn register(&self, _r: &mut PluginRegistrar<'_>) -> Result<(), PluginError> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::loader::PyPluginLoader;
use pyo3::prelude::*;
use uni_plugin::{CapabilitySet, PluginId, PluginRegistrar, PluginRegistry};
#[test]
fn handle_carries_loader_outcome() {
Python::initialize();
Python::attach(|py| {
let loader = PyPluginLoader::with_default_plugin_id("ai.test.handle");
let caps = CapabilitySet::from_iter_of([Capability::ScalarFn]);
let registry = PluginRegistry::new();
let mut r =
PluginRegistrar::new(PluginId::new("ai.test.placeholder"), &caps, ®istry);
let src = r#"
db.set_version("0.2.0")
@db.scalar_fn("triple", args=["float"], returns="float", determinism="pure")
def triple(x):
return x * 3.0
"#;
let outcome = loader
.load(py, src, "ai.test.handle", &mut r, &caps)
.expect("load");
r.commit_to_registry().expect("commit");
let handle = PyPluginHandle::new(outcome);
assert_eq!(handle.manifest().id.as_str(), "ai.test.handle");
assert_eq!(handle.manifest().version.to_string(), "0.2.0");
assert!(
handle
.manifest()
.capabilities
.contains(&Capability::ScalarFn)
);
assert_eq!(handle.runtime().len(), 1);
});
}
}