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/release.yaml b/.github/workflows/release.yaml index 4b1d6f3..7441b21 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 @@ -124,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 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 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/ diff --git a/Cargo.lock b/Cargo.lock index a83e491..89cb70b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,31 +2,79 @@ # 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" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "banquo" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b11e7b8aa7cdb48ea55930c89ee67799e3bb20a857a0986eeb517d6993f2a94" +checksum = "bc93c7ffd87d66bc1c5518b2f6e743600bca909ba62953ba8403f612241b9858" dependencies = [ "banquo-core", + "banquo-parser", ] [[package]] name = "banquo-core" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f185816cb843688a7fdf342ae6b851fef34d73e99d7c054fe647911f955073c6" +checksum = "65baba11da865dd1b9281d32dec9673d14bb3ccb8ccc8380f85b506c025a248a" dependencies = [ "ordered-float", "thiserror", ] +[[package]] +name = "banquo-hybrid_distance" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fadc1cd9f40bb630c48c70048562956f55bfc477d82425702e1acfeb361d902" +dependencies = [ + "banquo-core", + "petgraph", + "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", +] + [[package]] name = "banquo-python" version = "0.0.0" @@ -35,6 +83,77 @@ dependencies = [ "pyo3", ] +[[package]] +name = "cc" +version = "1.2.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17dd265a7d0f31ef544e1b20e03add05d3b45b491b633b10d67145d2acc1a38" +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.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[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,25 +161,26 @@ 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" -version = "0.2.176" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "memoffset" -version = "0.9.1" +name = "memchr" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "num-traits" @@ -71,67 +191,95 @@ 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" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +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.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", + "serde", +] + [[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", ] +[[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 +287,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,31 +299,96 @@ 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", ] [[package]] name = "quote" -version = "1.0.41" +version = "1.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +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 = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +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" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -184,24 +397,24 @@ 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" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "thiserror" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.69" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -210,12 +423,27 @@ 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 = "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 16265c0..15b92be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,5 +9,5 @@ name = "_banquo_impl" crate-type = ["cdylib"] [dependencies] -banquo = { version = "0.1.0" } -pyo3 = { version = "0.26.0", features = ["extension-module"] } +banquo = { version = "0.2.0", features = ["parser"] } +pyo3 = { version = "0.29.0", features = ["extension-module"] } 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 diff --git a/banquo/__init__.py b/banquo/__init__.py index 4713e82..1009c15 100644 --- a/banquo/__init__.py +++ b/banquo/__init__.py @@ -1,10 +1,10 @@ from typing import Final -from .core import Formula, evaluate +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() @@ -16,4 +16,5 @@ "Top", "Trace", "evaluate", + "formula", ] diff --git a/banquo/_banquo_impl.pyi b/banquo/_banquo_impl.pyi index 1507f9b..b7dfba0 100644 --- a/banquo/_banquo_impl.pyi +++ b/banquo/_banquo_impl.pyi @@ -1,14 +1,15 @@ -from collections.abc import Iterable -from typing import Any, Generic, TypeVar +from collections.abc import Callable, Iterable +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): ... -T = TypeVar("T", covariant=True) +_T = TypeVar("_T", infer_variance=True) class Top(Any): # pyright: ignore[reportAny, reportExplicitAny] def __le__(self, other: object) -> bool: ... @@ -26,51 +27,58 @@ 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]): + 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]: ... - def at_time(self, time: float) -> T | 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]): ... + + parse: Callable[[str], _StlModule.Formula] + +stl: _StlModule diff --git a/banquo/core.py b/banquo/core.py index 212d0c2..841cb28 100644 --- a/banquo/core.py +++ b/banquo/core.py @@ -1,14 +1,15 @@ from __future__ import annotations -from typing import Protocol, TypeVar +from collections.abc import Callable +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 -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]): @@ -43,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]): @@ -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) 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 6fd0d3c..0d91187 100644 --- a/banquo/operators.py +++ b/banquo/operators.py @@ -1,36 +1,33 @@ 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 ._banquo_impl import PanicException from .core import ( - Formula, EnsureInput, - SupportsNeg, - SupportsLE, - SupportsGE, EnsureOutput, + Formula, + SupportsGT, + SupportsLT, + SupportsNeg, SupportsNegGE, ) from .trace import Trace 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=SupportsLE, covariant=True) -M_ge = typing.TypeVar("M_ge", bound=SupportsGE, 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: @@ -39,8 +36,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 +48,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]: @@ -133,11 +129,11 @@ 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_") -M_le_ = typing.TypeVar("M_le_", bound=SupportsLE, covariant=True) +S_ = TypeVar("S_", infer_variance=True) +M_le_ = TypeVar("M_le_", bound=SupportsLT, infer_variance=True) class Always(Operator[S, M_le]): @@ -177,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=SupportsGE, covariant=True) +M_ge_ = TypeVar("M_ge_", bound=SupportsGT, infer_variance=True) class Eventually(Operator[S, M_ge]): diff --git a/banquo/stl.py b/banquo/stl.py new file mode 100644 index 0000000..d28a057 --- /dev/null +++ b/banquo/stl.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from ._banquo_impl import stl as _stl +from .core import EnsureOutput + + +class Formula(EnsureOutput[dict[str, float], float]): + def __init__(self, formula: _stl.Formula): + super().__init__(formula) + + +def parse(phi: str) -> Formula: + return Formula(_stl.parse(phi)) diff --git a/banquo/trace.py b/banquo/trace.py index 6c3e842..1bebdf9 100644 --- a/banquo/trace.py +++ b/banquo/trace.py @@ -1,14 +1,13 @@ 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 -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: @@ -36,6 +35,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()) diff --git a/pyproject.toml b/pyproject.toml index 2946cc8..b5856ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,9 +33,18 @@ ci = [ "basedpyright>=1.31.6", ] dev = [ - "maturin>=1.9.4", - "pytest>=8.3.5", - "ruff>=0.14.0", + "maturin>=1.9.4,<2.0", + "ruff>=0.15.0", + { include-group = "test" }, + { include-group = "types" }, +] +test = [ + "pytest>=8.4.2", +] +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] @@ -48,10 +57,6 @@ preview = true line-length = 100 target-version = "py39" -[tool.pyright] -include = ["banquo"] -pythonVersion = "3.9" - [tool.pytest.ini_options] testpaths = [ "tests" @@ -60,3 +65,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", +] diff --git a/src/lib.rs b/src/lib.rs index afb36a5..d0d1f09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,405 +1,27 @@ mod metric; +mod operators; +mod stl; +mod traces; #[pyo3::pymodule] mod _banquo_impl { - use std::collections::HashMap; + #[pymodule_export] + use crate::metric::{PyBottom, PyTop}; - use pyo3::exceptions::{PyKeyError, PyRuntimeError, PyValueError}; - use pyo3::prelude::*; - use pyo3::types::PyDict; - use pyo3::{FromPyObject, IntoPyObject}; + #[pymodule_export] + use crate::traces::PyTrace; - use super::metric::PyMetric; - use banquo::operators::{ - Always, And, BinaryOperatorError, Eventually, ForwardOperatorError, Implies, Next, Not, Or, + #[pymodule_export] + use crate::operators::{ + PyAlways, PyAnd, PyEventually, PyImplies, PyNext, PyNot, PyOr, PyPredicate, }; - 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); + #[pyo3::pymodule] + mod stl { + #[pymodule_export] + use crate::stl::PyFormula; - impl PyNot { - fn evaluate_inner(&self, trace: &Trace>) -> PyResult> { - self.0.evaluate(trace) - } + #[pymodule_export] + use crate::stl::parse; } - - #[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::EmptyInterval => { - PyValueError::new_err("Bounds interval must not be empty.") - } - ForwardOperatorError::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::EmptyInterval => { - PyValueError::new_err("Bounds interval must not be empty.") - } - ForwardOperatorError::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; - - #[pymodule_export] - use crate::metric::PyTop; - - #[pymodule_export] - use crate::metric::PyBottom; } 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))) } } diff --git a/src/operators.rs b/src/operators.rs new file mode 100644 index 0000000..29892ee --- /dev/null +++ b/src/operators.rs @@ -0,0 +1,318 @@ +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) + } +} + +#[derive(FromPyObject)] +struct PyFormula(Py); + +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/stl.rs b/src/stl.rs new file mode 100644 index 0000000..6971cb4 --- /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; +use crate::traces::{PyMetricTrace, PyTrace}; + +#[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.py(), trace.borrow().as_ref()) + .map(PyMetricTrace::from) + } +} + +#[pyfunction] +pub fn parse(phi: &str) -> PyResult { + banquo::stl::parse(phi) + .map(PyFormula) + .map_err(|e| PyValueError::new_err(format!("Error parsing formula: {:?}", e))) +} diff --git a/src/traces.rs b/src/traces.rs new file mode 100644 index 0000000..dac4837 --- /dev/null +++ b/src/traces.rs @@ -0,0 +1,124 @@ +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 { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { + Self::new(obj.as_any()) + } +} + +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)) + } +} diff --git a/tests/test_operators.py b/tests/test_operators.py index 4950507..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 +from banquo import Bottom, Trace, formula, operators from banquo.core import Formula pytestmark = pytest.mark.unit @@ -33,22 +33,23 @@ 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]): @typing_extensions.override def evaluate(self, trace: Trace[T]) -> Trace[T]: + assert isinstance(trace, Trace) return trace @@ -90,10 +91,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.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") @@ -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}) @@ -151,10 +153,12 @@ 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( # pyrefly: ignore[bad-specialization] + Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]() + ) with pytest.raises(operators.MetricAttributeError): - _ = formula.evaluate(bad_trace) # pyright: ignore[reportUnknownVariableType] + _ = formula.evaluate(bad_trace) class TestDisjunction(BinaryTest): @@ -176,10 +180,12 @@ 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( # pyrefly: ignore[bad-specialization] + Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]() + ) with pytest.raises(operators.MetricAttributeError): - _ = formula.evaluate(bad_trace) # pyright: ignore[reportUnknownVariableType] + _ = formula.evaluate(bad_trace) class TestImplication(BinaryTest): @@ -202,10 +208,12 @@ 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( # pyrefly: ignore[bad-specialization] + Left[BadMetric, BadMetric](), Right[BadMetric, BadMetric]() + ) with pytest.raises(operators.MetricAttributeError): - _ = formula.evaluate(bad_trace) # pyright: ignore[reportUnknownVariableType] + _ = formula.evaluate(bad_trace) class TestNext(UnaryTest): @@ -271,10 +279,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): @@ -316,7 +324,32 @@ 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 +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 diff --git a/tests/test_stl.py b/tests/test_stl.py new file mode 100644 index 0000000..378128b --- /dev/null +++ b/tests/test_stl.py @@ -0,0 +1,128 @@ +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) diff --git a/uv.lock b/uv.lock index 9b379ff..15c1c80 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 = [ @@ -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" }, @@ -116,9 +116,17 @@ ci = [ ] dev = [ { name = "maturin" }, + { name = "pyrefly" }, { name = "pytest" }, { name = "ruff" }, ] +test = [ + { name = "pytest" }, +] +types = [ + { name = "pyrefly" }, + { name = "pytest" }, +] [package.metadata] requires-dist = [{ name = "typing-extensions", specifier = ">=4.13.2" }] @@ -126,9 +134,15 @@ 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 = "pytest", specifier = ">=8.3.5" }, - { name = "ruff", specifier = ">=0.14.0" }, + { 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" }, +] +test = [{ name = "pytest", specifier = ">=8.4.2" }] +types = [ + { name = "pyrefly", specifier = ">=1.1.1" }, + { name = "pytest", specifier = ">=8.4.2" }, ] [[package]] @@ -140,6 +154,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" @@ -160,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]]