diff --git a/Cargo.toml b/Cargo.toml index b99e0fe..5e0f29c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.1.14" +version = "0.1.15" description = "Knowing by reasoning, not vectors." edition = "2024" authors = ["zTgx "] @@ -97,7 +97,7 @@ rand = "0.8" bm25 = { version = "2.3.2", features = ["parallelism"] } # Python bindings -pyo3 = { version = "0.28", features = ["extension-module"] } +pyo3 = { version = "0.28", features = ["extension-module", "abi3-py311"] } pyo3-async-runtimes = { version = "0.28", features = ["tokio-runtime"] } # Dev dependencies diff --git a/HISTORY.md b/HISTORY.md index 875283f..4d52f82 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # HISTORY +## 0.1.15 (2026-04-29) + +- **Python bindings**: enabled PyO3 abi3 for cross-version compatibility +- Single wheel now supports Python 3.11, 3.12, and 3.13+ +- Simplified exception type using `create_exception!` macro + ## 0.1.14 (2026-04-29) - Homepage redesign with product showcase and improved UX diff --git a/crates/vectorless-py/src/document.rs b/crates/vectorless-py/src/document.rs index 5c7d53f..1145893 100644 --- a/crates/vectorless-py/src/document.rs +++ b/crates/vectorless-py/src/document.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use pyo3::prelude::*; +use pyo3::exceptions::PyRuntimeError; use pyo3_async_runtimes::tokio::future_into_py; use tokio::sync::Mutex; @@ -15,8 +16,6 @@ use vectorless_primitives::{ SectionCardInfo, SectionSummaryInfo, SimilarResult, TocEntry, TopicEntryInfo, WordCount, }; -use super::error::VectorlessError; - // ========================================================================= // PyDocumentInfo (existing — returned by compile) // ========================================================================= @@ -126,7 +125,7 @@ fn id_to_str(id: u64) -> String { } fn to_py_err(e: impl std::fmt::Display) -> PyErr { - PyErr::from(VectorlessError::new(e.to_string(), "navigation")) + PyRuntimeError::new_err(e.to_string()) } #[pymethods] @@ -382,10 +381,10 @@ impl PyDocument { let num = node_id .strip_prefix('n') .ok_or_else(|| { - VectorlessError::new("NodeId must start with 'n'".to_string(), "navigation") + PyRuntimeError::new_err("NodeId must start with 'n'") })? .parse::() - .map_err(|_| VectorlessError::new("Invalid NodeId".to_string(), "navigation"))?; + .map_err(|_| PyRuntimeError::new_err("Invalid NodeId"))?; let nav = nav.lock().await; Ok(nav .chains_for(num) @@ -403,10 +402,10 @@ impl PyDocument { let num = node_id .strip_prefix('n') .ok_or_else(|| { - VectorlessError::new("NodeId must start with 'n'".to_string(), "navigation") + PyRuntimeError::new_err("NodeId must start with 'n'") })? .parse::() - .map_err(|_| VectorlessError::new("Invalid NodeId".to_string(), "navigation"))?; + .map_err(|_| PyRuntimeError::new_err("Invalid NodeId"))?; let nav = nav.lock().await; Ok(nav .overlaps_for(num) @@ -424,10 +423,10 @@ impl PyDocument { let num = node_id .strip_prefix('n') .ok_or_else(|| { - VectorlessError::new("NodeId must start with 'n'".to_string(), "navigation") + PyRuntimeError::new_err("NodeId must start with 'n'") })? .parse::() - .map_err(|_| VectorlessError::new("Invalid NodeId".to_string(), "navigation"))?; + .map_err(|_| PyRuntimeError::new_err("Invalid NodeId"))?; let nav = nav.lock().await; Ok(nav.evidence_score_for(num).await.map(PyEvidenceScore::from)) }) diff --git a/crates/vectorless-py/src/engine.rs b/crates/vectorless-py/src/engine.rs index 49ef759..229016b 100644 --- a/crates/vectorless-py/src/engine.rs +++ b/crates/vectorless-py/src/engine.rs @@ -4,6 +4,7 @@ //! Engine Python wrapper — async compile/forget/list_documents. use pyo3::prelude::*; +use pyo3::exceptions::PyRuntimeError; use pyo3_async_runtimes::tokio::future_into_py; use std::sync::Arc; use tokio::runtime::Runtime; @@ -11,7 +12,6 @@ use tokio::runtime::Runtime; use ::vectorless_engine::{Engine, EngineBuilder, IngestInput, RawNodeInput}; use super::document::{PyDocument, PyDocumentInfo}; -use super::error::VectorlessError; use super::error::to_py_err; use super::graph::PyDocumentGraph; use super::metrics::PyMetricsReport; @@ -73,10 +73,7 @@ async fn run_load_document(engine: Arc, doc_id: String) -> PyResult Err(PyErr::from(VectorlessError::new( - format!("Document not found: {doc_id}"), - "navigation", - ))), + None => Err(PyRuntimeError::new_err(format!("Document not found: {doc_id}"))), } } @@ -144,10 +141,7 @@ impl PyEngine { config: Option>, ) -> PyResult { let rt = Runtime::new().map_err(|e| { - PyErr::from(VectorlessError::new( - format!("Failed to create tokio runtime: {}", e), - "config", - )) + PyRuntimeError::new_err(format!("Failed to create tokio runtime: {}", e)) })?; let rust_config = config.map(|c| c.inner.clone()); @@ -173,10 +167,7 @@ impl PyEngine { }); let engine = engine.map_err(|e| { - PyErr::from(VectorlessError::new( - format!("Failed to create engine: {}", e), - "config", - )) + PyRuntimeError::new_err(format!("Failed to create engine: {}", e)) })?; Ok(Self { diff --git a/crates/vectorless-py/src/error.rs b/crates/vectorless-py/src/error.rs index 9f59cc4..7714ec5 100644 --- a/crates/vectorless-py/src/error.rs +++ b/crates/vectorless-py/src/error.rs @@ -3,69 +3,12 @@ //! Python exception types and error conversion. -use pyo3::exceptions::PyException; +use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use ::vectorless_engine::Error as RustError; -/// Python exception for vectorless errors. -#[pyclass(extends = PyException, subclass, skip_from_py_object)] -pub struct VectorlessError { - message: String, - kind: String, -} - -#[pymethods] -impl VectorlessError { - #[new] - fn new_py(message: String, kind: String) -> Self { - Self { message, kind } - } - - #[getter] - fn message(&self) -> &str { - &self.message - } - - #[getter] - fn kind(&self) -> &str { - &self.kind - } - - fn __str__(&self) -> &str { - &self.message - } - - fn __repr__(&self) -> String { - format!("VectorlessError('{}', kind='{}')", self.message, self.kind) - } -} - -impl VectorlessError { - pub fn new(message: String, kind: &str) -> Self { - Self { - message, - kind: kind.to_string(), - } - } -} - -impl From for PyErr { - fn from(err: VectorlessError) -> PyErr { - PyErr::new::((err.message, err.kind)) - } -} - /// Convert vectorless errors to Python exceptions. pub fn to_py_err(e: RustError) -> PyErr { - let message = e.to_string(); - let kind = match &e { - RustError::DocumentNotFound(_) => "not_found", - RustError::Parse(_) => "parse", - RustError::Config(_) => "config", - RustError::Workspace(_) => "workspace", - RustError::Llm(_) => "llm", - _ => "unknown", - }; - VectorlessError::new(message, kind).into() + PyRuntimeError::new_err(e.to_string()) } diff --git a/crates/vectorless-py/src/lib.rs b/crates/vectorless-py/src/lib.rs index 09437f9..5ef7957 100644 --- a/crates/vectorless-py/src/lib.rs +++ b/crates/vectorless-py/src/lib.rs @@ -20,7 +20,6 @@ use document::{ PyTocEntry, PyTopicEntry, PyWordCount, }; use engine::PyEngine; -use error::VectorlessError; use graph::{PyDocumentGraph, PyDocumentGraphNode, PyEdgeEvidence, PyGraphEdge, PyWeightedKeyword}; use metrics::{PyLlmMetricsReport, PyMetricsReport, PyRetrievalMetricsReport}; @@ -39,7 +38,7 @@ fn _vectorless(m: &Bound<'_, PyModule>) -> PyResult<()> { .init(); }); - m.add_class::()?; + // VectorlessError is just PyRuntimeError, no need to register m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/pyproject.toml b/pyproject.toml index 7c69925..da14e5d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,7 +70,7 @@ Repository = "https://github.com/vectorlessflow/vectorless" python-source = "." module-name = "vectorless._vectorless" manifest-path = "crates/vectorless-py/Cargo.toml" -features = ["pyo3/extension-module"] +features = ["pyo3/extension-module", "pyo3/abi3-py311"] [tool.pytest.ini_options] asyncio_mode = "auto"