PyO3-based Python extension module _weaveback. Exposes the
weaveback-agent-core Workspace API to Python callers.
The cdylib is built with maturin or cargo build and imported
as import _weaveback from Python.
PyWorkspace class
Wraps a Workspace instance. All methods return JSON-serialisable
Python objects via pythonize.
// <<@file weaveback-py/src/lib.rs>>=
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use pythonize::{depythonize, pythonize};
use weaveback_agent_core::{ChangePlan, Workspace, WorkspaceConfig};
#[pyclass]
struct PyWorkspace {
inner: Workspace,
}
#[pymethods]
impl PyWorkspace {
#[new]
fn new(project_root: String, db_path: String, gen_dir: String) -> Self {
let config = WorkspaceConfig {
project_root: project_root.into(),
db_path: db_path.into(),
gen_dir: gen_dir.into(),
};
Self {
inner: Workspace::open(config),
}
}
fn search(&self, py: Python<'_>, query: &str, limit: usize) -> PyResult<Py<PyAny>> {
let value = self.inner.session().search(query, limit)
.map_err(PyRuntimeError::new_err)?;
pythonize(py, &value)
.map(|value| value.unbind())
.map_err(Into::into)
}
fn trace(&self, py: Python<'_>, out_file: &str, out_line: u32, out_col: u32) -> PyResult<Py<PyAny>> {
let value = self.inner.session().trace(out_file, out_line, out_col)
.map_err(PyRuntimeError::new_err)?;
pythonize(py, &value)
.map(|value| value.unbind())
.map_err(Into::into)
}
fn validate_change_plan(&self, py: Python<'_>, plan: Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
let plan: ChangePlan = depythonize(&plan)?;
let value = self.inner.session().validate_change_plan(&plan)
.map_err(PyRuntimeError::new_err)?;
pythonize(py, &value)
.map(|value| value.unbind())
.map_err(Into::into)
}
fn preview_change_plan(&self, py: Python<'_>, plan: Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
let plan: ChangePlan = depythonize(&plan)?;
let value = self.inner.session().preview_change_plan(&plan)
.map_err(PyRuntimeError::new_err)?;
pythonize(py, &value)
.map(|value| value.unbind())
.map_err(Into::into)
}
fn apply_change_plan(&self, py: Python<'_>, plan: Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
let plan: ChangePlan = depythonize(&plan)?;
let value = self.inner.session().apply_change_plan(&plan)
.map_err(PyRuntimeError::new_err)?;
pythonize(py, &value)
.map(|value| value.unbind())
.map_err(Into::into)
}
}
#[pymodule]
fn _weaveback(_py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
module.add_class::<PyWorkspace>()?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_py_workspace_basic() {
Python::initialize();
let tmp = tempdir().unwrap();
let db_path = tmp.path().join("wb.db");
let gen_dir = tmp.path().join("gen");
std::fs::create_dir_all(&gen_dir).unwrap();
// Initialize an empty database so Workspace::open doesn't fail
weaveback_tangle::db::WeavebackDb::open(&db_path).unwrap();
Python::attach(|py| {
let ws = PyWorkspace::new(
tmp.path().to_string_lossy().to_string(),
db_path.to_string_lossy().to_string(),
gen_dir.to_string_lossy().to_string()
);
// Search (empty DB should return empty list or empty result)
let res = ws.search(py, "test", 10).unwrap();
assert!(res.bind(py).is_instance_of::<pyo3::types::PyList>());
// Trace
let res = ws.trace(py, "nonexistent.rs", 1, 1).unwrap();
assert!(res.bind(py).is_none() || res.bind(py).is_instance_of::<pyo3::types::PyDict>());
// Change plan methods (validate, preview, apply)
// Use pythonize to create a valid ChangePlan from Rust
let plan = weaveback_agent_core::ChangePlan {
plan_id: "test-plan".to_string(),
goal: "test-goal".to_string(),
constraints: vec![],
edits: vec![weaveback_agent_core::PlannedEdit {
edit_id: "e1".to_string(),
rationale: "r1".to_string(),
target: weaveback_agent_core::ChangeTarget {
src_file: "test.adoc".to_string(),
src_line: 1,
src_line_end: 2,
},
new_src_lines: vec!["line1".to_string()],
anchor: weaveback_agent_core::OutputAnchor {
out_file: "test.rs".to_string(),
out_line: 1,
expected_output: "old".to_string(),
},
}],
};
let plan_any = pythonize(py, &plan).unwrap();
let _ = ws.validate_change_plan(py, plan_any.clone());
let _ = ws.preview_change_plan(py, plan_any.clone());
let _ = ws.apply_change_plan(py, plan_any.clone());
});
}
}
// @