From bdc2dd9b87e6944bf38b4211ffdb29b14d1b7ad7 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Tue, 14 Jul 2026 21:47:29 -0700 Subject: [PATCH 01/36] Add type assertions to operator helper formula implementations These assertions guarantee that the EnsureInput wrappers in the core are functioning properly when transitioning from Rust evaluation to Python evaluation. --- tests/test_operators.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_operators.py b/tests/test_operators.py index 4950507..6fae82a 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -49,6 +49,7 @@ def __ge__(self, other: object, /) -> bool: class Const(Formula[T, T]): @typing_extensions.override def evaluate(self, trace: Trace[T]) -> Trace[T]: + assert isinstance(trace, Trace) return trace @@ -110,6 +111,7 @@ def evaluate(self, trace: Trace[tuple[L, R]]) -> Trace[L]: class Right(Formula[tuple[L, R], R]): @typing_extensions.override def evaluate(self, trace: Trace[tuple[L, R]]) -> Trace[R]: + assert isinstance(trace, Trace) return Trace({time: state[1] for time, state in trace}) From ff4412df40be3b31ff8c90d22d651fc95dced73b Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Tue, 14 Jul 2026 21:49:15 -0700 Subject: [PATCH 02/36] Create a decorator to define formula implementations This makes it slightly easier than having to define a class for quick operator or expression definitions. --- banquo/core.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/banquo/core.py b/banquo/core.py index 212d0c2..a431160 100644 --- a/banquo/core.py +++ b/banquo/core.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections.abc import Callable from typing import Protocol, TypeVar from typing_extensions import Self, override @@ -100,3 +101,18 @@ def evaluate(formula: Formula[S, M], trace: Trace[S]) -> M: return next(iter(result.states())) except StopIteration: raise ValueError("Provided formula evaluated to an empty trace.") + + +class UserFormula(Formula[S, M]): + def __init__(self, func: Callable[[Trace[S]], Trace[M]]): + self.func: Callable[[Trace[S]], Trace[M]] = func + + @override + def evaluate(self, trace: Trace[S]) -> Trace[M]: + return self.func(trace) + + +def formula(f: Callable[[Trace[S]], Trace[M]]) -> Formula[S, M]: + """Transform a function into a Formula implementation.""" + + return UserFormula(f) From 20dd0d94d91480a97e7736d2155df40434301d9a Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Tue, 14 Jul 2026 21:49:59 -0700 Subject: [PATCH 03/36] Export formula decorator from top-level banquo module --- banquo/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/banquo/__init__.py b/banquo/__init__.py index 4713e82..0e1de7b 100644 --- a/banquo/__init__.py +++ b/banquo/__init__.py @@ -1,6 +1,6 @@ from typing import Final -from .core import Formula, evaluate +from .core import Formula, evaluate, formula from .expressions import Predicate from .trace import Trace from ._banquo_impl import Top as _Top @@ -16,4 +16,5 @@ "Top", "Trace", "evaluate", + "formula", ] From 354a6ecfa5b2e3eda1b8df1f1b37307b803c23e7 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Tue, 14 Jul 2026 21:53:17 -0700 Subject: [PATCH 04/36] Create test case for user-defined operators using decorator --- tests/test_operators.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/test_operators.py b/tests/test_operators.py index 6fae82a..c4c7894 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -6,7 +6,7 @@ import pytest import typing_extensions -from banquo import Bottom, Trace, operators +from banquo import Bottom, Trace, operators, formula from banquo.core import Formula pytestmark = pytest.mark.unit @@ -322,3 +322,28 @@ def test_unsupported_metric(self, bad_trace: Trace[BadMetric]): with pytest.raises(operators.MetricAttributeError): _ = formula.evaluate(bad_trace) # pyright: ignore[reportUnknownVariableType] + + +@formula +def neg(input: Trace[float]) -> Trace[float]: + assert isinstance(input, Trace) # Ensure the input trace is correctly wrapped + return Trace.from_timed_states(input.times(), (-state for state in input.states())) + + +class TestCustomOperator(UnaryTest): + @pytest.fixture + def expected(self, input: Trace[float]) -> Trace[float]: + return Trace({time: -state for time, state in input}) + + def test_evaluate(self, input: Trace[float], expected: Trace[float]): + result = neg.evaluate(input) + + assert isinstance(result, Trace) + assert result == expected + + def test_nesting(self, input: Trace[float], expected: Trace[float]): + formula = operators.And(neg, Const()) + result = formula.evaluate(input) + + assert isinstance(result, Trace) + assert result == expected From 589836e85a84c2c69d694622a9fd23065a1500d6 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Tue, 14 Jul 2026 21:54:18 -0700 Subject: [PATCH 05/36] Wrap inner formula of Next operator --- banquo/operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/banquo/operators.py b/banquo/operators.py index 6fd0d3c..99046a5 100644 --- a/banquo/operators.py +++ b/banquo/operators.py @@ -133,7 +133,7 @@ class Next(Operator[S, M]): """ def __init__(self, subformula: Formula[S, M]): - super().__init__(_Next(subformula), "") + super().__init__(_Next(_inner_or_wrap(subformula)), "") S_ = typing.TypeVar("S_") From 24e0f39a20d18a8bba5ff32e4db4083146775527 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Tue, 14 Jul 2026 21:58:37 -0700 Subject: [PATCH 06/36] Update error conversion for forward operators Since Banquo now provides access to the ForwardEvaluationError, we need to handle it as a unique case when converting errors from forward operator bindings. --- src/lib.rs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index afb36a5..4c70344 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,7 +11,8 @@ mod _banquo_impl { use super::metric::PyMetric; use banquo::operators::{ - Always, And, BinaryOperatorError, Eventually, ForwardOperatorError, Implies, Next, Not, Or, + Always, And, BinaryOperatorError, Eventually, ForwardEvaluationError, ForwardOperatorError, + Implies, Next, Not, Or, }; use banquo::predicate::Predicate; use banquo::{Formula, Trace}; @@ -330,12 +331,14 @@ mod _banquo_impl { impl PyAlways { fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { self.0.evaluate(trace).map_err(|err| match err { - ForwardOperatorError::EmptyInterval => { - PyValueError::new_err("Bounds interval must not be empty.") - } - ForwardOperatorError::EmptySubtraceEvaluation(t) => { - PyRuntimeError::new_err(format!("Subtrace at time {} is empty.", t)) - } + ForwardOperatorError::EvaluationError(eval_err) => match eval_err { + ForwardEvaluationError::EmptyInterval => { + PyValueError::new_err("Bounds interval must not be empty.") + } + ForwardEvaluationError::EmptySubtraceEvaluation(t) => { + PyRuntimeError::new_err(format!("Subtrace at time {} is empty.", t)) + } + }, ForwardOperatorError::FormulaError(err) => err, }) } @@ -365,12 +368,14 @@ mod _banquo_impl { impl PyEventually { fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { self.0.evaluate(trace).map_err(|err| match err { - ForwardOperatorError::EmptyInterval => { - PyValueError::new_err("Bounds interval must not be empty.") - } - ForwardOperatorError::EmptySubtraceEvaluation(t) => { - PyRuntimeError::new_err(format!("Subtrace at time {} is empty.", t)) - } + ForwardOperatorError::EvaluationError(eval_err) => match eval_err { + ForwardEvaluationError::EmptyInterval => { + PyValueError::new_err("Bounds interval must not be empty.") + } + ForwardEvaluationError::EmptySubtraceEvaluation(t) => { + PyRuntimeError::new_err(format!("Subtrace at time {} is empty.", t)) + } + }, ForwardOperatorError::FormulaError(err) => err, }) } From b19120d189c23a57422a97486a5da4445fa435f9 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Wed, 15 Jul 2026 22:23:28 -0700 Subject: [PATCH 07/36] Create separate Rust modules for easier modification Splitting up the monolithic _banquo_impl module makes maintenance and refactoring simpler. Implement new methods on PyMetricTrace for inverting inner results to outer result and update PyFormula implementation to short circuit if the contained formula produces an error. --- src/lib.rs | 407 +---------------------------------------------- src/operators.rs | 323 +++++++++++++++++++++++++++++++++++++ src/traces.rs | 122 ++++++++++++++ 3 files changed, 452 insertions(+), 400 deletions(-) create mode 100644 src/operators.rs create mode 100644 src/traces.rs diff --git a/src/lib.rs b/src/lib.rs index 4c70344..3dc6959 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,410 +1,17 @@ mod metric; +mod operators; +mod traces; #[pyo3::pymodule] mod _banquo_impl { - use std::collections::HashMap; - - use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; - use pyo3::prelude::*; - use pyo3::types::PyDict; - use pyo3::{FromPyObject, IntoPyObject}; - - use super::metric::PyMetric; - use banquo::operators::{ - Always, And, BinaryOperatorError, Eventually, ForwardEvaluationError, ForwardOperatorError, - Implies, Next, Not, Or, - }; - use banquo::predicate::Predicate; - use banquo::{Formula, Trace}; - - #[pyclass(name = "Trace", subclass, generic)] - struct PyTrace(Trace>); - - impl<'py> FromPyObject<'py> for PyTrace { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - Self::new(obj) - } - } - - #[pymethods] - impl PyTrace { - #[new] - fn new(elements: &Bound<'_, PyAny>) -> PyResult { - // If we construct a pytrace from a pytrace, we can copy without converting to python objects - if let Ok(pytrace) = elements.cast::() { - let py = elements.py(); - let copied = pytrace - .borrow() - .0 - .iter() - .map_states(|obj| obj.clone_ref(py)) - .collect(); - return Ok(PyTrace(copied)); - } - - elements - .cast::()? - .iter() - .map(|(key, value)| key.extract::().map(|time| (time, value.unbind()))) - .collect::>>() - .map(|trace| Self(trace)) - } - - fn __getitem__(&self, py: Python<'_>, time: f64) -> PyResult> { - self.at_time(py, time).ok_or_else(|| { - PyKeyError::new_err(format!("Time {} is not present in trace", time)) - }) - } - - fn times(&self) -> Vec { - self.0.times().collect() - } - - fn states(&self, py: Python<'_>) -> Vec> { - self.0.states().map(|state| state.clone_ref(py)).collect() - } - - fn at_time(&self, py: Python<'_>, time: f64) -> Option> { - self.0.at_time(time).map(|state| state.clone_ref(py)) - } - } - - #[pyclass(name = "Predicate", subclass)] - struct PyPredicate(Predicate); - - struct PyMetricTrace(Trace); - - impl PyMetricTrace { - fn into_inner(self) -> Trace { - self.0 - } - } - - impl From for PyMetricTrace { - fn from(value: PyTrace) -> Self { - Self(value.0.into_iter().map_states(PyMetric::from).collect()) - } - } - - impl<'py> IntoPyObject<'py> for PyMetricTrace { - type Target = PyTrace; - type Output = Bound<'py, Self::Target>; - type Error = PyErr; - - fn into_pyobject(self, py: Python<'py>) -> Result { - let trace = self - .0 - .into_iter() - .map_states(|state| state.into()) - .collect::>>(); - - Bound::new(py, PyTrace(trace)) - } - } - - impl PyPredicate { - fn evaluate_inner( - &self, - py: Python<'_>, - trace: &Trace>, - ) -> PyResult> { - let converted = trace - .iter() - .map(|(time, state)| state.extract::>(py).map(|s| (time, s))) - .collect::>>() - .map_err(|_| { - PyValueError::new_err("Predicate only supports dict values as trace states.") - })?; - - let evaluated = self - .0 - .evaluate(&converted) - .map_err(|err| PyValueError::new_err(err.to_string()))?; - - evaluated - .into_iter() - .map(|(time, rho)| PyMetric::try_from_f64(rho, py).map(|m| (time, m))) - .collect() - } - } - - #[pymethods] - impl PyPredicate { - #[new] - pub fn new(coefficients: HashMap, constant: f64) -> Self { - let mut p = Predicate::from_iter(coefficients); - p += constant; - - Self(p) - } - - pub fn __eq__(&self, other: &Bound<'_, Self>) -> bool { - self.0 == other.borrow().0 - } - - pub fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { - self.evaluate_inner(trace.py(), &trace.borrow().0) - .map(PyMetricTrace) - } - } - - struct PyFormula(Py); - - impl<'py> FromPyObject<'py> for PyFormula { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - Ok(Self(obj.clone().unbind())) - } - } - - fn evaluate(obj: &Bound<'_, PyAny>, trace: &Trace>) -> PyResult> { - if let Ok(pred) = obj.cast::() { - return pred.borrow().evaluate_inner(obj.py(), trace); - } - - if let Ok(not) = obj.cast::() { - return not.borrow().evaluate_inner(trace); - } - - if let Ok(and) = obj.cast::() { - return and.borrow().evaluate_inner(trace); - } - - if let Ok(or) = obj.cast::() { - return or.borrow().evaluate_inner(trace); - } - - if let Ok(implies) = obj.cast::() { - return implies.borrow().evaluate_inner(trace); - } - - if let Ok(always) = obj.cast::() { - return always.borrow().evaluate_inner(trace); - } - - if let Ok(eventually) = obj.cast::() { - return eventually.borrow().evaluate_inner(trace); - } - - let py = obj.py(); - let new_trace = trace.iter().map_states(|obj| obj.clone_ref(py)).collect(); - - obj.call_method1("evaluate", (PyTrace(new_trace),)) - .and_then(|result| result.extract::()) - .map(|result| PyMetricTrace::from(result).into_inner()) - } - - impl Formula> for PyFormula { - type Metric = PyMetric; - type Error = PyErr; - - fn evaluate(&self, trace: &Trace>) -> Result, Self::Error> { - Python::attach(|py| evaluate(self.0.bind(py), trace)) - } - } - - #[pyclass(name = "Not")] - struct PyNot(Not); - - impl PyNot { - fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { - self.0.evaluate(trace) - } - } - - #[pymethods] - impl PyNot { - #[new] - fn new(subformula: PyFormula) -> Self { - Self(Not::new(subformula)) - } - - fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { - self.evaluate_inner(&trace.borrow().0).map(PyMetricTrace) - } - } - - #[pyclass(name = "And")] - struct PyAnd(And); - - impl PyAnd { - fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { - self.0.evaluate(trace).map_err(|err| match err { - BinaryOperatorError::LeftError(left) => left, - BinaryOperatorError::RightError(right) => right, - BinaryOperatorError::EvaluationError(err) => { - PyRuntimeError::new_err(err.to_string()) - } - }) - } - } - - #[pymethods] - impl PyAnd { - #[new] - fn new(lhs: PyFormula, rhs: PyFormula) -> Self { - Self(And::new(lhs, rhs)) - } - - fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { - self.evaluate_inner(&trace.borrow().0).map(PyMetricTrace) - } - } - - #[pyclass(name = "Or")] - struct PyOr(Or); - - impl PyOr { - fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { - self.0.evaluate(trace).map_err(|err| match err { - BinaryOperatorError::LeftError(left) => left, - BinaryOperatorError::RightError(right) => right, - BinaryOperatorError::EvaluationError(err) => { - PyRuntimeError::new_err(err.to_string()) - } - }) - } - } - - #[pymethods] - impl PyOr { - #[new] - fn new(lhs: PyFormula, rhs: PyFormula) -> Self { - Self(Or::new(lhs, rhs)) - } - - fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { - self.evaluate_inner(&trace.borrow().0).map(PyMetricTrace) - } - } - - #[pyclass(name = "Implies")] - struct PyImplies(Implies); - - impl PyImplies { - fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { - self.0.evaluate(trace).map_err(|err| match err { - BinaryOperatorError::LeftError(left) => left, - BinaryOperatorError::RightError(right) => right, - BinaryOperatorError::EvaluationError(err) => { - PyRuntimeError::new_err(err.to_string()) - } - }) - } - } - - #[pymethods] - impl PyImplies { - #[new] - fn new(lhs: PyFormula, rhs: PyFormula) -> Self { - Self(Implies::new(lhs, rhs)) - } - - fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { - self.evaluate_inner(&trace.borrow().0).map(PyMetricTrace) - } - } - - #[pyclass(name = "Next")] - struct PyNext(Next); - - impl PyNext { - fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { - self.0.evaluate(trace) - } - } - - #[pymethods] - impl PyNext { - #[new] - fn new(subformula: PyFormula) -> Self { - Self(Next::new(subformula)) - } - - fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { - self.evaluate_inner(&trace.borrow().0).map(PyMetricTrace) - } - } - - #[pyclass(name = "Always")] - struct PyAlways(Always); - - impl PyAlways { - fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { - self.0.evaluate(trace).map_err(|err| match err { - ForwardOperatorError::EvaluationError(eval_err) => match eval_err { - ForwardEvaluationError::EmptyInterval => { - PyValueError::new_err("Bounds interval must not be empty.") - } - ForwardEvaluationError::EmptySubtraceEvaluation(t) => { - PyRuntimeError::new_err(format!("Subtrace at time {} is empty.", t)) - } - }, - ForwardOperatorError::FormulaError(err) => err, - }) - } - } - - #[pymethods] - impl PyAlways { - #[new] - fn new(bounds: Option<(f64, f64)>, subformula: PyFormula) -> Self { - let inner = if let Some((lo, hi)) = bounds { - Always::bounded(lo..=hi, subformula) - } else { - Always::unbounded(subformula) - }; - - Self(inner) - } - - fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { - self.evaluate_inner(&trace.borrow().0).map(PyMetricTrace) - } - } - - #[pyclass(name = "Eventually")] - struct PyEventually(Eventually); - - impl PyEventually { - fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { - self.0.evaluate(trace).map_err(|err| match err { - ForwardOperatorError::EvaluationError(eval_err) => match eval_err { - ForwardEvaluationError::EmptyInterval => { - PyValueError::new_err("Bounds interval must not be empty.") - } - ForwardEvaluationError::EmptySubtraceEvaluation(t) => { - PyRuntimeError::new_err(format!("Subtrace at time {} is empty.", t)) - } - }, - ForwardOperatorError::FormulaError(err) => err, - }) - } - } - - #[pymethods] - impl PyEventually { - #[new] - fn new(bounds: Option<(f64, f64)>, subformula: PyFormula) -> Self { - let inner = if let Some((lo, hi)) = bounds { - Eventually::bounded(lo..=hi, subformula) - } else { - Eventually::unbounded(subformula) - }; - - Self(inner) - } - - fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { - self.evaluate_inner(&trace.borrow().0).map(PyMetricTrace) - } - } - #[pymodule_export] - use pyo3::panic::PanicException; + use crate::metric::{PyBottom, PyTop}; #[pymodule_export] - use crate::metric::PyTop; + use crate::traces::PyTrace; #[pymodule_export] - use crate::metric::PyBottom; + use crate::operators::{ + PyAlways, PyAnd, PyEventually, PyImplies, PyNext, PyNot, PyOr, PyPredicate, + }; } diff --git a/src/operators.rs b/src/operators.rs new file mode 100644 index 0000000..9b6eb5d --- /dev/null +++ b/src/operators.rs @@ -0,0 +1,323 @@ +use std::collections::HashMap; + +use banquo::operators::{Always, And, Eventually, Implies, Next, Not, Or}; +use banquo::operators::{BinaryOperatorError, ForwardEvaluationError, ForwardOperatorError}; +use banquo::{Formula, Predicate, Trace}; +use pyo3::exceptions::{PyRuntimeError, PyValueError}; +use pyo3::prelude::*; + +use crate::metric::PyMetric; +use crate::stl; +use crate::traces::{PyMetricTrace, PyTrace}; + +#[pyclass(name = "Predicate", subclass)] +pub struct PyPredicate(Predicate); + +impl PyPredicate { + fn evaluate_inner( + &self, + py: Python<'_>, + trace: &Trace>, + ) -> PyResult> { + let converted = trace + .iter() + .map(|(time, state)| state.extract::>(py).map(|s| (time, s))) + .collect::>>() + .map_err(|_| { + PyValueError::new_err("Predicate only supports dict values as trace states.") + })?; + + let evaluated = self + .0 + .evaluate(&converted) + .map_err(|err| PyValueError::new_err(err.to_string()))? + .into_iter() + .map(|(time, rho)| (time, PyMetric::from_f64(rho))) + .collect(); + + Ok(evaluated) + } +} + +#[pymethods] +impl PyPredicate { + #[new] + pub fn new(coefficients: HashMap, constant: f64) -> Self { + let mut p = Predicate::from_iter(coefficients); + p += constant; + + Self(p) + } + + pub fn __eq__(&self, other: &Bound<'_, Self>) -> bool { + self.0 == other.borrow().0 + } + + pub fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { + self.evaluate_inner(trace.py(), trace.borrow().as_ref()) + .map(PyMetricTrace::from) + } +} + +struct PyFormula(Py); + +impl<'py> FromPyObject<'py> for PyFormula { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + Ok(Self(obj.clone().unbind())) + } +} + +fn evaluate(obj: &Bound<'_, PyAny>, trace: &Trace>) -> PyResult> { + if let Ok(pred) = obj.cast::() { + return pred.borrow().evaluate_inner(obj.py(), trace); + } + + if let Ok(not) = obj.cast::() { + return not.borrow().evaluate_inner(trace); + } + + if let Ok(and) = obj.cast::() { + return and.borrow().evaluate_inner(trace); + } + + if let Ok(or) = obj.cast::() { + return or.borrow().evaluate_inner(trace); + } + + if let Ok(implies) = obj.cast::() { + return implies.borrow().evaluate_inner(trace); + } + + if let Ok(always) = obj.cast::() { + return always.borrow().evaluate_inner(trace); + } + + if let Ok(eventually) = obj.cast::() { + return eventually.borrow().evaluate_inner(trace); + } + + if let Ok(formula) = obj.cast::() { + return formula.borrow().evaluate_inner(obj.py(), trace); + } + + let py = obj.py(); + let new_trace: Trace> = trace.iter().map_states(|obj| obj.clone_ref(py)).collect(); + let pytrace = PyTrace::from(new_trace); + + obj.call_method1("evaluate", (pytrace,)) + .and_then(|result| result.extract::()) + .map(|result| PyMetricTrace::from(result).into_inner()) +} + +impl Formula> for PyFormula { + type Metric = PyMetric; + type Error = PyErr; + + fn evaluate(&self, trace: &Trace>) -> Result, Self::Error> { + // Apply the internal operator implementation + let evaluated = Python::attach(|py| evaluate(self.0.bind(py), trace))?; + + // Invert results to stop evaluation if a metric value contains an error + let inverted = PyMetricTrace::from(evaluated).invert()?; + + Ok(inverted.into_inner()) + } +} + +#[pyclass(name = "Not")] +pub struct PyNot(Not); + +impl PyNot { + fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { + self.0.evaluate(trace) + } +} + +#[pymethods] +impl PyNot { + #[new] + fn new(subformula: PyFormula) -> Self { + Self(Not::new(subformula)) + } + + fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { + self.evaluate_inner(trace.borrow().as_ref()) + .map(PyMetricTrace::from) + } +} + +#[pyclass(name = "And")] +pub struct PyAnd(And); + +impl PyAnd { + fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { + self.0.evaluate(trace).map_err(|err| match err { + BinaryOperatorError::LeftError(left) => left, + BinaryOperatorError::RightError(right) => right, + BinaryOperatorError::EvaluationError(err) => PyRuntimeError::new_err(err.to_string()), + }) + } +} + +#[pymethods] +impl PyAnd { + #[new] + fn new(lhs: PyFormula, rhs: PyFormula) -> Self { + Self(And::new(lhs, rhs)) + } + + fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { + self.evaluate_inner(trace.borrow().as_ref()) + .map(PyMetricTrace::from) + } +} + +#[pyclass(name = "Or")] +pub struct PyOr(Or); + +impl PyOr { + fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { + self.0.evaluate(trace).map_err(|err| match err { + BinaryOperatorError::LeftError(left) => left, + BinaryOperatorError::RightError(right) => right, + BinaryOperatorError::EvaluationError(err) => PyRuntimeError::new_err(err.to_string()), + }) + } +} + +#[pymethods] +impl PyOr { + #[new] + fn new(lhs: PyFormula, rhs: PyFormula) -> Self { + Self(Or::new(lhs, rhs)) + } + + fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { + self.evaluate_inner(trace.borrow().as_ref()) + .map(PyMetricTrace::from) + } +} + +#[pyclass(name = "Implies")] +pub struct PyImplies(Implies); + +impl PyImplies { + fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { + self.0.evaluate(trace).map_err(|err| match err { + BinaryOperatorError::LeftError(left) => left, + BinaryOperatorError::RightError(right) => right, + BinaryOperatorError::EvaluationError(err) => PyRuntimeError::new_err(err.to_string()), + }) + } +} + +#[pymethods] +impl PyImplies { + #[new] + fn new(lhs: PyFormula, rhs: PyFormula) -> Self { + Self(Implies::new(lhs, rhs)) + } + + fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { + self.evaluate_inner(trace.borrow().as_ref()) + .map(PyMetricTrace::from) + } +} + +#[pyclass(name = "Next")] +pub struct PyNext(Next); + +impl PyNext { + fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { + self.0.evaluate(trace) + } +} + +#[pymethods] +impl PyNext { + #[new] + fn new(subformula: PyFormula) -> Self { + Self(Next::new(subformula)) + } + + fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { + self.evaluate_inner(trace.borrow().as_ref()) + .map(PyMetricTrace::from) + } +} + +#[pyclass(name = "Always")] +pub struct PyAlways(Always); + +impl PyAlways { + fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { + self.0.evaluate(trace).map_err(|err| match err { + ForwardOperatorError::EvaluationError(eval_err) => match eval_err { + ForwardEvaluationError::EmptyInterval => { + PyValueError::new_err("Bounds interval must not be empty.") + } + ForwardEvaluationError::EmptySubtraceEvaluation(t) => { + PyRuntimeError::new_err(format!("Subtrace at time {} is empty.", t)) + } + }, + ForwardOperatorError::FormulaError(err) => err, + }) + } +} + +#[pymethods] +impl PyAlways { + #[new] + fn new(bounds: Option<(f64, f64)>, subformula: PyFormula) -> Self { + let inner = if let Some((lo, hi)) = bounds { + Always::bounded(lo..=hi, subformula) + } else { + Always::unbounded(subformula) + }; + + Self(inner) + } + + fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { + self.evaluate_inner(trace.borrow().as_ref()) + .map(PyMetricTrace::from) + } +} + +#[pyclass(name = "Eventually")] +pub struct PyEventually(Eventually); + +impl PyEventually { + fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { + self.0.evaluate(trace).map_err(|err| match err { + ForwardOperatorError::EvaluationError(eval_err) => match eval_err { + ForwardEvaluationError::EmptyInterval => { + PyValueError::new_err("Bounds interval must not be empty.") + } + ForwardEvaluationError::EmptySubtraceEvaluation(t) => { + PyRuntimeError::new_err(format!("Subtrace at time {} is empty.", t)) + } + }, + ForwardOperatorError::FormulaError(err) => err, + }) + } +} + +#[pymethods] +impl PyEventually { + #[new] + fn new(bounds: Option<(f64, f64)>, subformula: PyFormula) -> Self { + let inner = if let Some((lo, hi)) = bounds { + Eventually::bounded(lo..=hi, subformula) + } else { + Eventually::unbounded(subformula) + }; + + Self(inner) + } + + fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { + self.evaluate_inner(trace.borrow().as_ref()) + .map(PyMetricTrace::from) + } +} diff --git a/src/traces.rs b/src/traces.rs new file mode 100644 index 0000000..c7e0232 --- /dev/null +++ b/src/traces.rs @@ -0,0 +1,122 @@ +use banquo::Trace; +use pyo3::exceptions::PyKeyError; +use pyo3::prelude::*; +use pyo3::types::PyDict; + +use crate::metric::PyMetric; + +#[pyclass(name = "Trace", subclass, generic)] +pub struct PyTrace(Trace>); + +impl From>> for PyTrace { + fn from(value: Trace>) -> Self { + Self(value) + } +} + +impl<'py> FromPyObject<'py> for PyTrace { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + Self::new(obj) + } +} + +impl AsRef>> for PyTrace { + fn as_ref(&self) -> &Trace> { + &self.0 + } +} + +#[pymethods] +impl PyTrace { + #[new] + fn new(elements: &Bound<'_, PyAny>) -> PyResult { + // If we construct a pytrace from a pytrace, we can copy without converting to python objects + if let Ok(pytrace) = elements.cast::() { + let py = elements.py(); + let copied = pytrace + .borrow() + .0 + .iter() + .map_states(|obj| obj.clone_ref(py)) + .collect(); + return Ok(PyTrace(copied)); + } + + elements + .cast::()? + .iter() + .map(|(key, value)| key.extract::().map(|time| (time, value.unbind()))) + .collect::>>() + .map(|trace| Self(trace)) + } + + fn __getitem__(&self, py: Python<'_>, time: f64) -> PyResult> { + self.at_time(py, time) + .ok_or_else(|| PyKeyError::new_err(format!("Time {} is not present in trace", time))) + } + + fn times(&self) -> Vec { + self.0.times().collect() + } + + fn states(&self, py: Python<'_>) -> Vec> { + self.0.states().map(|state| state.clone_ref(py)).collect() + } + + fn at_time(&self, py: Python<'_>, time: f64) -> Option> { + self.0.at_time(time).map(|state| state.clone_ref(py)) + } +} + +pub struct PyMetricTrace(Trace); + +impl PyMetricTrace { + pub fn into_inner(self) -> Trace { + self.0 + } + + /// After evaluation, a PyMetricTrace is a trace of Result values that may or may not + /// contain an error. This function iterates over the states, collapsing the result values + /// into a single outer result value and resetting the inner values to successes. + pub fn invert(self) -> PyResult { + let inverted: Trace = self + .0 + .into_iter() + .map(|(time, metric)| { + metric + .into_inner() + .map(|value| (time, PyMetric::from(value))) + }) + .collect::>()?; + + Ok(Self(inverted)) + } +} + +impl From for PyMetricTrace { + fn from(value: PyTrace) -> Self { + Self(value.0.into_iter().map_states(PyMetric::from).collect()) + } +} + +impl From> for PyMetricTrace { + fn from(value: Trace) -> Self { + Self(value) + } +} + +impl<'py> IntoPyObject<'py> for PyMetricTrace { + type Target = PyTrace; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + let trace = self + .0 + .into_iter() + .map(|(time, state)| state.into_inner().map(|value| (time, value))) + .collect::>>>()?; + + Bound::new(py, PyTrace(trace)) + } +} From 2871a51798bc8867a867dac8cae2a201e666485b Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Wed, 15 Jul 2026 22:26:51 -0700 Subject: [PATCH 08/36] Update PyMetric to contain a result This allows us to implement Meet and Join without having to resort to panicking if an operator is not defined. Now the error is caught in the value and can be reported. This method is less efficient because we evaluate the whole trace even if the first element errors but we gain better error reporting. --- src/metric.rs | 102 +++++++++++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 43 deletions(-) diff --git a/src/metric.rs b/src/metric.rs index ccf34ef..7352ee0 100644 --- a/src/metric.rs +++ b/src/metric.rs @@ -1,8 +1,9 @@ use std::cmp::Ordering; use std::ops::Neg; +use pyo3::PyErr; use pyo3::basic::CompareOp; -use pyo3::types::{PyAny, PyAnyMethods, PyBool, PyNotImplemented}; +use pyo3::types::{PyAny, PyAnyMethods, PyBool, PyModule, PyNotImplemented}; use pyo3::{Bound, IntoPyObject, IntoPyObjectExt, Py, PyResult, Python, pyclass, pymethods}; use banquo::{Bottom, Join, Meet, Top}; @@ -65,38 +66,62 @@ impl PyBottom { } } -#[derive(IntoPyObject)] -pub struct PyMetric(Py); +type PyMetricInner = PyResult>; + +pub struct PyMetric(PyMetricInner); impl PyMetric { - pub fn try_from_f64(value: f64, py: Python<'_>) -> PyResult { - value.into_py_any(py).map(Self) + pub fn from_f64(value: f64) -> Self { + Self(Python::attach(|py| value.into_py_any(py))) + } + + pub fn into_inner(self) -> PyMetricInner { + self.0 } } impl From> for PyMetric { fn from(value: Py) -> Self { - Self(value) + Self(Ok(value)) } } -impl From> for PyMetric { - fn from(value: Bound<'_, PyAny>) -> Self { - Self(value.unbind()) +impl<'py> IntoPyObject<'py> for PyMetric { + type Target = PyAny; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.0.map(|value| value.into_bound(py)) } } -impl Into> for PyMetric { - fn into(self) -> Py { - self.0 - } +fn transpose_results(lhs: Result, rhs: Result) -> Result<(A, B), E> { + Ok((lhs?, rhs?)) } -// Required for PartialOrd implementation impl PartialEq for PyMetric { fn eq(&self, other: &Self) -> bool { - // If equality is not defined, then the two objects are never equal - Python::attach(|py| self.0.bind(py).eq(&other.0).unwrap()) + // Two metrics are equal if their inner values are equal and not errors + Python::attach(|py| -> bool { + transpose_results(self.0.as_ref(), other.0.as_ref()) + .map_err(|e| e.clone_ref(py)) + .and_then(|(lhs, rhs)| lhs.bind(py).eq(rhs)) + .unwrap_or(false) + }) + } +} + +impl PartialOrd for PyMetric { + fn partial_cmp(&self, other: &Self) -> Option { + // Create an ordering for the two metrics only if both are not errors + // If the compare method creates an error, transform it to a None value + Python::attach(|py| -> Option { + transpose_results(self.0.as_ref(), other.0.as_ref()) + .map_err(|e| e.clone_ref(py)) + .and_then(|(lhs, rhs)| lhs.bind(py).compare(rhs)) + .ok() + }) } } @@ -104,55 +129,46 @@ impl Neg for PyMetric { type Output = Self; fn neg(self) -> Self::Output { - Python::attach(|py| self.0.bind(py).neg().map(Self::from).unwrap()) - } -} + let negated = Python::attach(|py| { + self.0 + .and_then(|value| value.bind(py).neg()) + .map(|value| value.unbind()) + }); -// Required for Meet implementation -impl PartialOrd for PyMetric { - fn partial_cmp(&self, other: &Self) -> Option { - Python::attach(|py| Some(self.0.bind(py).compare(&other.0).unwrap())) + Self(negated) } } -fn pymeet<'py>(lhs: &Bound<'py, PyAny>, rhs: &Bound<'py, PyAny>) -> Bound<'py, PyAny> { - if lhs.le(rhs).unwrap() { - lhs.clone() - } else { - rhs.clone() - } +fn builtin(py: Python<'_>, lhs: &PyMetricInner, rhs: &PyMetricInner, name: &str) -> PyMetricInner { + let (lval, rval) = + transpose_results(lhs.as_ref(), rhs.as_ref()).map_err(|e| e.clone_ref(py))?; + + PyModule::import(py, "builtins")? + .getattr(name)? + .call((lval, rval), None) + .map(|value| value.unbind()) } -// Required for operator implementations (And, Iff, etc.) impl Meet for PyMetric { fn min(&self, other: &Self) -> Self { - Python::attach(|py| pymeet(self.0.bind(py), other.0.bind(py)).into()) - } -} - -fn pyjoin<'py>(lhs: &Bound<'py, PyAny>, rhs: &Bound<'py, PyAny>) -> Bound<'py, PyAny> { - if lhs.ge(rhs).unwrap() { - lhs.clone() - } else { - rhs.clone() + Self(Python::attach(|py| builtin(py, &self.0, &other.0, "min"))) } } -// Required for operator implementations (Or, Implies, etc.) impl Join for PyMetric { fn max(&self, other: &Self) -> Self { - Python::attach(|py| pyjoin(self.0.bind(py), other.0.bind(py)).into()) + Self(Python::attach(|py| builtin(py, &self.0, &other.0, "max"))) } } impl Top for PyMetric { fn top() -> Self { - Python::attach(|py| PyMetric(PyTop.into_py_any(py).unwrap())) + Python::attach(|py| Self(PyTop.into_py_any(py))) } } impl Bottom for PyMetric { fn bottom() -> Self { - Python::attach(|py| PyMetric(PyBottom.into_py_any(py).unwrap())) + Python::attach(|py| Self(PyBottom.into_py_any(py))) } } From 2d9fbee94a6a2660aeaab9dc7421237fca5d4a3d Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Wed, 15 Jul 2026 22:28:57 -0700 Subject: [PATCH 09/36] Require < and > operators instead of <= and >= for operator implementations We delegate to the python definitions of `min` and `max` in the PyMetric Meet and Join implementations, which use the strict inequality operators. Thus we change our type bounds appropriately. --- banquo/core.py | 10 +++++----- banquo/operators.py | 20 +++++++++----------- tests/test_operators.py | 8 ++++---- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/banquo/core.py b/banquo/core.py index a431160..678ad6a 100644 --- a/banquo/core.py +++ b/banquo/core.py @@ -44,15 +44,15 @@ class SupportsNeg(Protocol): def __neg__(self) -> Self: ... -class SupportsLE(Protocol): - def __le__(self, value: Self, /) -> bool: ... +class SupportsLT(Protocol): + def __lt__(self, value: Self, /) -> bool: ... -class SupportsGE(Protocol): - def __ge__(self, value: Self, /) -> bool: ... +class SupportsGT(Protocol): + def __gt__(self, value: Self, /) -> bool: ... -class SupportsNegGE(SupportsNeg, SupportsGE, Protocol): ... +class SupportsNegGE(SupportsNeg, SupportsGT, Protocol): ... class EnsureInput(Formula[S, M]): diff --git a/banquo/operators.py b/banquo/operators.py index 99046a5..933e9f5 100644 --- a/banquo/operators.py +++ b/banquo/operators.py @@ -11,13 +11,12 @@ from ._banquo_impl import Next as _Next from ._banquo_impl import Not as _Not from ._banquo_impl import Or as _Or -from ._banquo_impl import PanicException from .core import ( Formula, EnsureInput, SupportsNeg, - SupportsLE, - SupportsGE, + SupportsLT, + SupportsGT, EnsureOutput, SupportsNegGE, ) @@ -28,8 +27,8 @@ S = typing.TypeVar("S") M = typing.TypeVar("M", covariant=True) M_neg = typing.TypeVar("M_neg", bound=SupportsNeg, covariant=True) -M_le = typing.TypeVar("M_le", bound=SupportsLE, covariant=True) -M_ge = typing.TypeVar("M_ge", bound=SupportsGE, covariant=True) +M_le = typing.TypeVar("M_le", bound=SupportsLT, covariant=True) +M_ge = typing.TypeVar("M_ge", bound=SupportsGT, covariant=True) M_neg_ge = typing.TypeVar("M_neg_ge", bound=SupportsNegGE, covariant=True) @@ -39,8 +38,7 @@ def and_(self: Formula[S, M_le], other: Formula[S, M_le]) -> And[S, M_le]: class MetricAttributeError(AttributeError): - def __init__(self, missing_method: str): - super().__init__(f"Metric must implement {missing_method} method") + pass class Operator(EnsureOutput[S, M], OperatorMixin): @@ -52,8 +50,8 @@ def __init__(self, subformula: Formula[S, M], required_method: str): def evaluate(self, trace: Trace[S]) -> Trace[M]: try: return super().evaluate(trace) - except PanicException as e: - raise MetricAttributeError(self.required_method) from e + except TypeError as e: + raise MetricAttributeError() from e def _inner_or_wrap(formula: Formula[S, M]) -> Formula[S, M]: @@ -137,7 +135,7 @@ def __init__(self, subformula: Formula[S, M]): S_ = typing.TypeVar("S_") -M_le_ = typing.TypeVar("M_le_", bound=SupportsLE, covariant=True) +M_le_ = typing.TypeVar("M_le_", bound=SupportsLT, covariant=True) class Always(Operator[S, M_le]): @@ -177,7 +175,7 @@ def with_bounds(bounds: Bounds, subformula: Formula[S_, M_le_]) -> Always[S_, M_ return Always(_Always(bounds, _inner_or_wrap(subformula))) -M_ge_ = typing.TypeVar("M_ge_", bound=SupportsGE, covariant=True) +M_ge_ = typing.TypeVar("M_ge_", bound=SupportsGT, covariant=True) class Eventually(Operator[S, M_ge]): diff --git a/tests/test_operators.py b/tests/test_operators.py index c4c7894..106363f 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -33,17 +33,17 @@ def __eq__(self, other: object) -> bool: def __neg__(self) -> GoodMetric: return GoodMetric(-self.value) - def __le__(self, other: object, /) -> bool: + def __lt__(self, other: object, /) -> bool: if not isinstance(other, GoodMetric): return NotImplemented - return self.value <= other.value + return self.value < other.value - def __ge__(self, other: object, /) -> bool: + def __gt__(self, other: object, /) -> bool: if not isinstance(other, GoodMetric): return NotImplemented - return self.value >= other.value + return self.value > other.value class Const(Formula[T, T]): From 2e9697d287ec808f5fa1c00583df7ad6a2eeffc8 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Wed, 15 Jul 2026 22:37:58 -0700 Subject: [PATCH 10/36] Update pyo3 dependency version --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 16265c0..c8a2cc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,4 +10,4 @@ crate-type = ["cdylib"] [dependencies] banquo = { version = "0.1.0" } -pyo3 = { version = "0.26.0", features = ["extension-module"] } +pyo3 = { version = "0.29.0", features = ["extension-module"] } From 6d3898238929d369df0d3c079fa67139d5f2244d Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Wed, 15 Jul 2026 22:39:03 -0700 Subject: [PATCH 11/36] Update FromPyObject implementations to new interface from pyo3 0.29 --- src/operators.rs | 7 +------ src/traces.rs | 8 +++++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/operators.rs b/src/operators.rs index 9b6eb5d..29892ee 100644 --- a/src/operators.rs +++ b/src/operators.rs @@ -59,14 +59,9 @@ impl PyPredicate { } } +#[derive(FromPyObject)] struct PyFormula(Py); -impl<'py> FromPyObject<'py> for PyFormula { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - Ok(Self(obj.clone().unbind())) - } -} - fn evaluate(obj: &Bound<'_, PyAny>, trace: &Trace>) -> PyResult> { if let Ok(pred) = obj.cast::() { return pred.borrow().evaluate_inner(obj.py(), trace); diff --git a/src/traces.rs b/src/traces.rs index c7e0232..dac4837 100644 --- a/src/traces.rs +++ b/src/traces.rs @@ -14,9 +14,11 @@ impl From>> for PyTrace { } } -impl<'py> FromPyObject<'py> for PyTrace { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - Self::new(obj) +impl<'py> FromPyObject<'_, 'py> for PyTrace { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { + Self::new(obj.as_any()) } } From 9e0a113294e5bd5bc7caa396e8d28eea48513a13 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Thu, 16 Jul 2026 09:59:51 -0700 Subject: [PATCH 12/36] Replace Pyright config with Pyrefly config --- pyproject.toml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2946cc8..332d520 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,10 +48,6 @@ preview = true line-length = 100 target-version = "py39" -[tool.pyright] -include = ["banquo"] -pythonVersion = "3.9" - [tool.pytest.ini_options] testpaths = [ "tests" @@ -60,3 +56,11 @@ markers = [ "conformance: Tests to verify correct behavior of formulas", "unit: Tests to verify correct behavior of operators", ] + +[tool.pyrefly] +preset = "default" +python-version = "3.9" +project-includes = [ + "banquo", + "tests", +] From d8dbb63bd7f03c11a8be25fc00fd5988b15ae74b Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Thu, 16 Jul 2026 15:48:08 -0700 Subject: [PATCH 13/36] Remove rules for artifact directories that now ship with ignore rules built in --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index c8f0442..a941bea 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ -.pytest_cache/ *.py[cod] # C extensions @@ -10,7 +9,6 @@ __pycache__/ # Distribution / packaging .Python -.venv/ env/ bin/ build/ From bf6c2e188c324acbc01a4abd5b2464299b331878 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Thu, 16 Jul 2026 10:05:04 -0700 Subject: [PATCH 14/36] Update Banquo version to pull in parser --- Cargo.lock | 306 +++++++++++++++++++++++++++++++++++++++++++++++------ Cargo.toml | 2 +- 2 files changed, 273 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a83e491..2fde0e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,30 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "ar_archive_writer" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348" +dependencies = [ + "object", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -10,21 +34,37 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "banquo" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b11e7b8aa7cdb48ea55930c89ee67799e3bb20a857a0986eeb517d6993f2a94" +version = "0.1.1" dependencies = [ "banquo-core", + "banquo-parser", ] [[package]] name = "banquo-core" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f185816cb843688a7fdf342ae6b851fef34d73e99d7c054fe647911f955073c6" +version = "0.1.2" dependencies = [ "ordered-float", - "thiserror", + "thiserror 1.0.69", +] + +[[package]] +name = "banquo-hybrid_distance" +version = "0.1.0" +dependencies = [ + "banquo-core", + "petgraph", + "thiserror 1.0.69", +] + +[[package]] +name = "banquo-parser" +version = "0.2.0" +dependencies = [ + "banquo-core", + "banquo-hybrid_distance", + "chumsky", + "thiserror 2.0.18", ] [[package]] @@ -35,6 +75,77 @@ dependencies = [ "pyo3", ] +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chumsky" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d2bfadce76f963d776feff99db6dc33783829539258314776383b33e2a00f8" +dependencies = [ + "hashbrown 0.15.5", + "regex-automata", + "serde", + "stacker", + "unicode-ident", + "unicode-segmentation", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + [[package]] name = "heck" version = "0.5.0" @@ -42,10 +153,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] -name = "indoc" -version = "2.0.6" +name = "indexmap" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] [[package]] name = "libc" @@ -54,13 +169,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] -name = "memoffset" -version = "0.9.1" +name = "memchr" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "num-traits" @@ -71,6 +183,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -86,6 +207,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -101,37 +232,44 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea" +dependencies = [ + "ar_archive_writer", + "cc", +] + [[package]] name = "pyo3" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba0117f4212101ee6544044dae45abe1083d30ce7b29c4b5cbdfa2354e07383" +checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" dependencies = [ - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", ] [[package]] name = "pyo3-build-config" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc6ddaf24947d12a9aa31ac65431fb1b851b8f4365426e182901eabfb87df5f" +checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" dependencies = [ "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025474d3928738efb38ac36d4744a74a400c901c7596199e20e45d98eb194105" +checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" dependencies = [ "libc", "pyo3-build-config", @@ -139,9 +277,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e64eb489f22fe1c95911b77c44cc41e7c19f3082fc81cce90f657cdc42ffded" +checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -151,13 +289,12 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.26.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "100246c0ecf400b475341b8455a9213344569af29a3c841d29270e53102e0fcf" +checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" dependencies = [ "heck", "proc-macro2", - "pyo3-build-config", "quote", "syn", ] @@ -171,6 +308,72 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "stacker" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys", +] + [[package]] name = "syn" version = "2.0.106" @@ -194,7 +397,16 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", ] [[package]] @@ -208,6 +420,17 @@ dependencies = [ "syn", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.19" @@ -215,7 +438,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] -name = "unindent" -version = "0.2.4" +name = "unicode-segmentation" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml index c8a2cc4..31cc878 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,5 +9,5 @@ name = "_banquo_impl" crate-type = ["cdylib"] [dependencies] -banquo = { version = "0.1.0" } +banquo = { version = "0.1.1", path = "../banquo/banquo", features = ["parser"] } pyo3 = { version = "0.29.0", features = ["extension-module"] } From 5a567be07d95b065162cff3da504fd71f7e7fbc3 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Wed, 15 Jul 2026 22:23:28 -0700 Subject: [PATCH 15/36] Create Rust python bindings for STL formula and parsing --- src/lib.rs | 10 ++++++++++ src/stl.rs | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/stl.rs diff --git a/src/lib.rs b/src/lib.rs index 3dc6959..d0d1f09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ mod metric; mod operators; +mod stl; mod traces; #[pyo3::pymodule] @@ -14,4 +15,13 @@ mod _banquo_impl { use crate::operators::{ PyAlways, PyAnd, PyEventually, PyImplies, PyNext, PyNot, PyOr, PyPredicate, }; + + #[pyo3::pymodule] + mod stl { + #[pymodule_export] + use crate::stl::PyFormula; + + #[pymodule_export] + use crate::stl::parse; + } } diff --git a/src/stl.rs b/src/stl.rs new file mode 100644 index 0000000..a504e5a --- /dev/null +++ b/src/stl.rs @@ -0,0 +1,54 @@ +use std::collections::HashMap; + +use banquo::Formula; +use banquo::trace::Trace; +use pyo3::PyResult; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +use crate::metric::PyMetric; + +#[pyclass(name = "Formula")] +pub struct PyFormula(banquo::stl::Formula); + +impl PyFormula { + pub fn evaluate_inner( + &self, + py: Python<'_>, + trace: &Trace>, + ) -> PyResult> { + let converted = trace + .iter() + .map(|(time, state)| state.extract::>(py).map(|s| (time, s))) + .collect::>>() + .map_err(|_| { + PyValueError::new_err("Predicate only supports dict values as trace states.") + })?; + + let evaluated = self + .0 + .evaluate(&converted) + .map_err(|err| PyValueError::new_err(err.to_string()))? + .into_iter() + .map(|(time, rho)| (time, PyMetric::from_f64(rho))) + .collect(); + + Ok(evaluated) + } +} + +/* +#[pymethods] +impl PyFormula { + fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { + self.evaluate_inner(&trace.borrow().0).map(PyMetricTrace) + } +} +*/ + +#[pyfunction] +pub fn parse(phi: &str) -> PyResult { + banquo::stl::parse(phi) + .map(PyFormula) + .map_err(|_| PyValueError::new_err("error parsing formula")) +} From e9a1559a5ac0dabc7993054300792b60a47bae69 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Thu, 16 Jul 2026 10:05:22 -0700 Subject: [PATCH 16/36] Update type hints for _banquo_impl bindings Define a custom module type to represent the STL submodule in the _banquo_impl library. --- banquo/_banquo_impl.pyi | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/banquo/_banquo_impl.pyi b/banquo/_banquo_impl.pyi index 1507f9b..8198613 100644 --- a/banquo/_banquo_impl.pyi +++ b/banquo/_banquo_impl.pyi @@ -1,5 +1,6 @@ -from collections.abc import Iterable +from collections.abc import Callable, Iterable from typing import Any, Generic, TypeVar +from types import ModuleType from typing_extensions import Self, override @@ -74,3 +75,11 @@ class Eventually(Formula[S, M_ge]): def __new__(cls, bounds: Bounds | None, subformula: Formula[S, M_ge]) -> Self: ... @override def evaluate(self, trace: Trace[S]) -> Trace[M_ge]: ... + +class _StlModule(ModuleType): + class Formula(Formula[dict[str, float], float]): + ... + + parse: Callable[[str], _StlModule.Formula] + +stl: _StlModule From 41a75b4cd904038fdd226adfb6902e4d98d50783 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Thu, 16 Jul 2026 10:05:37 -0700 Subject: [PATCH 17/36] Create initial STL python wrapper module. --- banquo/stl.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 banquo/stl.py diff --git a/banquo/stl.py b/banquo/stl.py new file mode 100644 index 0000000..6dbf714 --- /dev/null +++ b/banquo/stl.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from .core import EnsureOutput +from . import _banquo_impl as _impl + + +class Formula(EnsureOutput[dict[str, float], float]): + def __init__(self, formula: _impl.stl.Formula): + super().__init__(formula) + + +def parse(phi: str) -> Formula: + return Formula(_impl.stl.parse(phi)) From ba2e088da46bdd95777c6661dd54447a17294c4b Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Thu, 16 Jul 2026 20:59:41 -0700 Subject: [PATCH 18/36] Improve error reporting from STL parsing wrapper function --- src/stl.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/stl.rs b/src/stl.rs index a504e5a..6971cb4 100644 --- a/src/stl.rs +++ b/src/stl.rs @@ -7,6 +7,7 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use crate::metric::PyMetric; +use crate::traces::{PyMetricTrace, PyTrace}; #[pyclass(name = "Formula")] pub struct PyFormula(banquo::stl::Formula); @@ -37,18 +38,17 @@ impl PyFormula { } } -/* #[pymethods] impl PyFormula { fn evaluate(&self, trace: &Bound<'_, PyTrace>) -> PyResult { - self.evaluate_inner(&trace.borrow().0).map(PyMetricTrace) + self.evaluate_inner(trace.py(), trace.borrow().as_ref()) + .map(PyMetricTrace::from) } } -*/ #[pyfunction] pub fn parse(phi: &str) -> PyResult { banquo::stl::parse(phi) .map(PyFormula) - .map_err(|_| PyValueError::new_err("error parsing formula")) + .map_err(|e| PyValueError::new_err(format!("Error parsing formula: {:?}", e))) } From 458b336f4e0ba6606dda1a755a06f57c58f52f1e Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Thu, 16 Jul 2026 21:00:02 -0700 Subject: [PATCH 19/36] Delegate Trace __repr__ implementation to __str__ --- banquo/trace.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/banquo/trace.py b/banquo/trace.py index 6c3e842..7547c71 100644 --- a/banquo/trace.py +++ b/banquo/trace.py @@ -36,6 +36,10 @@ def __new__(cls, elements: Mapping[float, T] | _Trace[T]): def __str__(self) -> str: return str({time: state for time, state in self}) + @override + def __repr__(self) -> str: + return str(self) + @override def __iter__(self) -> Iterator[tuple[float, T]]: return zip(super().times(), super().states()) From e9429a11fcd0e1cbc9250e7ec80f3edf3f9fd0d1 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Thu, 16 Jul 2026 21:00:31 -0700 Subject: [PATCH 20/36] Simplify STL wrapper module import --- banquo/stl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/banquo/stl.py b/banquo/stl.py index 6dbf714..7fcd8f8 100644 --- a/banquo/stl.py +++ b/banquo/stl.py @@ -1,13 +1,13 @@ from __future__ import annotations from .core import EnsureOutput -from . import _banquo_impl as _impl +from ._banquo_impl import stl as _stl class Formula(EnsureOutput[dict[str, float], float]): - def __init__(self, formula: _impl.stl.Formula): + def __init__(self, formula: _stl.Formula): super().__init__(formula) def parse(phi: str) -> Formula: - return Formula(_impl.stl.parse(phi)) + return Formula(_stl.parse(phi)) From aca8775e36f05d0b51f4f67a0d5b7ef24287a4d8 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Thu, 16 Jul 2026 21:00:43 -0700 Subject: [PATCH 21/36] Create unit tests for STL parsing functionality --- tests/test_stl.py | 127 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 tests/test_stl.py diff --git a/tests/test_stl.py b/tests/test_stl.py new file mode 100644 index 0000000..958cee1 --- /dev/null +++ b/tests/test_stl.py @@ -0,0 +1,127 @@ +import pytest + +from banquo import Predicate, stl +from banquo import Trace as _Trace +from banquo import operators as ops + +def test_parse() -> None: + formula = stl.parse("always x <= 10.0") + assert isinstance(formula, stl.Formula) + + formula = stl.parse("not (eventually{0.0,3.5} (not (-1.0 * x <= 2.0 and 1.0 * x <= 2.0)))") + assert isinstance(formula, stl.Formula) + + with pytest.raises(ValueError): + _ = stl.parse("this is not a valid formula") + + +Trace = _Trace[dict[str, float]] + + +@pytest.fixture +def trace() -> Trace: + entries = [ + (0.0000, 0.0000), + (0.3947, 0.5881), + (0.7587, 1.1068), + (1.0660, 1.4967), + (1.2998, 1.7169), + (1.4546, 1.7508), + (1.5377, 1.6075), + (1.5675, 1.3204), + (1.5708, 0.9412), + (1.5787, 0.5313), + (1.6216, 0.1525), + (1.7242, -0.1431), + (1.9019, -0.3207), + (2.1583, -0.368), + (2.4844, -0.2963), + (2.8603, -0.1383), + (3.2583, 0.0582), + (3.6471, 0.2386), + (3.9968, 0.3511), + (4.2840, 0.3561), + (4.4947, 0.2326), + (4.6273, -0.017), + (4.6925, -0.3667), + (4.7114, -0.7708), + (4.7128, -1.1705), + (4.7280, -1.5029), + (4.7861, -1.7113), + (4.9095, -1.7537), + (5.1104, -1.6104), + (5.3886, -1.2874), + (5.7317, -0.816), + ] + + return Trace({time: {"x": value} for time, value in entries}) + + +@pytest.fixture +def p1() -> Predicate: + return Predicate({"x": -1.0}, 2.0) + + +@pytest.fixture +def p2() -> Predicate: + return Predicate({"x": 1.0}, 2.0) + + +def test_predicate(p1: Predicate, trace: Trace): + f = stl.parse("-1.0 * x <= 2.0") + assert p1.evaluate(trace) == f.evaluate(trace) + + +def test_negation(trace: Trace, p1: Predicate): + formula = ops.Not(p1) + parsed = stl.parse("not -1.0 * x <= 2.0") + + assert formula.evaluate(trace) == parsed.evaluate(trace) + + +def test_conjunction(trace: Trace, p1: Predicate, p2: Predicate): + formula = ops.And(p1, p2) + parsed = stl.parse("-1.0 * x <= 2.0 and 1.0 * x <= 2.0") + + assert formula.evaluate(trace) == parsed.evaluate(trace) + + +def test_disjunction(trace: Trace, p1: Predicate, p2: Predicate): + formula = ops.Or(p1, p2) + parsed = stl.parse("-1.0 * x <= 2.0 or 1.0 * x <= 2.0") + + assert formula.evaluate(trace) == parsed.evaluate(trace) + + +def test_implication(trace: Trace, p1: Predicate, p2: Predicate): + formula = ops.Implies(p1, p2) + parsed = stl.parse("-1.0 * x <= 2.0 implies 1.0 * x <= 2.0") + + assert formula.evaluate(trace) == parsed.evaluate(trace) + + +def test_eventually(trace: Trace, p1: Predicate): + formula = ops.Eventually(p1) + parsed = stl.parse("eventually -1.0 * x <= 2.0") + + assert formula.evaluate(trace) == parsed.evaluate(trace) + + formula = ops.Eventually.with_bounds((0.0, 3.5), p1) + parsed = stl.parse("eventually{0.0,3.5} -1.0 * x <= 2.0") + + +def test_always(trace: Trace, p1: Predicate): + formula = ops.Always(p1) + parsed = stl.parse("always -1.0 * x <= 2.0") + + assert formula.evaluate(trace) == parsed.evaluate(trace) + + formula = ops.Always.with_bounds((0.0, 3.5), p1) + parsed = stl.parse("always{0.0,3.5} -1.0 * x <= 2.0") + + +def test_composition(trace: Trace, p1: Predicate, p2: Predicate): + formula = ops.Not(ops.Eventually.with_bounds((0.0, 3.5), ops.Not(ops.And(p2, p1)))) + parsed = stl.parse("not (eventually{0.0,3.5} (not (1.0 * x <= 2.0 and -1.0 * x <= 2.0)))") + + assert formula.evaluate(trace) == parsed.evaluate(trace) From a3478ee445c357741dfac457bf1731548edbfab6 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 10:58:42 -0700 Subject: [PATCH 22/36] Update banquo dependency to version 0.2.0 --- Cargo.lock | 90 +++++++++++++++++++++++------------------------------- Cargo.toml | 2 +- 2 files changed, 40 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2fde0e0..2b3fca0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,13 +28,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "banquo" -version = "0.1.1" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc93c7ffd87d66bc1c5518b2f6e743600bca909ba62953ba8403f612241b9858" dependencies = [ "banquo-core", "banquo-parser", @@ -42,29 +44,35 @@ dependencies = [ [[package]] name = "banquo-core" -version = "0.1.2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e92dc4ffd039baafab4f4ad500c9f4a7860d6111389fc0f2496662e43ab6e6b" dependencies = [ "ordered-float", - "thiserror 1.0.69", + "thiserror", ] [[package]] name = "banquo-hybrid_distance" -version = "0.1.0" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c894832211466e2ef67a5325c3e57393473b566f7f4c525af6131921e7dc178d" dependencies = [ "banquo-core", "petgraph", - "thiserror 1.0.69", + "thiserror", ] [[package]] name = "banquo-parser" version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb93c15e8d0b4f220392c6a74b5070740a9266fefbaebce053771b8c24c89686" dependencies = [ "banquo-core", "banquo-hybrid_distance", "chumsky", - "thiserror 2.0.18", + "thiserror", ] [[package]] @@ -77,9 +85,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.65" +version = "1.2.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" dependencies = [ "find-msvc-tools", "shlex", @@ -164,15 +172,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.176" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "num-traits" @@ -194,9 +202,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "ordered-float" @@ -219,15 +227,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -301,18 +309,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] [[package]] name = "regex-automata" -version = "0.4.14" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -376,9 +384,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -387,18 +395,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.3" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "thiserror" @@ -406,18 +405,7 @@ version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -433,9 +421,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" diff --git a/Cargo.toml b/Cargo.toml index 31cc878..15b92be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,5 +9,5 @@ name = "_banquo_impl" crate-type = ["cdylib"] [dependencies] -banquo = { version = "0.1.1", path = "../banquo/banquo", features = ["parser"] } +banquo = { version = "0.2.0", features = ["parser"] } pyo3 = { version = "0.29.0", features = ["extension-module"] } From a99d5d9af79762168df6722b53877fdcd8ee67c6 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 10:58:57 -0700 Subject: [PATCH 23/36] Update python dependency groups Replace pyright with pyrefly and create more dependency groups for better control in CI workflows --- pyproject.toml | 9 ++++++++- uv.lock | 33 +++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 332d520..4f227d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,8 +34,15 @@ ci = [ ] dev = [ "maturin>=1.9.4", - "pytest>=8.3.5", "ruff>=0.14.0", + { include-group = "test" }, + { include-group = "types" }, +] +test = [ + "pytest>=8.4.2", +] +types = [ + "pyrefly>=1.1.1", ] [tool.maturin] diff --git a/uv.lock b/uv.lock index 9b379ff..1d895dc 100644 --- a/uv.lock +++ b/uv.lock @@ -28,7 +28,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -116,9 +116,16 @@ ci = [ ] dev = [ { name = "maturin" }, + { name = "pyrefly" }, { name = "pytest" }, { name = "ruff" }, ] +test = [ + { name = "pytest" }, +] +types = [ + { name = "pyrefly" }, +] [package.metadata] requires-dist = [{ name = "typing-extensions", specifier = ">=4.13.2" }] @@ -127,9 +134,12 @@ requires-dist = [{ name = "typing-extensions", specifier = ">=4.13.2" }] ci = [{ name = "basedpyright", specifier = ">=1.31.6" }] dev = [ { name = "maturin", specifier = ">=1.9.4" }, - { name = "pytest", specifier = ">=8.3.5" }, + { name = "pyrefly", specifier = ">=1.1.1" }, + { name = "pytest", specifier = ">=8.4.2" }, { name = "ruff", specifier = ">=0.14.0" }, ] +test = [{ name = "pytest", specifier = ">=8.4.2" }] +types = [{ name = "pyrefly", specifier = ">=1.1.1" }] [[package]] name = "pygments" @@ -140,6 +150,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyrefly" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/20/976165fa4b1517a1a92f393b3f4d4badabfff1165eff09d4cd4908428183/pyrefly-1.1.1.tar.gz", hash = "sha256:6deda959f8603a7dbdf112c48983e2275b2903cf33c8c739ed65d7e71a4fd520", size = 5880491, upload-time = "2026-06-18T23:45:43.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/d6/02ba666018c6a1cb4ddfa2db98ada721adddd374db5c29ba47a0bf2637fa/pyrefly-1.1.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f4b8595f91885bc8b5e3c282ab68d1df21201668a84e6508b1e15f2feec0bb8d", size = 13631867, upload-time = "2026-06-18T23:45:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/71/47/7a3457dbbddb513a83cf4fe527d5d5ebda5201a1010ad2a6034030e3e358/pyrefly-1.1.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6b238e1362622d47a6eb5af704fd8b613c94e8c303386efd6350e3da59fecc8", size = 13075304, upload-time = "2026-06-18T23:45:16.865Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/70f4b3f42d58ed686a80df31e04eca54d88036cea4f9b96195c64ad0b2b5/pyrefly-1.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b50d4510e4f8aaea79e2c4b343a4d7a060c9451c0b2aa9bfe10d7ca1ef33d68d", size = 13446966, upload-time = "2026-06-18T23:45:19.644Z" }, + { url = "https://files.pythonhosted.org/packages/3c/53/12a19bd6c7af985bcbc13c6910d0f9f6684069ead2282a5c08c2bfbb5d03/pyrefly-1.1.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f330cf039ef3da3b910c84f3a7e431f0cf8d0c1d2dad26491d6cadf3c7cd4759", size = 14449222, upload-time = "2026-06-18T23:45:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/93/f0/e55c48a50076fc0f9ecf4bdedec50456db383e01162f5e2121f8468be071/pyrefly-1.1.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6342d87c52b04f72156da04f554c4d57f3616f2b32d1763969efb22d05a1407", size = 14472947, upload-time = "2026-06-18T23:45:24.858Z" }, + { url = "https://files.pythonhosted.org/packages/b6/e7/30e085b31fed978ecb675bdbb54df566673ab550469e5af2d350f6af0be6/pyrefly-1.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c08b814ad03175e9cf47111390537161828b472044c39ab3320252b3ac6b2edd", size = 13975252, upload-time = "2026-06-18T23:45:27.247Z" }, + { url = "https://files.pythonhosted.org/packages/47/58/49c3e67641133d3fe5d8d9a660dc0826c6c37ca197d86cad05fa7dd8bfd6/pyrefly-1.1.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d50cad97f19fc893b04deff7239626cffff5dd27ffb29b7d303a1b770247b208", size = 13471780, upload-time = "2026-06-18T23:45:29.775Z" }, + { url = "https://files.pythonhosted.org/packages/71/1e/65a7ba8355e2c39d8331832905fb74dcc85fc122a3f1dfd6dbf2a88907ad/pyrefly-1.1.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2150b450ee6a6bcbe69b2d45d9a4ebc934a609e1abcf65e490433f38eb873d84", size = 13989306, upload-time = "2026-06-18T23:45:32.576Z" }, + { url = "https://files.pythonhosted.org/packages/37/de/b7ee1ab2392c36945738246fba7524439810befa3cfcc03cb6157567fc10/pyrefly-1.1.1-py3-none-win32.whl", hash = "sha256:5ffd8a8ed62fe4e6bf0afe1837d1bad149bb3b9f80e928ef248c96b836db3742", size = 12608469, upload-time = "2026-06-18T23:45:35.419Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9c/a0f5b52934bf80e9c7eff08222e7caf318287b9aef76acb8d9ac5740581b/pyrefly-1.1.1-py3-none-win_amd64.whl", hash = "sha256:4e0430f3ef69c8ac73505fd6584db70ed504665a9f0816fef7f723de510f26cb", size = 13502172, upload-time = "2026-06-18T23:45:38.375Z" }, + { url = "https://files.pythonhosted.org/packages/42/3d/4c6bcb3d456835f51445d3662a428f56c3ea5643ec798c577030ae34298c/pyrefly-1.1.1-py3-none-win_arm64.whl", hash = "sha256:83baf0db71e172665db1fca0ced50b8f7773f5192ca57e8ac6773a772b6d2fc5", size = 12895979, upload-time = "2026-06-18T23:45:41.026Z" }, +] + [[package]] name = "pytest" version = "8.4.2" From 5993b49192208351f39d4088c8453e1e9f419948 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 11:00:58 -0700 Subject: [PATCH 24/36] Update CI workflow definitions to use new dependency groups --- .github/workflows/lint.yaml | 4 ++-- .github/workflows/test.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 1f569a5..2a4e191 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -28,11 +28,11 @@ jobs: with: args: check --output-format=github - basedpyright: + pyrefly: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: astral-sh/setup-uv@v6 with: python-version: ${{ matrix.python-version }} - - run: uv run --no-dev --group ci basedpyright --outputjson + - run: uv run --no-dev --group types pyrefly check --output-format=github diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 400fa89..f07e6cf 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -24,6 +24,6 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Unit tests - run: uv run --frozen pytest -v -m unit + run: uv run --frozen --no-dev --group test pytest -v -m unit - name: Conformance tests - run: uv run --frozen pytest -v -m conformance + run: uv run --frozen --no-dev --group test pytest -v -m conformance From ab6aa4ab912c1febbd412158779628fb96054f97 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 11:08:56 -0700 Subject: [PATCH 25/36] Include test group in types group to make pytest available when type-checking test modules --- pyproject.toml | 2 ++ uv.lock | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4f227d1..bbb27ab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,8 @@ test = [ ] types = [ "pyrefly>=1.1.1", + # We include the test group here to make pytest available when type checking test modules + { include-group = "test" }, ] [tool.maturin] diff --git a/uv.lock b/uv.lock index 1d895dc..50ed5ed 100644 --- a/uv.lock +++ b/uv.lock @@ -125,6 +125,7 @@ test = [ ] types = [ { name = "pyrefly" }, + { name = "pytest" }, ] [package.metadata] @@ -139,7 +140,10 @@ dev = [ { name = "ruff", specifier = ">=0.14.0" }, ] test = [{ name = "pytest", specifier = ">=8.4.2" }] -types = [{ name = "pyrefly", specifier = ">=1.1.1" }] +types = [ + { name = "pyrefly", specifier = ">=1.1.1" }, + { name = "pytest", specifier = ">=8.4.2" }, +] [[package]] name = "pygments" From e6741ad6968b9e76eb56ba05dd915d4e264d8199 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 11:09:43 -0700 Subject: [PATCH 26/36] Suppress pyrefly errors for known incorrect usage in operator test module --- tests/test_operators.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/test_operators.py b/tests/test_operators.py index 106363f..ae5abfb 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -91,10 +91,11 @@ def test_supported_metric(self, good_trace: Trace[GoodMetric], expected: Trace[f assert formula.evaluate(good_trace) == expected def test_unsupported_metric(self, bad_trace: Trace[BadMetric]): - formula = operators.Not(Const[BadMetric]()) # pyright: ignore[reportArgumentType, reportUnknownVariableType] + formula = operators.Not(Const[BadMetric]()) # pyrefly: ignore[bad-specialization] + with pytest.raises(operators.MetricAttributeError): - _ = formula.evaluate(bad_trace) # pyright: ignore[reportUnknownVariableType] + _ = formula.evaluate(bad_trace) L = TypeVar("L") @@ -153,10 +154,10 @@ def test_supported_metric( assert formula.evaluate(good_trace) == expected def test_unsupported_metric(self, bad_trace: Trace[tuple[BadMetric, BadMetric]]): - formula = operators.And(Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]()) # pyright: ignore[reportArgumentType, reportUnknownVariableType] + formula = operators.And(Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]()) # pyrefly: ignore[bad-specialization] with pytest.raises(operators.MetricAttributeError): - _ = formula.evaluate(bad_trace) # pyright: ignore[reportUnknownVariableType] + _ = formula.evaluate(bad_trace) class TestDisjunction(BinaryTest): @@ -178,10 +179,10 @@ def test_supported_metric( assert formula.evaluate(good_trace) == expected def test_unsupported_metric(self, bad_trace: Trace[tuple[BadMetric, BadMetric]]): - formula = operators.Or(Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]()) # pyright: ignore[reportArgumentType, reportUnknownVariableType] + formula = operators.Or(Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]()) # pyrefly: ignore[bad-specialization] with pytest.raises(operators.MetricAttributeError): - _ = formula.evaluate(bad_trace) # pyright: ignore[reportUnknownVariableType] + _ = formula.evaluate(bad_trace) class TestImplication(BinaryTest): @@ -204,10 +205,10 @@ def test_supported_metric( assert result == expected def test_unsupported_metric(self, bad_trace: Trace[tuple[BadMetric, BadMetric]]): - formula = operators.Implies(Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]()) # pyright: ignore[reportArgumentType, reportUnknownVariableType] + formula = operators.Implies(Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]()) # pyrefly: ignore[bad-specialization] with pytest.raises(operators.MetricAttributeError): - _ = formula.evaluate(bad_trace) # pyright: ignore[reportUnknownVariableType] + _ = formula.evaluate(bad_trace) class TestNext(UnaryTest): @@ -273,10 +274,10 @@ def test_supported_metric(self, good_trace: Trace[GoodMetric], expected: Trace[f assert formula.evaluate(good_trace) == expected def test_unsupported_metric(self, bad_trace: Trace[BadMetric]): - formula = operators.Always(Const[BadMetric]()) # pyright: ignore[reportArgumentType, reportUnknownVariableType] + formula = operators.Always(Const[BadMetric]()) # pyrefly: ignore[bad-specialization] with pytest.raises(operators.MetricAttributeError): - _ = formula.evaluate(bad_trace) # pyright: ignore[reportUnknownVariableType] + _ = formula.evaluate(bad_trace) class TestFinally(UnaryTest): @@ -318,10 +319,10 @@ def test_supported_metric(self, good_trace: Trace[GoodMetric], expected: Trace[f assert formula.evaluate(good_trace) == expected def test_unsupported_metric(self, bad_trace: Trace[BadMetric]): - formula = operators.Eventually(Const[BadMetric]()) # pyright: ignore[reportArgumentType, reportUnknownVariableType] + formula = operators.Eventually(Const[BadMetric]()) # pyrefly: ignore[bad-specialization] with pytest.raises(operators.MetricAttributeError): - _ = formula.evaluate(bad_trace) # pyright: ignore[reportUnknownVariableType] + _ = formula.evaluate(bad_trace) @formula From 232bb2b43bfc404013c3d2bf2f49e47719ca3941 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 11:12:49 -0700 Subject: [PATCH 27/36] Format python modules --- banquo/_banquo_impl.pyi | 3 +-- tests/test_operators.py | 15 ++++++++++----- tests/test_stl.py | 1 + 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/banquo/_banquo_impl.pyi b/banquo/_banquo_impl.pyi index 8198613..c4249ba 100644 --- a/banquo/_banquo_impl.pyi +++ b/banquo/_banquo_impl.pyi @@ -77,8 +77,7 @@ class Eventually(Formula[S, M_ge]): def evaluate(self, trace: Trace[S]) -> Trace[M_ge]: ... class _StlModule(ModuleType): - class Formula(Formula[dict[str, float], float]): - ... + class Formula(Formula[dict[str, float], float]): ... parse: Callable[[str], _StlModule.Formula] diff --git a/tests/test_operators.py b/tests/test_operators.py index ae5abfb..5cc4f25 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -91,8 +91,7 @@ def test_supported_metric(self, good_trace: Trace[GoodMetric], expected: Trace[f assert formula.evaluate(good_trace) == expected def test_unsupported_metric(self, bad_trace: Trace[BadMetric]): - formula = operators.Not(Const[BadMetric]()) # pyrefly: ignore[bad-specialization] - + formula = operators.Not(Const[BadMetric]()) # pyrefly: ignore[bad-specialization] with pytest.raises(operators.MetricAttributeError): _ = formula.evaluate(bad_trace) @@ -154,7 +153,9 @@ def test_supported_metric( assert formula.evaluate(good_trace) == expected def test_unsupported_metric(self, bad_trace: Trace[tuple[BadMetric, BadMetric]]): - formula = operators.And(Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]()) # pyrefly: ignore[bad-specialization] + formula = operators.And( # pyrefly: ignore[bad-specialization] + Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]() + ) with pytest.raises(operators.MetricAttributeError): _ = formula.evaluate(bad_trace) @@ -179,7 +180,9 @@ def test_supported_metric( assert formula.evaluate(good_trace) == expected def test_unsupported_metric(self, bad_trace: Trace[tuple[BadMetric, BadMetric]]): - formula = operators.Or(Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]()) # pyrefly: ignore[bad-specialization] + formula = operators.Or( # pyrefly: ignore[bad-specialization] + Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]() + ) with pytest.raises(operators.MetricAttributeError): _ = formula.evaluate(bad_trace) @@ -205,7 +208,9 @@ def test_supported_metric( assert result == expected def test_unsupported_metric(self, bad_trace: Trace[tuple[BadMetric, BadMetric]]): - formula = operators.Implies(Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]()) # pyrefly: ignore[bad-specialization] + formula = operators.Implies( # pyrefly: ignore[bad-specialization] + Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]() + ) with pytest.raises(operators.MetricAttributeError): _ = formula.evaluate(bad_trace) diff --git a/tests/test_stl.py b/tests/test_stl.py index 958cee1..378128b 100644 --- a/tests/test_stl.py +++ b/tests/test_stl.py @@ -4,6 +4,7 @@ from banquo import Trace as _Trace from banquo import operators as ops + def test_parse() -> None: formula = stl.parse("always x <= 10.0") assert isinstance(formula, stl.Formula) From 8a6cec8eec488c8a3c024ca08158cbca9ca17c19 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 11:18:33 -0700 Subject: [PATCH 28/36] Update Ruff minimum version to 0.15 --- pyproject.toml | 2 +- uv.lock | 39 +++++++++++++++++++-------------------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bbb27ab..577d56f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ ci = [ ] dev = [ "maturin>=1.9.4", - "ruff>=0.14.0", + "ruff>=0.15.0", { include-group = "test" }, { include-group = "types" }, ] diff --git a/uv.lock b/uv.lock index 50ed5ed..bf4508e 100644 --- a/uv.lock +++ b/uv.lock @@ -193,28 +193,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.14.0" +version = "0.15.22" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/41/b9/9bd84453ed6dd04688de9b3f3a4146a1698e8faae2ceeccce4e14c67ae17/ruff-0.14.0.tar.gz", hash = "sha256:62ec8969b7510f77945df916de15da55311fade8d6050995ff7f680afe582c57", size = 5452071, upload-time = "2025-10-07T18:21:55.763Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/06/ae069393fc66e8ff33036d4b368003833bf6e88ccf182e17e7a2f1c754fd/ruff-0.15.22.tar.gz", hash = "sha256:3f15175b1fb580126f58285a5dae6b2ea89000136d980c64499211f116b54809", size = 4785063, upload-time = "2026-07-16T15:14:13.244Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/4e/79d463a5f80654e93fa653ebfb98e0becc3f0e7cf6219c9ddedf1e197072/ruff-0.14.0-py3-none-linux_armv6l.whl", hash = "sha256:58e15bffa7054299becf4bab8a1187062c6f8cafbe9f6e39e0d5aface455d6b3", size = 12494532, upload-time = "2025-10-07T18:21:00.373Z" }, - { url = "https://files.pythonhosted.org/packages/ee/40/e2392f445ed8e02aa6105d49db4bfff01957379064c30f4811c3bf38aece/ruff-0.14.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:838d1b065f4df676b7c9957992f2304e41ead7a50a568185efd404297d5701e8", size = 13160768, upload-time = "2025-10-07T18:21:04.73Z" }, - { url = "https://files.pythonhosted.org/packages/75/da/2a656ea7c6b9bd14c7209918268dd40e1e6cea65f4bb9880eaaa43b055cd/ruff-0.14.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:703799d059ba50f745605b04638fa7e9682cc3da084b2092feee63500ff3d9b8", size = 12363376, upload-time = "2025-10-07T18:21:07.833Z" }, - { url = "https://files.pythonhosted.org/packages/42/e2/1ffef5a1875add82416ff388fcb7ea8b22a53be67a638487937aea81af27/ruff-0.14.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ba9a8925e90f861502f7d974cc60e18ca29c72bb0ee8bfeabb6ade35a3abde7", size = 12608055, upload-time = "2025-10-07T18:21:10.72Z" }, - { url = "https://files.pythonhosted.org/packages/4a/32/986725199d7cee510d9f1dfdf95bf1efc5fa9dd714d0d85c1fb1f6be3bc3/ruff-0.14.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e41f785498bd200ffc276eb9e1570c019c1d907b07cfb081092c8ad51975bbe7", size = 12318544, upload-time = "2025-10-07T18:21:13.741Z" }, - { url = "https://files.pythonhosted.org/packages/9a/ed/4969cefd53315164c94eaf4da7cfba1f267dc275b0abdd593d11c90829a3/ruff-0.14.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30a58c087aef4584c193aebf2700f0fbcfc1e77b89c7385e3139956fa90434e2", size = 14001280, upload-time = "2025-10-07T18:21:16.411Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ad/96c1fc9f8854c37681c9613d825925c7f24ca1acfc62a4eb3896b50bacd2/ruff-0.14.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f8d07350bc7af0a5ce8812b7d5c1a7293cf02476752f23fdfc500d24b79b783c", size = 15027286, upload-time = "2025-10-07T18:21:19.577Z" }, - { url = "https://files.pythonhosted.org/packages/b3/00/1426978f97df4fe331074baf69615f579dc4e7c37bb4c6f57c2aad80c87f/ruff-0.14.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eec3bbbf3a7d5482b5c1f42d5fc972774d71d107d447919fca620b0be3e3b75e", size = 14451506, upload-time = "2025-10-07T18:21:22.779Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/9c1cea6e493c0cf0647674cca26b579ea9d2a213b74b5c195fbeb9678e15/ruff-0.14.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16b68e183a0e28e5c176d51004aaa40559e8f90065a10a559176713fcf435206", size = 13437384, upload-time = "2025-10-07T18:21:25.758Z" }, - { url = "https://files.pythonhosted.org/packages/29/b4/4cd6a4331e999fc05d9d77729c95503f99eae3ba1160469f2b64866964e3/ruff-0.14.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb732d17db2e945cfcbbc52af0143eda1da36ca8ae25083dd4f66f1542fdf82e", size = 13447976, upload-time = "2025-10-07T18:21:28.83Z" }, - { url = "https://files.pythonhosted.org/packages/3b/c0/ac42f546d07e4f49f62332576cb845d45c67cf5610d1851254e341d563b6/ruff-0.14.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:c958f66ab884b7873e72df38dcabee03d556a8f2ee1b8538ee1c2bbd619883dd", size = 13682850, upload-time = "2025-10-07T18:21:31.842Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c4/4b0c9bcadd45b4c29fe1af9c5d1dc0ca87b4021665dfbe1c4688d407aa20/ruff-0.14.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7eb0499a2e01f6e0c285afc5bac43ab380cbfc17cd43a2e1dd10ec97d6f2c42d", size = 12449825, upload-time = "2025-10-07T18:21:35.074Z" }, - { url = "https://files.pythonhosted.org/packages/4b/a8/e2e76288e6c16540fa820d148d83e55f15e994d852485f221b9524514730/ruff-0.14.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4c63b2d99fafa05efca0ab198fd48fa6030d57e4423df3f18e03aa62518c565f", size = 12272599, upload-time = "2025-10-07T18:21:38.08Z" }, - { url = "https://files.pythonhosted.org/packages/18/14/e2815d8eff847391af632b22422b8207704222ff575dec8d044f9ab779b2/ruff-0.14.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:668fce701b7a222f3f5327f86909db2bbe99c30877c8001ff934c5413812ac02", size = 13193828, upload-time = "2025-10-07T18:21:41.216Z" }, - { url = "https://files.pythonhosted.org/packages/44/c6/61ccc2987cf0aecc588ff8f3212dea64840770e60d78f5606cd7dc34de32/ruff-0.14.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a86bf575e05cb68dcb34e4c7dfe1064d44d3f0c04bbc0491949092192b515296", size = 13628617, upload-time = "2025-10-07T18:21:44.04Z" }, - { url = "https://files.pythonhosted.org/packages/73/e6/03b882225a1b0627e75339b420883dc3c90707a8917d2284abef7a58d317/ruff-0.14.0-py3-none-win32.whl", hash = "sha256:7450a243d7125d1c032cb4b93d9625dea46c8c42b4f06c6b709baac168e10543", size = 12367872, upload-time = "2025-10-07T18:21:46.67Z" }, - { url = "https://files.pythonhosted.org/packages/41/77/56cf9cf01ea0bfcc662de72540812e5ba8e9563f33ef3d37ab2174892c47/ruff-0.14.0-py3-none-win_amd64.whl", hash = "sha256:ea95da28cd874c4d9c922b39381cbd69cb7e7b49c21b8152b014bd4f52acddc2", size = 13464628, upload-time = "2025-10-07T18:21:50.318Z" }, - { url = "https://files.pythonhosted.org/packages/c6/2a/65880dfd0e13f7f13a775998f34703674a4554906167dce02daf7865b954/ruff-0.14.0-py3-none-win_arm64.whl", hash = "sha256:f42c9495f5c13ff841b1da4cb3c2a42075409592825dada7c5885c2c844ac730", size = 12565142, upload-time = "2025-10-07T18:21:53.577Z" }, + { url = "https://files.pythonhosted.org/packages/23/18/ee54b7ae1e121be7a28ea6da4b67564ebb0530e183a54415ab7e3bcd2c4e/ruff-0.15.22-py3-none-linux_armv6l.whl", hash = "sha256:44423e73493737f5e7c5b41d475483898ff37afcdae38bc3da5085e29af1c2d8", size = 10781258, upload-time = "2026-07-16T15:13:19.452Z" }, + { url = "https://files.pythonhosted.org/packages/2f/d2/2520cb14761ddbeaf57642a76942fc36adcbdbe53b4532241995f6fc485c/ruff-0.15.22-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b82c6482946e9eda7ff2e091d25b8bad3f718684e1916d41bd56873cee05b697", size = 10999477, upload-time = "2026-07-16T15:13:23.318Z" }, + { url = "https://files.pythonhosted.org/packages/c9/10/74e53572aa758dfaa678c2a2646b5c5515d884b7ca56be4d2ce03ca4b560/ruff-0.15.22-py3-none-macosx_11_0_arm64.whl", hash = "sha256:11c1c715af53a09f714e011106bffc419751ec8232fcb5da42173284ea3fec6f", size = 10466716, upload-time = "2026-07-16T15:13:26.162Z" }, + { url = "https://files.pythonhosted.org/packages/1e/cc/44eaaf0844e028182f2d0a8f2190d0f359159aed0a9e5ab861d892f1ae2a/ruff-0.15.22-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:742a29cf29bddb7c8327895d6a10e0e6c5b38a96dd407af9b5d0857f809c0576", size = 10892644, upload-time = "2026-07-16T15:13:29.229Z" }, + { url = "https://files.pythonhosted.org/packages/9f/21/8edf559014d2b0f82beea19cfb713993ad802ccda16868769979c6090a84/ruff-0.15.22-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72af58b951b0ae395935ae79763dc349bc0eb706319d28f7a33ad2cfb3cfc178", size = 10576719, upload-time = "2026-07-16T15:13:32.35Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1e/3a13abd392a3b50b62e5938a831f9ab6e588358cacad5c18545b716d2182/ruff-0.15.22-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d425005c1835eb24e2ee4161cb90e8db263415f4a71c8c72c33abaa6c0c224", size = 11376494, upload-time = "2026-07-16T15:13:35.958Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3e/422d3d95bcf04dd78e1aeac22184d4f9a8fb2c01865d39d44618484a0317/ruff-0.15.22-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b9b3f8779a4f08c969defc3c8c35abffaa757e601ed5ae66d6d1db6519969a", size = 12208370, upload-time = "2026-07-16T15:13:39.185Z" }, + { url = "https://files.pythonhosted.org/packages/1e/91/5d065a0e0a02bf4813f5119ad278462eed081d2b832eb7c021ade0ec9e65/ruff-0.15.22-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e0dd1b2e4d3d585f897a0d137cbf4eaf6223bef4e8ce34d6bb12556c5f9249e", size = 11581098, upload-time = "2026-07-16T15:13:42.132Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f9/a0d4871d12fae702eb1f41b686caf05f1f8b124dc6db6f784f53d74918fa/ruff-0.15.22-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365523eb91d9224e1bcb03b022fbf0facb8f9e23792a2c53d9d4b3924bdbdebb", size = 11399422, upload-time = "2026-07-16T15:13:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/18/80/c843a5176cddbceb0b7e8dd41cf9993490796c1c469348d384f5a5c13c56/ruff-0.15.22-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:fabfd168afdf29fee5be98b831efa9683c94d7c5a3b58b9ce5a2e38444589a74", size = 11381683, upload-time = "2026-07-16T15:13:48.46Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/8485de0ae92239438a36cfc51350db9b9e85c9ebdfaea91b18e422706662/ruff-0.15.22-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:225dbf095a87f1d9f90f5fd7924d2613ee452a75a4308c63a8f50f761787aa7c", size = 10850295, upload-time = "2026-07-16T15:13:51.655Z" }, + { url = "https://files.pythonhosted.org/packages/fa/91/24977ec2ec72eaf15e4394ace2959fdff2dd1e14f03e005e838023407169/ruff-0.15.22-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1877d63b9d24ed278744f1523fd11b85540566d54641f97c566d7d9dc5ca5296", size = 10579640, upload-time = "2026-07-16T15:13:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/9c/47/9b51216951974df1f263ac19da550d34252e0ed7218c25f10c5ef9ed7517/ruff-0.15.22-py3-none-musllinux_1_2_i686.whl", hash = "sha256:a1606c510bd7215680d32efab38965f7cdec3ef69f5170a3f4791404ffdd5262", size = 11105077, upload-time = "2026-07-16T15:13:57.915Z" }, + { url = "https://files.pythonhosted.org/packages/c2/47/20e9d4a3b8016778acea5fc32bb50d35d207500a17ddb529ffa6996feef8/ruff-0.15.22-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:630479b18625f5ffc373f77603a22a9f8ac0acd7ff0501178b5db28ec71e9c64", size = 11490980, upload-time = "2026-07-16T15:14:01.032Z" }, + { url = "https://files.pythonhosted.org/packages/4d/76/3f72d8fc38c1cb77b38c56a70da9d0c17700cc1cc50f9649c9d3c8f5ba71/ruff-0.15.22-py3-none-win32.whl", hash = "sha256:e5ba0e4a13fd14abbed2a77b517a3911290c6c6c59ef67784328d1668fab76cf", size = 10789165, upload-time = "2026-07-16T15:14:04.16Z" }, + { url = "https://files.pythonhosted.org/packages/cb/46/4965251734c2b6fcdca1b1b187d20bcac3af0ee5b083b89c910bb961ce3a/ruff-0.15.22-py3-none-win_amd64.whl", hash = "sha256:9be63ba1eb936acd2d1342fb8337c356353706fce233b2a15a09a97037e6acde", size = 11938297, upload-time = "2026-07-16T15:14:07.316Z" }, + { url = "https://files.pythonhosted.org/packages/57/c9/e69b1ff4c8b69093ef08b8919ab767af0569666865b39c30a8795d88d3c6/ruff-0.15.22-py3-none-win_arm64.whl", hash = "sha256:e1168075b72158510839f250027659cdd78476f40507dd517892304c41318661", size = 11298172, upload-time = "2026-07-16T15:14:10.51Z" }, ] [[package]] From d2b6afb454e6c76dd92ad4d8e4876e6dece0d5dd Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 11:17:11 -0700 Subject: [PATCH 29/36] Format README example using Ruff --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 8b6135b..24a4831 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,6 @@ these terms is a temporal logic formula, which can be used to evaluate a each state. This is an example of this process: ```python - import banquo as bq import banquo.operators as ops From 14ef3ba0d3114ef1af84dc3a26bcacc79b441b3b Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 13:04:31 -0700 Subject: [PATCH 30/36] Update lockfile --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index bf4508e..b0ce2b3 100644 --- a/uv.lock +++ b/uv.lock @@ -137,7 +137,7 @@ dev = [ { name = "maturin", specifier = ">=1.9.4" }, { name = "pyrefly", specifier = ">=1.1.1" }, { name = "pytest", specifier = ">=8.4.2" }, - { name = "ruff", specifier = ">=0.14.0" }, + { name = "ruff", specifier = ">=0.15.0" }, ] test = [{ name = "pytest", specifier = ">=8.4.2" }] types = [ From 087f0be88071ae4cc0531ddc3d994d495fe3e849 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 13:07:13 -0700 Subject: [PATCH 31/36] Reorganize imports --- banquo/__init__.py | 4 ++-- banquo/_banquo_impl.pyi | 16 ++++++++-------- banquo/core.py | 4 ++-- banquo/expressions.py | 2 +- banquo/operators.py | 14 ++++++-------- banquo/stl.py | 2 +- banquo/trace.py | 3 +-- tests/test_operators.py | 2 +- 8 files changed, 22 insertions(+), 25 deletions(-) diff --git a/banquo/__init__.py b/banquo/__init__.py index 0e1de7b..1009c15 100644 --- a/banquo/__init__.py +++ b/banquo/__init__.py @@ -1,10 +1,10 @@ from typing import Final +from ._banquo_impl import Bottom as _Bottom +from ._banquo_impl import Top as _Top from .core import Formula, evaluate, formula from .expressions import Predicate from .trace import Trace -from ._banquo_impl import Top as _Top -from ._banquo_impl import Bottom as _Bottom Top: Final[_Top] = _Top() Bottom: Final[_Bottom] = _Bottom() diff --git a/banquo/_banquo_impl.pyi b/banquo/_banquo_impl.pyi index c4249ba..fd8d0ed 100644 --- a/banquo/_banquo_impl.pyi +++ b/banquo/_banquo_impl.pyi @@ -1,11 +1,11 @@ from collections.abc import Callable, Iterable -from typing import Any, Generic, TypeVar from types import ModuleType +from typing import Any, Generic -from typing_extensions import Self, override +from typing_extensions import Self, TypeVar, override from .core import Formula -from .operators import Bounds, M, M_neg, M_le, M_ge, M_neg_ge +from .operators import Bounds, M, M_ge, M_le, M_neg, M_neg_ge class PanicException(Exception): ... @@ -27,12 +27,12 @@ class Bottom(Any): # pyright: ignore[reportAny, reportExplicitAny] def __ge__(self, other: object) -> bool: ... def __gt__(self, other: object) -> bool: ... -class Trace(Generic[T]): - def __new__(cls, elements: dict[float, T] | Trace[T]) -> Self: ... - def __getitem__(self, value: float) -> T: ... +class Trace(Generic[_T_co]): + def __new__(cls, elements: dict[float, _T_co] | Trace[_T_co]) -> Self: ... + def __getitem__(self, value: float) -> _T_co: ... def times(self) -> Iterable[float]: ... - def states(self) -> Iterable[T]: ... - def at_time(self, time: float) -> T | None: ... + def states(self) -> Iterable[_T_co]: ... + def at_time(self, time: float) -> _T_co | None: ... class Predicate(Formula[dict[str, float], float]): def __new__(cls, coefficients: dict[str, float], constant: float) -> Self: ... diff --git a/banquo/core.py b/banquo/core.py index 678ad6a..46c5bac 100644 --- a/banquo/core.py +++ b/banquo/core.py @@ -1,9 +1,9 @@ from __future__ import annotations from collections.abc import Callable -from typing import Protocol, TypeVar +from typing import Protocol -from typing_extensions import Self, override +from typing_extensions import Self, TypeVar, override from ._banquo_impl import Trace as _Trace from .trace import Trace diff --git a/banquo/expressions.py b/banquo/expressions.py index 576c244..263b6b2 100644 --- a/banquo/expressions.py +++ b/banquo/expressions.py @@ -3,8 +3,8 @@ from collections.abc import Mapping from ._banquo_impl import Predicate as _Predicate -from .operators import OperatorMixin from .core import EnsureOutput +from .operators import OperatorMixin class Predicate(EnsureOutput[dict[str, float], float], OperatorMixin): diff --git a/banquo/operators.py b/banquo/operators.py index 933e9f5..182beb8 100644 --- a/banquo/operators.py +++ b/banquo/operators.py @@ -1,23 +1,21 @@ from __future__ import annotations -import typing +from typing_extensions import TypeAlias, TypeVar, override -from typing_extensions import TypeAlias, override - -from ._banquo_impl import And as _And from ._banquo_impl import Always as _Always +from ._banquo_impl import And as _And from ._banquo_impl import Eventually as _Eventually from ._banquo_impl import Implies as _Implies from ._banquo_impl import Next as _Next from ._banquo_impl import Not as _Not from ._banquo_impl import Or as _Or from .core import ( - Formula, EnsureInput, - SupportsNeg, - SupportsLT, - SupportsGT, EnsureOutput, + Formula, + SupportsGT, + SupportsLT, + SupportsNeg, SupportsNegGE, ) from .trace import Trace diff --git a/banquo/stl.py b/banquo/stl.py index 7fcd8f8..d28a057 100644 --- a/banquo/stl.py +++ b/banquo/stl.py @@ -1,7 +1,7 @@ from __future__ import annotations -from .core import EnsureOutput from ._banquo_impl import stl as _stl +from .core import EnsureOutput class Formula(EnsureOutput[dict[str, float], float]): diff --git a/banquo/trace.py b/banquo/trace.py index 7547c71..da22fe1 100644 --- a/banquo/trace.py +++ b/banquo/trace.py @@ -1,9 +1,8 @@ from __future__ import annotations from collections.abc import Iterable, Iterator, Mapping -from typing import TypeVar -from typing_extensions import override +from typing_extensions import TypeVar, override from ._banquo_impl import Trace as _Trace diff --git a/tests/test_operators.py b/tests/test_operators.py index 5cc4f25..3492016 100644 --- a/tests/test_operators.py +++ b/tests/test_operators.py @@ -6,7 +6,7 @@ import pytest import typing_extensions -from banquo import Bottom, Trace, operators, formula +from banquo import Bottom, Trace, formula, operators from banquo.core import Formula pytestmark = pytest.mark.unit From 237fede1d2dec5e1ff76af597b743d5bb96af29a Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 13:08:57 -0700 Subject: [PATCH 32/36] Use new infer_variance flag instead of manually specifying type variable variance --- banquo/_banquo_impl.pyi | 56 ++++++++++++++++++++--------------------- banquo/core.py | 4 +-- banquo/operators.py | 18 ++++++------- banquo/trace.py | 4 +-- 4 files changed, 41 insertions(+), 41 deletions(-) diff --git a/banquo/_banquo_impl.pyi b/banquo/_banquo_impl.pyi index fd8d0ed..b7dfba0 100644 --- a/banquo/_banquo_impl.pyi +++ b/banquo/_banquo_impl.pyi @@ -9,7 +9,7 @@ from .operators import Bounds, M, M_ge, M_le, M_neg, M_neg_ge class PanicException(Exception): ... -T = TypeVar("T", covariant=True) +_T = TypeVar("_T", infer_variance=True) class Top(Any): # pyright: ignore[reportAny, reportExplicitAny] def __le__(self, other: object) -> bool: ... @@ -27,54 +27,54 @@ class Bottom(Any): # pyright: ignore[reportAny, reportExplicitAny] def __ge__(self, other: object) -> bool: ... def __gt__(self, other: object) -> bool: ... -class Trace(Generic[_T_co]): - def __new__(cls, elements: dict[float, _T_co] | Trace[_T_co]) -> Self: ... - def __getitem__(self, value: float) -> _T_co: ... +class Trace(Generic[_T]): + def __new__(cls, elements: dict[float, _T] | Trace[_T]) -> Self: ... + def __getitem__(self, value: float) -> _T: ... def times(self) -> Iterable[float]: ... - def states(self) -> Iterable[_T_co]: ... - def at_time(self, time: float) -> _T_co | None: ... + def states(self) -> Iterable[_T]: ... + def at_time(self, time: float) -> _T | None: ... class Predicate(Formula[dict[str, float], float]): def __new__(cls, coefficients: dict[str, float], constant: float) -> Self: ... @override def evaluate(self, trace: Trace[dict[str, float]]) -> Trace[float]: ... -S = TypeVar("S", contravariant=True) +_S = TypeVar("_S", infer_variance=True) -class Not(Formula[S, M_neg]): - def __new__(cls, subformula: Formula[S, M_neg]) -> Self: ... +class Not(Formula[_S, M_neg]): + def __new__(cls, subformula: Formula[_S, M_neg]) -> Self: ... @override - def evaluate(self, trace: Trace[S]) -> Trace[M_neg]: ... + def evaluate(self, trace: Trace[_S]) -> Trace[M_neg]: ... -class And(Formula[S, M_le]): - def __new__(cls, lhs: Formula[S, M_le], rhs: Formula[S, M_le]) -> Self: ... +class And(Formula[_S, M_le]): + def __new__(cls, lhs: Formula[_S, M_le], rhs: Formula[_S, M_le]) -> Self: ... @override - def evaluate(self, trace: Trace[S]) -> Trace[M_le]: ... + def evaluate(self, trace: Trace[_S]) -> Trace[M_le]: ... -class Or(Formula[S, M_ge]): - def __new__(cls, lhs: Formula[S, M_ge], rhs: Formula[S, M_ge]) -> Self: ... +class Or(Formula[_S, M_ge]): + def __new__(cls, lhs: Formula[_S, M_ge], rhs: Formula[_S, M_ge]) -> Self: ... @override - def evaluate(self, trace: Trace[S]) -> Trace[M_ge]: ... + def evaluate(self, trace: Trace[_S]) -> Trace[M_ge]: ... -class Implies(Formula[S, M_neg_ge]): - def __new__(cls, lhs: Formula[S, M_neg_ge], rhs: Formula[S, M_neg_ge]) -> Self: ... +class Implies(Formula[_S, M_neg_ge]): + def __new__(cls, lhs: Formula[_S, M_neg_ge], rhs: Formula[_S, M_neg_ge]) -> Self: ... @override - def evaluate(self, trace: Trace[S]) -> Trace[M_neg_ge]: ... + def evaluate(self, trace: Trace[_S]) -> Trace[M_neg_ge]: ... -class Next(Formula[S, M]): - def __new__(cls, subformula: Formula[S, M]) -> Self: ... +class Next(Formula[_S, M]): + def __new__(cls, subformula: Formula[_S, M]) -> Self: ... @override - def evaluate(self, trace: Trace[S]) -> Trace[M]: ... + def evaluate(self, trace: Trace[_S]) -> Trace[M]: ... -class Always(Formula[S, M_le]): - def __new__(cls, bounds: Bounds | None, subformula: Formula[S, M_le]) -> Self: ... +class Always(Formula[_S, M_le]): + def __new__(cls, bounds: Bounds | None, subformula: Formula[_S, M_le]) -> Self: ... @override - def evaluate(self, trace: Trace[S]) -> Trace[M_le]: ... + def evaluate(self, trace: Trace[_S]) -> Trace[M_le]: ... -class Eventually(Formula[S, M_ge]): - def __new__(cls, bounds: Bounds | None, subformula: Formula[S, M_ge]) -> Self: ... +class Eventually(Formula[_S, M_ge]): + def __new__(cls, bounds: Bounds | None, subformula: Formula[_S, M_ge]) -> Self: ... @override - def evaluate(self, trace: Trace[S]) -> Trace[M_ge]: ... + def evaluate(self, trace: Trace[_S]) -> Trace[M_ge]: ... class _StlModule(ModuleType): class Formula(Formula[dict[str, float], float]): ... diff --git a/banquo/core.py b/banquo/core.py index 46c5bac..841cb28 100644 --- a/banquo/core.py +++ b/banquo/core.py @@ -8,8 +8,8 @@ from ._banquo_impl import Trace as _Trace from .trace import Trace -S = TypeVar("S", contravariant=True) -M = TypeVar("M", covariant=True) +S = TypeVar("S", infer_variance=True) +M = TypeVar("M", infer_variance=True) class Formula(Protocol[S, M]): diff --git a/banquo/operators.py b/banquo/operators.py index 182beb8..0d91187 100644 --- a/banquo/operators.py +++ b/banquo/operators.py @@ -22,12 +22,12 @@ Bounds: TypeAlias = tuple[float, float] -S = typing.TypeVar("S") -M = typing.TypeVar("M", covariant=True) -M_neg = typing.TypeVar("M_neg", bound=SupportsNeg, covariant=True) -M_le = typing.TypeVar("M_le", bound=SupportsLT, covariant=True) -M_ge = typing.TypeVar("M_ge", bound=SupportsGT, covariant=True) -M_neg_ge = typing.TypeVar("M_neg_ge", bound=SupportsNegGE, covariant=True) +S = TypeVar("S", infer_variance=True) +M = TypeVar("M", infer_variance=True) +M_neg = TypeVar("M_neg", bound=SupportsNeg, infer_variance=True) +M_le = TypeVar("M_le", bound=SupportsLT, infer_variance=True) +M_ge = TypeVar("M_ge", bound=SupportsGT, infer_variance=True) +M_neg_ge = TypeVar("M_neg_ge", bound=SupportsNegGE, infer_variance=True) class OperatorMixin: @@ -132,8 +132,8 @@ def __init__(self, subformula: Formula[S, M]): super().__init__(_Next(_inner_or_wrap(subformula)), "") -S_ = typing.TypeVar("S_") -M_le_ = typing.TypeVar("M_le_", bound=SupportsLT, covariant=True) +S_ = TypeVar("S_", infer_variance=True) +M_le_ = TypeVar("M_le_", bound=SupportsLT, infer_variance=True) class Always(Operator[S, M_le]): @@ -173,7 +173,7 @@ def with_bounds(bounds: Bounds, subformula: Formula[S_, M_le_]) -> Always[S_, M_ return Always(_Always(bounds, _inner_or_wrap(subformula))) -M_ge_ = typing.TypeVar("M_ge_", bound=SupportsGT, covariant=True) +M_ge_ = TypeVar("M_ge_", bound=SupportsGT, infer_variance=True) class Eventually(Operator[S, M_ge]): diff --git a/banquo/trace.py b/banquo/trace.py index da22fe1..1bebdf9 100644 --- a/banquo/trace.py +++ b/banquo/trace.py @@ -6,8 +6,8 @@ from ._banquo_impl import Trace as _Trace -T = TypeVar("T", covariant=True) -U = TypeVar("U", covariant=True) +T = TypeVar("T", infer_variance=True) +U = TypeVar("U", infer_variance=True) def _iter_eq(lhs: Iterable[object], rhs: Iterable[object]) -> bool: From c0aaabc0090b99b9b3f83cfcf4f442526ffce6c0 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 13:48:28 -0700 Subject: [PATCH 33/36] Bound upper maturin version in dev-dependencies This matches the version bounds in the build dependencies --- pyproject.toml | 2 +- uv.lock | 34 +++++++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 577d56f..b5856ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ ci = [ "basedpyright>=1.31.6", ] dev = [ - "maturin>=1.9.4", + "maturin>=1.9.4,<2.0", "ruff>=0.15.0", { include-group = "test" }, { include-group = "types" }, diff --git a/uv.lock b/uv.lock index b0ce2b3..15c1c80 100644 --- a/uv.lock +++ b/uv.lock @@ -46,26 +46,26 @@ wheels = [ [[package]] name = "maturin" -version = "1.9.4" +version = "1.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/7c/b11b870fc4fd84de2099906314ce45488ae17be32ff5493519a6cddc518a/maturin-1.9.4.tar.gz", hash = "sha256:235163a0c99bc6f380fb8786c04fd14dcf6cd622ff295ea3de525015e6ac40cf", size = 213647, upload-time = "2025-08-27T11:37:57.079Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/b3/addd877f871fb1860d46d3a4f206ecb10b946c85846805e6367631926fd3/maturin-1.14.1.tar.gz", hash = "sha256:9d6577a62cd08e0ceba7a0db06fb098e0c9b1b3429bad747a4f3a18215a1b3df", size = 369637, upload-time = "2026-06-19T05:19:49.774Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/90/0d99389eea1939116fca841cad0763600c8d3183a02a9478d066736c60e8/maturin-1.9.4-py3-none-linux_armv6l.whl", hash = "sha256:6ff37578e3f5fdbe685110d45f60af1f5a7dfce70a1e26dfe3810af66853ecae", size = 8276133, upload-time = "2025-08-27T11:37:23.325Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ed/c8ec68b383e50f084bf1fa9605e62a90cd32a3f75d9894ed3a6e5d4cc5b3/maturin-1.9.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f3837bb53611b2dafa1c090436c330f2d743ba305ef00d8801a371f4495e7e1b", size = 15994496, upload-time = "2025-08-27T11:37:27.092Z" }, - { url = "https://files.pythonhosted.org/packages/84/4e/401ff5f3cfc6b123364d4b94379bf910d7baee32c9c95b72784ff2329357/maturin-1.9.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4227d627d8e3bfe45877a8d65e9d8351a9d01434549f0da75d2c06a1b570de58", size = 8362228, upload-time = "2025-08-27T11:37:31.181Z" }, - { url = "https://files.pythonhosted.org/packages/51/8e/c56176dd360da9650c62b8a5ecfb85432cf011e97e46c186901e6996002e/maturin-1.9.4-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:1bb2aa0fa29032e9c5aac03ac400396ddea12cadef242f8967e9c8ef715313a1", size = 8271397, upload-time = "2025-08-27T11:37:33.672Z" }, - { url = "https://files.pythonhosted.org/packages/d2/46/001fcc5c6ad509874896418d6169a61acd619df5b724f99766308c44a99f/maturin-1.9.4-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:a0868d52934c8a5d1411b42367633fdb5cd5515bec47a534192282167448ec30", size = 8775625, upload-time = "2025-08-27T11:37:35.86Z" }, - { url = "https://files.pythonhosted.org/packages/b4/2e/26fa7574f01c19b7a74680fd70e5bae2e8c40fed9683d1752e765062cc2b/maturin-1.9.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:68b7b833b25741c0f553b78e8b9e095b31ae7c6611533b3c7b71f84c2cb8fc44", size = 8051117, upload-time = "2025-08-27T11:37:38.278Z" }, - { url = "https://files.pythonhosted.org/packages/73/ee/ca7308832d4f5b521c1aa176d9265f6f93e0bd1ad82a90fd9cd799f6b28c/maturin-1.9.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:08dc86312afee55af778af919818632e35d8d0464ccd79cb86700d9ea560ccd7", size = 8132122, upload-time = "2025-08-27T11:37:40.499Z" }, - { url = "https://files.pythonhosted.org/packages/45/e8/c623955da75e801a06942edf1fdc4e772a9e8fbc1ceebbdc85d59584dc10/maturin-1.9.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:ef20ffdd943078c4c3699c29fb2ed722bb6b4419efdade6642d1dbf248f94a70", size = 10586762, upload-time = "2025-08-27T11:37:42.718Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4b/19ad558fdf54e151b1b4916ed45f1952ada96684ee6db64f9cd91cabec09/maturin-1.9.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:368e958468431dfeec80f75eea9639b4356d8c42428b0128444424b083fecfb0", size = 8926988, upload-time = "2025-08-27T11:37:45.492Z" }, - { url = "https://files.pythonhosted.org/packages/7e/27/153ad15eccae26921e8a01812da9f3b7f9013368f8f92c36853f2043b2a3/maturin-1.9.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:273f879214f63f79bfe851cd7d541f8150bdbfae5dfdc3c0c4d125d02d1f41b4", size = 8536758, upload-time = "2025-08-27T11:37:48.213Z" }, - { url = "https://files.pythonhosted.org/packages/43/e3/f304c3bdc3fba9adebe5348d4d2dd015f1152c0a9027aaf52cae0bb182c8/maturin-1.9.4-py3-none-win32.whl", hash = "sha256:ed2e54d132ace7e61829bd49709331007dd9a2cc78937f598aa76a4f69b6804d", size = 7265200, upload-time = "2025-08-27T11:37:50.881Z" }, - { url = "https://files.pythonhosted.org/packages/14/14/f86d0124bf1816b99005c058a1dbdca7cb5850d9cf4b09dcae07a1bc6201/maturin-1.9.4-py3-none-win_amd64.whl", hash = "sha256:8e450bb2c9afdf38a0059ee2e1ec2b17323f152b59c16f33eb9c74edaf1f9f79", size = 8237391, upload-time = "2025-08-27T11:37:53.23Z" }, - { url = "https://files.pythonhosted.org/packages/3f/25/8320fc2591e45b750c3ae71fa596b47aefa802d07d6abaaa719034a85160/maturin-1.9.4-py3-none-win_arm64.whl", hash = "sha256:7a6f980a9b67a5c13c844c268eabd855b54a6a765df4b4bb07d15a990572a4c9", size = 6988277, upload-time = "2025-08-27T11:37:55.429Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/97c5a5bd9c71653a066c0976a484eaaae50b9369557838a4176b7b0bdaa5/maturin-1.14.1-py3-none-linux_armv6l.whl", hash = "sha256:522292398945442cdafa9daeb2271b2340fbde57027b818f923f88eab04174f8", size = 10207496, upload-time = "2026-06-19T05:19:09.321Z" }, + { url = "https://files.pythonhosted.org/packages/fe/83/294bca639b0e052f1e2f65199b3db258780c7d4e31408b934c9c974a1379/maturin-1.14.1-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ffe5ad71f21d1e6603c4dd75f7fee34adf5ed5ebcebb692886549888ebb329ed", size = 19680113, upload-time = "2026-06-19T05:19:13.43Z" }, + { url = "https://files.pythonhosted.org/packages/43/b6/79c881410a3b1c187f7eb3d407aecae646c6a4433d630d72200359015e83/maturin-1.14.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3306078070c1508fd715b9116070cbcaff5959024272a9f1e6f5cb29768b86c", size = 10169205, upload-time = "2026-06-19T05:19:16.615Z" }, + { url = "https://files.pythonhosted.org/packages/93/9d/44b6f26dcb7f7a04c5501ac2dbb6ca1490150682baa525ca5860504f9eab/maturin-1.14.1-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:cd457cd88961156e26379e1155bd287cc0ec1c8b2f1582b0660fb31b87c8842d", size = 10188098, upload-time = "2026-06-19T05:19:19.736Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bd/9c0d5d6983905ce2c9edaa073a7e89355a9cf7f396988e05d32f1c37785d/maturin-1.14.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:dfc54ae32e6fcb18302193ab9a30b0b25eefffba994ae13238974805533ef75e", size = 10627576, upload-time = "2026-06-19T05:19:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/e5/33/b096412bd6a7cb399652b260666f901adf88a687181a6dbd6a3f89f0a94e/maturin-1.14.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:a131d912b5267e640bc96d70f4914e10590aed64082ec9abacba7cea52004224", size = 10085181, upload-time = "2026-06-19T05:19:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/56/8d/08c3bf469c38a23c9e6c877e338193001eb604d010fedc08341974e38528/maturin-1.14.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:be18fc568fb76884c0205456336892a75105ec398e6b667cd777c6268bd06d69", size = 10026363, upload-time = "2026-06-19T05:19:28.904Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/c4d1a92839f8745ab4aab988a7db884a79d6d710bd3b286fcf9316dece1a/maturin-1.14.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:994a0c8ba3ad8a92b3a9ee1b02645d200d610216b15cff5102b0fe65e8e08666", size = 13321347, upload-time = "2026-06-19T05:19:32.411Z" }, + { url = "https://files.pythonhosted.org/packages/b3/fa/170f04624d03fd07d2a8b1b67de83a127af93aef9eaa425839553347297b/maturin-1.14.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be80866363e605d137991b491a741a84cde9ae350183c4c85f49690ca9aaaa65", size = 10877609, upload-time = "2026-06-19T05:19:35.448Z" }, + { url = "https://files.pythonhosted.org/packages/61/ad/1ae2e1d0ded282bf2c55ac13f0811d87deb425e200ae64a15785675dede9/maturin-1.14.1-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:5282dffd4b539d2be245f4e5b1a5ab6bc1033b58f4a4872f5833f9d43c954aa4", size = 10417316, upload-time = "2026-06-19T05:19:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/fb/27/bf677183920718da49cd7982d6a3ffc440aad8919329f571d189f81b7bdf/maturin-1.14.1-py3-none-win32.whl", hash = "sha256:1a04de0a20188f95c721b5702eed18140bdcccb28c386797093eca3f62f4d4e0", size = 8931293, upload-time = "2026-06-19T05:19:41.183Z" }, + { url = "https://files.pythonhosted.org/packages/63/4b/585adeb9167b08d3cdff0032a938b0e72655c92003df4f52c3f696a1bcc2/maturin-1.14.1-py3-none-win_amd64.whl", hash = "sha256:3c9f94640ecc4895e94abaf834a0684430032c865b2748a36c12461fd9252fdd", size = 10314067, upload-time = "2026-06-19T05:19:44.389Z" }, + { url = "https://files.pythonhosted.org/packages/51/d4/dac8c0720ae246be1700afb6fbdbbea20fe35b13f6570b2f70faa005df77/maturin-1.14.1-py3-none-win_arm64.whl", hash = "sha256:15cea8fcb3ba47dd636f50092bb34baea8b04ac777392f23e6bf8a9a61efb894", size = 9718943, upload-time = "2026-06-19T05:19:47.49Z" }, ] [[package]] @@ -104,7 +104,7 @@ wheels = [ [[package]] name = "pybanquo" -version = "0.1.0" +version = "0.1.1" source = { editable = "." } dependencies = [ { name = "typing-extensions" }, @@ -134,7 +134,7 @@ requires-dist = [{ name = "typing-extensions", specifier = ">=4.13.2" }] [package.metadata.requires-dev] ci = [{ name = "basedpyright", specifier = ">=1.31.6" }] dev = [ - { name = "maturin", specifier = ">=1.9.4" }, + { name = "maturin", specifier = ">=1.9.4,<2.0" }, { name = "pyrefly", specifier = ">=1.1.1" }, { name = "pytest", specifier = ">=8.4.2" }, { name = "ruff", specifier = ">=0.15.0" }, From d49366511d7fa442f087e18e5ce13b8ff8ab1913 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 13:50:49 -0700 Subject: [PATCH 34/36] Add CFLAGS for s390x toolchain The psm library is failing to compile on the s390x architecture. We set the processor architecture to z10 as found in the discussion [here](https://github.com/rust-lang/stacker/issues/79). --- .github/workflows/release.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4b1d6f3..06d0054 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -40,6 +40,8 @@ jobs: target: s390x - runner: ubuntu-22.04 target: ppc64le + env: + CFLAGS_s390x_unknown_linux_gnu: "-march=z10" steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 From a416dab97a81269acb4d9a29d1b1e7beb88a7691 Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 13:58:33 -0700 Subject: [PATCH 35/36] Update Cargo lockfile --- Cargo.lock | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2b3fca0..89cb70b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,9 +44,9 @@ dependencies = [ [[package]] name = "banquo-core" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e92dc4ffd039baafab4f4ad500c9f4a7860d6111389fc0f2496662e43ab6e6b" +checksum = "65baba11da865dd1b9281d32dec9673d14bb3ccb8ccc8380f85b506c025a248a" dependencies = [ "ordered-float", "thiserror", @@ -54,9 +54,9 @@ dependencies = [ [[package]] name = "banquo-hybrid_distance" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c894832211466e2ef67a5325c3e57393473b566f7f4c525af6131921e7dc178d" +checksum = "4fadc1cd9f40bb630c48c70048562956f55bfc477d82425702e1acfeb361d902" dependencies = [ "banquo-core", "petgraph", @@ -127,9 +127,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "fixedbitset" -version = "0.4.2" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" [[package]] name = "foldhash" @@ -208,21 +208,23 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "ordered-float" -version = "4.6.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" dependencies = [ "num-traits", ] [[package]] name = "petgraph" -version = "0.6.5" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", + "hashbrown 0.15.5", "indexmap", + "serde", ] [[package]] From 0050f571c6efe6e77d083e6c99a93a998b69724b Mon Sep 17 00:00:00 2001 From: Quinn Thibeault Date: Fri, 17 Jul 2026 14:09:36 -0700 Subject: [PATCH 36/36] Update macos runners macos-13 runner for the x86_64 build is no longer supported, so we upgrade to macos 15 and ensure it is running on an intel machine. For the arm64 build we use the latest version of the macos runner, which will always be running on apple silicon. --- .github/workflows/release.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 06d0054..7441b21 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -126,9 +126,9 @@ jobs: strategy: matrix: platform: - - runner: macos-13 + - runner: macos-15-intel target: x86_64 - - runner: macos-14 + - runner: macos-latest target: aarch64 steps: - uses: actions/checkout@v4