Skip to content

Latest commit

 

History

History
500 lines (394 loc) · 25.1 KB

CHANGELOG.md

File metadata and controls

500 lines (394 loc) · 25.1 KB

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[Unreleased]

Added

  • Free-threaded Python. The test suite runs on the free-threaded build of Python 3.14 (python3.14t), and free-threaded wheels ship for Linux, macOS and Windows x64. Importing cel leaves the GIL disabled (#45).

Performance

  • Parsing no longer holds the GIL. compile(), and the parse inside evaluate(), release the GIL for expressions of 32 bytes or more, so threads that parse concurrently run in parallel: about 3× the throughput from 4 threads on a 4-core machine, with single-threaded cost unchanged. Executing a compiled program still holds the GIL (#45).

Changed

  • Program and OptionalValue are immutable; attributes can no longer be assigned on them.
  • One Context may be evaluated against from many threads at once, and evaluations see a consistent snapshot. An evaluation that starts during add_variable, add_function or update waits for it. Mutating a Context from two threads at once raises RuntimeError on a free-threaded interpreter; build a context before sharing it, or guard mutation with a lock.

Fixed

  • On a free-threaded interpreter, evaluating against a Context while another thread was mutating it raised ValueError: evaluation_context must be a Context object or a dict. It now waits for the mutation to finish.
  • Loading the extension module a second time in one process (from a sub-interpreter, for instance) no longer panics.

Updated

  • Building from the sdist requires maturin 1.14 or later.

[0.10.0] - 2026-09-15

Fixes import cel in a clean install, which has been broken since 0.6.0 for environments without typing_extensions; makes evaluation against a cel.Context cost the same regardless of how many variables and functions it carries; and repairs the Python security scan that had been silently failing since 0.8.0.

Added

  • Context.variables and Context.functions are now readable from Python, as the class docstring has promised since the beginning. Each returns a fresh dict: variables converted back to Python values (through the same conversion evaluation results use) and functions as the registered callables. Mutating the returned dict does not change the context.

Performance

  • A cel.Context now builds its CEL-side environment once and reuses it. Previously every evaluate() and Program.execute() call re-converted each variable and re-wrapped each registered Python function into a fresh cel-rust context, so the per-call cost grew with the size of the context: executing a pre-compiled 1 + 2 against a Context carrying the 47 extended-stdlib functions (what the cel CLI sets up) took about 9 µs, against about 0.2 µs with no context. The environment is now cached on the Context and shared by every evaluation until add_variable, add_function or update changes it, bringing that case down to about 0.2 µs as well. set_variable_resolver does not invalidate the cache: the resolver is bound per call in a child scope (cel-rust's Context::new_inner_scope), with the same lookup order as before (resolver, then registered variables). Dict contexts are still materialised on every call, since a dict can change between calls without notice; for hot loops, prefer a Context.
  • Behaviour that is unchanged and now pinned by tests: every mutator is visible on the next evaluation; a Python function may modify, or evaluate against, the Context it was registered on while an evaluation is in progress (the change applies from the next evaluation); and one Context can be shared between threads.

Changed

  • The ValueError raised when the interpreter panics during evaluation now reads "Internal evaluation error" rather than "Internal parser error"; parse failures were never what it reported.

Fixed

  • import cel failed in a clean install. cel.cli imported Annotated from typing_extensions, which is not one of this package's declared dependencies. It used to arrive transitively through Typer, but Typer 0.21.2 (February 2026) dropped that dependency, so pip install common-expression-language into an environment with nothing else in it left import cel raising ModuleNotFoundError. The development lockfile masked this because mypy still pulls typing_extensions in. Annotated now comes from the standard library (typing), which has provided it since Python 3.9. CI now installs the built wheel into an empty virtual environment and imports the package and runs the cel command, so an undeclared runtime dependency fails the build.
  • The type stubs (cel.pyi) now use the parameter names the runtime actually accepts: evaluate(src, evaluation_context) rather than (expression, context), and Context.add_function(name, function) rather than func. A keyword call that type-checked, such as evaluate(expression=...), failed at runtime with TypeError, and the working spelling failed to type-check. Context(variables, functions) also accepts functions positionally at runtime, which the stub now reflects.
  • The Python dependency scan in the security workflow had been failing on every run since the switch to safety scan, which requires an interactive login since Safety 3; continue-on-error hid that, so no Python advisories have been checked since 0.8.0. The step now exports the locked dependency set with uv export and audits it with pip-audit, which needs no account.

Removed

  • cel.evaluation_modes.EvaluationMode, left over from the evaluation-mode feature removed in 0.5.2 and referenced by nothing.
  • docs/requirements.txt, superseded by the docs dependency group that Read the Docs and the README both use.

Updated

  • Dependabot now watches the Cargo lockfile, uv.lock and the GitHub Actions pins weekly, grouping minor and patch bumps into one pull request per ecosystem.
  • CodeQL analysis uses github/codeql-action v4; v3 is deprecated in December 2026.
  • Package metadata: Python 3.11 to 3.14 and Typing :: Typed trove classifiers; the README's licence line names Apache-2.0 rather than deferring to the crate this package once wrapped; mkdocs.yml points at the Read the Docs URL that actually serves the documentation.

Documented

  • Naive datetime values are interpreted in the host's local time zone when converted to a CEL timestamp, so the same value can mean a different instant on different machines (#50). The Python API reference now carries a warning, and the quick start, tutorials and Context.add_variable docstring use timezone-aware datetime.now(timezone.utc) instead of naive datetime.now().

[0.9.0] - 2026-09-09

Upgrades to cel-rust 0.14.5, which brings native type(), range-checked int()/uint() conversions and, notably, rejection of CEL reserved words as identifiers. Also the first release published through PyPI trusted publishing, with the GitHub release created automatically from this section.

Changed

  • CEL reserved words are now rejected as identifiers. cel-rust 0.14.4 enforces the specification's reserved word list (as, break, const, continue, else, for, function, if, import, let, loop, package, namespace, return, var, void, while), so an expression such as var == "x" now raises ValueError at parse time even if the context defines var. Names that merely contain a reserved word (var_2) and map keys ({"var": 1}["var"]) are unaffected. Rename such variables before upgrading.
  • type() is now provided natively by cel-rust 0.14.5 and returns a first-class CEL type value, so type(x) == int, type(1u) == uint and type(null) == null_type work inside expressions. Returned to Python, a type value arrives as its name. The string-returning type() shim and the identity dyn() shim have been removed from cel.stdlib's core library because the native functions take precedence anyway; the only visible differences are that uint is now reported as "uint" rather than "int" and null as "null_type" rather than "null".
  • int() and uint() reject conversions that do not fit the target type (for example int(9223372036854775808u), uint(-1), int(1e300) and int(double("NaN"))), raising OverflowError instead of silently wrapping or saturating.
  • duration and timestamp values report their spec type names from type(): google.protobuf.Duration and google.protobuf.Timestamp.

Fixed

  • Negative hexadecimal literals (-0x10) parse; stacked unary operators (--1, !!true) cancel; \u escapes inside bytes literals are rejected per the spec; and optional.of(1) == optional.of(1) compares by content (all from cel-rust 0.14.4/0.14.5).

Updated

  • cel-rust 0.14.3 to 0.14.5 (the minimum is now 0.14.5), plus in-range transitive bumps (log 0.4.34, uuid 1.26.0, smallvec 1.16.0, cc 1.4.5, syn 3.0.5).
  • Python dev dependencies moved from the deprecated tool.uv.dev-dependencies to dependency-groups.dev; uv.lock refreshed (ruff 0.16.6, typer 0.27.2, maturin 1.15.0).
  • GitHub Actions: actions/checkout v7, actions/setup-python v7, actions/upload-artifact v7, actions/download-artifact v8 (digest mismatches now fail the job), astral-sh/setup-uv v10 (cache disabled on pull requests from forks and on Dependabot rollups, to guard against cache poisoning).

Updated

  • Releases publish to PyPI with trusted publishing instead of a stored PYPI_API_TOKEN: the release job exchanges a short-lived GitHub OIDC token for the upload credential, so no long-lived PyPI token is kept in repository secrets. The job runs in a pypi environment, which the trusted publisher on PyPI is configured against.
  • The release job now also creates the GitHub release for the pushed tag, using the matching CHANGELOG.md section as the release notes and attaching the wheels and sdist. Pushing a tag previously published to PyPI but left no GitHub release behind.

[0.8.0] - 2026-08-19

Adds the sum aggregation to the extended standard library, refreshes the locked Rust dependencies, and makes CI reproducible across Python 3.11 to 3.14.

Added

  • cel.stdlib's core library gains sum, completing the aggregation trio (min, max, sum) requested in #14. It follows Kubernetes' CEL list library: int, uint, double and duration are supported; sum([]) is 0; booleans are rejected rather than counted as 1/0; and numbers cannot be mixed with durations. Like the rest of the extended stdlib it works in both call forms — sum([1, 2, 3]) and items.map(i, i.weight).sum().
  • fold/reduce remain unavailable, and the reason is now documented in cel.stdlib and the standard-library reference: a CEL function receives evaluated arguments, whereas a fold needs its accumulator expression left unevaluated, and cel-rust expands its comprehension macros from a fixed table in the parser. They have to arrive in cel-rust rather than in this wrapper.

Documented

  • The two lossy conversions at the Python callback boundary, which apply to every cel.stdlib function rather than just the aggregations: a uint result returns as an int (Python has a single integer type), so sum([1u, 2u]) + 1u has no overload where the native [1u, 2u][0] + 1u works; and duration is carried by datetime.timedelta, whose resolution is microseconds, so sub-microsecond durations are rounded before a function sees them. Both behaviours are pinned by tests.

Updated

  • Refreshed the locked Rust dependencies within their existing ranges: cel 0.14.0 to 0.14.3, PyO3 0.29.0 to 0.29.2, plus transitive bumps (regex 1.13.1, serde 1.0.229, serde_json 1.0.151, thiserror 2.0.20, uuid 1.24.1).
  • uv.lock is now committed so CI resolves the same Python dependencies as local development; refresh it with uv lock --upgrade.
  • CI tests Python 3.13 and 3.14 in addition to 3.11 and 3.12.
  • Pinned ruff to the 0.16 line and excluded Markdown from ruff format, so formatter releases no longer change what CI expects of the docs.
  • The security workflow runs safety scan via uvx (the retired safety check command no longer works, and the scanner is no longer added to this project's dev dependencies), and the Claude workflow uses anthropics/claude-code-action@v1 instead of the deprecated @beta tag.

[0.7.0] - 2026-07-04

Upgrades to cel-rust 0.14, ships an extended standard library that mirrors cel-go, and exposes expression static analysis. A 1.0 release is intentionally deferred until the upstream cel crate supports the CEL protobuf AST (cel.expr.Expr / CheckedExpr) so that cross-implementation portability can be offered — see the Notes section below.

Updated

  • Updated cel-rust from 0.13.0 to 0.14.0.
  • Updated PyO3 from 0.27 to 0.29. This resolves two upstream advisories that affected earlier PyO3 versions: an out-of-bounds read in nth/nth_back for PyList/PyTuple iterators (high), and a missing Sync bound on PyCFunction::new_closure closures (moderate).
  • Switched pyo3-log from a git fork (pinned to a PyO3 0.27 branch) to the released pyo3-log 0.13.4 crate, removing the git dependency.
  • Refreshed transitive dependencies (chrono 0.4.45, log 0.4.33, regex 1.12.4, serde_json 1.0.150, arc-swap 1.9.2).

Added

  • Extended standard library (cel.stdlib): opt-in function libraries that mirror cel-go's extensions and fill gaps in cel-rust's built-ins. Register them with cel.stdlib.add_stdlib_to_context(context) (all libraries) or pass extensions=[...] to select individual ones. The CLI enables them automatically. Libraries:
    • core: bool, dyn, type, min, max
    • strings: charAt, indexOf, lastIndexOf, substring, replace, split, join, lowerAscii, upperAscii, trim, reverse, strings.quote
    • math: math.greatest, math.least, math.abs, math.sign, math.ceil, math.floor, math.round, math.trunc, math.isNaN, math.isInf, math.isFinite, math.sqrt, and the math.bit* operations
    • sets: sets.contains, sets.equivalent, sets.intersects
    • encoders: base64.encode, base64.decode
    • lists: contains, distinct, flatten, slice, sort, reverse, first, last, lists.range
  • Expression static analysis on Program: variables(), functions() and references() report the identifiers an expression uses without evaluating it. Program.source exposes the original CEL text.
  • Method-call syntax for custom functions: a registered function can now be called either as f(x, y) or as x.f(y). When called as a method the receiver is passed as the first argument, matching CEL's semantics. This is what lets the standard-library extensions be written as members ("s".charAt(i), [1,2].contains(x)).

Changed

  • Behaviour change (cel 0.14): contains is now a string-only built-in. [1, 2, 3].contains(2) and {...}.contains(k) no longer resolve to a built-in — use the in operator (2 in [1, 2, 3], "k" in m) for list/map membership, or enable the lists extension in cel.stdlib which restores a multi-type contains.
  • Behaviour change (cel 0.14): min and max are no longer part of the core standard library. They are provided by the core extension in cel.stdlib.
  • Behaviour change (cel 0.14): built-in functions and overloads now take precedence over user-registered functions of the same name (e.g. registering a function called double will not override the built-in double() conversion). Give custom functions names that do not collide with built-ins.
  • Error messages: type-mismatch errors now report canonical CEL type names (e.g. Unsupported operation: string + int) and several additional execution errors map to idiomatic Python exceptions (UnsupportedIndex, ValuesNotComparable, UnexpectedType, InvalidArgumentCount → TypeError).

Performance

  • The CEL standard-library environment is now built once and shared across all evaluations (via cel 0.14's Env/Context::with_env) instead of being rebuilt on every evaluate()/Program.execute() call.

Notes

  • Cross-implementation portability / "bytecode" is explicitly out of scope. CEL has no bytecode format; portable interchange in the ecosystem uses the protobuf AST (cel.expr.Expr / CheckedExpr), which the upstream cel Rust crate cannot yet produce or consume (it has no protobuf support and no type checker). The portable artifact for this library is the CEL source string; Program.references() provides static analysis.

[0.6.0] - 2026-05-12

Updated

  • Updated cel-rust from 0.12.0 to 0.13.0.

Added

  • Context.set_variable_resolver(callback) exposes cel 0.13's VariableResolver trait to Python. The callback receives a variable name and returns the value (or None to fall through to variables registered with add_variable). Useful for backing a CEL context with on-demand sources (database lookups, lazily-loaded config files, etc.) without materializing the full set of variables upfront. Exceptions raised by the resolver are logged and treated as "not handled".
  • Idiomatic Python exception mapping for several CEL runtime errors that previously fell through to ValueError:
    • Arithmetic overflow → OverflowError (e.g. 9223372036854775807 + 1, including the new overflow-safe int math in cel 0.13).
    • Division by zero / modulo by zero → ZeroDivisionError.
    • List index out of bounds → IndexError.
    • Missing map key (e.g. {"a": 1}.b) → KeyError.

Changed

  • Behaviour change (cel 0.13): bytes concatenation with + now works per the CEL spec (b'hello' + b'world' returns b'helloworld'). Previously raised TypeError.
  • Behaviour change (cel 0.13): logical && and || are now "err-resilient" per CEL spec — X && false short-circuits to false and X || true short-circuits to true even when X is not a boolean. Conversely, false || X and true && X now raise TypeError when X is not boolean (previously false || X returned X).
  • Error mapping: more operations now route through CEL's NoSuchOverload (e.g. 1 + 2u, 1 * 2u, indexing into a string). These map to TypeError with a generic message listing common causes and conversion functions (int(x), uint(x), double(x)). The previous type-specific messages (e.g. "Cannot mix signed and unsigned integers") are still produced for the operand orderings cel-rust dispatches via UnsupportedBinaryOperator. Tests asserting on specific message text may need updating.
  • Behaviour change (cel 0.13): no implicit type coercion on map index access; indexing into a string now raises TypeError (NoSuchOverload) per CEL spec.
  • Behaviour change (cel 0.13): integer arithmetic overflow now raises OverflowError instead of silently wrapping. Affects +, -, * on both int and uint at the type's bounds.

Performance

  • Microbenchmarks comparing cel 0.13 vs 0.12 (release build, taking the min of 3 runs per scenario):

    Scenario compile compiled execute evaluate
    x + y * 2 +2.8% +8.4% +3.3%
    greet + ' ' + name +23.0% +4.3% +0.5%
    size(items) (1k) ~0% +63.4% +53.2%
    map field access −7.1% +8.3% +6.5%
    custom Python fn +1.4% +6.7% −1.9%

    Most scenarios are within ~10% of 0.12; the size(items) regression on a 1000-element list is cel 0.13's dyn-Val refactor adding per-element boxing overhead at the Python→CEL boundary. Smaller lists are not noticeably affected.

[0.5.6] - 2026-02-07

Fixed

  • Mapping conversion now respects dict subclasses and custom Mapping implementations, preserving __getitem__ behavior for member access (issue #22)

[0.5.5] - 2026-02-07

Added

  • compile() API and Program.execute() for reuse and performance
  • OptionalValue wrapper to expose CEL optional values in Python
  • Compile vs execute benchmark script (examples/performance/compile_execute_benchmark.py)

Updated

  • Updated cel-rust from 0.11.6 to 0.12.0

Changed

  • Documentation: pre-compilation guidance and OptionalValue examples

[0.5.4] - 2025-10-23

Updated

  • Updated cel-rust from 0.11.4 to 0.11.6
  • Updated PyO3 from 0.25.1 to 0.27.1

Changed

  • Reduced logging verbosity

[0.5.3] - 2025-10-14

  • Added new cel.stdlib module with Python implementations of CEL functions missing from upstream cel-rust.
  • CLI automatically includes all stdlib functions
  • Minor removed warning level logging in cel crate

[0.5.2] - 2025-09-12

🚨 Breaking Changes

  • Python evaluation mode removed: The library now operates exclusively in strict CEL mode
    • Removed: EvaluationMode.PYTHON and all automatic integer-to-float promotion
    • Removed: mode parameter from evaluate() function
    • Removed: --mode CLI option
    • Behavior change: Mixed arithmetic like 1 + 2.5 now raises TypeError instead of automatically promoting to 3.5
    • Migration: Use explicit type conversion (e.g., double(1) + 2.5) for mixed arithmetic
    • Rationale: Eliminates complex AST preprocessing that was breaking has() short-circuiting and other CEL functions

🐛 Fixed

  • CEL function short-circuiting: Fixed issue where has() and other CEL functions failed due to AST preprocessing interference
  • String literal corruption: Eliminated string literal modification that occurred during integer promotion preprocessing

Updated

  • Updated cel crate from v0.11.0 to v0.11.1
  • Updated documentation to reflect strict CEL mode operation
  • Updated tests to work with strict CEL mode only
  • Removed complex preprocessing logic

[0.5.1] - 2025-08-11

✨ Added

  • EvaluationMode enum: Control type handling behavior in CEL expressions (deprecated and removed in later version)
    • EvaluationMode.PYTHON (default for Python API): Python-friendly type promotions (removed)
    • EvaluationMode.STRICT (default for CLI): Strict CEL type rules with no coercion (now the only mode)
  • Type checking support: Added complete type stub files (.pyi) for PyO3 extension

[0.5.0] - 2025-08-08

🚨 Breaking Changes (Rust API only)

  • Upgraded cel crate (formerly cel-interpreter) 0.10.0 → 0.11.0:
    • Function registration now uses IntoFunction trait.
    • Python integration updated to use Arguments extractor for variadic args.
    • Imports renamed from cel_interpreter:: to cel::.
    • No changes to Python API – all existing code continues to work.

✨ Changed

  • Internal: Refactored Python integration to match new CEL API.
  • Updated dependencies:
    • pyo3: 0.25.0 → 0.25.1
    • pyo3-log: 0.12.1 → 0.12.4

🗒 Maintainer Notes

  • Prepared for upcoming CEL Rust features:
    • Enhanced type system & introspection
    • type() function support
    • Optional value handling

[0.4.1] - 2025-08-02

✨ Added

  • Automatic type coercion for mixed int/float arithmetic (deprecated and removed in later version):
    • Float literals automatically promote integer literals to floats.
    • Context variables containing floats trigger int → float promotion.
    • Preserves array indexing with integers (e.g., list[2] stays integer).
  • Enhanced error handling:
    • Parser panics now caught with std::panic::catch_unwind.
    • Invalid expressions return a ValueError instead of crashing Python.

🐛 Fixed

  • Mixed-type arithmetic now works in expressions like:
    • 3.14 * 2
    • 2 + 3.14
    • value * 2 (where value is float)
  • Parser panics from cel-interpreter handled gracefully with proper error messages.
  • Updated to latest PyO3 APIs to remove deprecation warnings.

⚠ Known Issues

  • Bytes concatenation (b'hello' + b'world') unsupported in cel-interpreter 0.10.0.
    • Spec requires: should work.
    • Current: returns "Unsupported binary operator 'add'".
    • Workaround: bytes(string(part1) + string(part2)).
    • Status: Missing feature in cel-interpreter, not in our wrapper.

📦 Dependencies

  • cel-interpreter: 0.9.0 → 0.10.0 (breaking changes internally)
  • log: 0.4.22 → 0.4.27
  • chrono: 0.4.38 → 0.4.41
  • pyo3: 0.22.6 → 0.25.0 (major API upgrade to IntoPyObject)
  • pyo3-log: 0.11.0 → 0.12.1 (compatible with PyO3 0.25.0)

🗒 Maintainer Notes

  • PyO3 migration: moved from deprecated IntoPy to new IntoPyObject API.
  • New conversion system improves error handling and type safety.
  • All 120 tests pass on current dependency set.