Skip to content

Commit d05fe76

Browse files
hardbyteclaude
andauthored
Fix the silently failing Python security scan, add Dependabot, align type stubs with the runtime (#52)
* Fix the silently failing Python security scan, add Dependabot, align stubs with the runtime The Python dependency scan has exited 1 on every run since 0.8.0 switched it to `safety scan`: Safety 3 demands an interactive login ("Please login or register Safety CLI ... EOF when reading a line"), and the step's continue-on-error hid the failure, so no Python advisories have actually been checked. Replace it with `uv export` of the locked, hashed dependency set plus `uvx pip-audit --disable-pip`, which needs no account and audits exactly the pins in uv.lock. CodeQL moves to codeql-action v4 (v3 is deprecated December 2026), and a Dependabot config watches Cargo.lock, uv.lock and the Actions pins weekly with minor/patch updates grouped per ecosystem. The type stubs named parameters the runtime does not accept: `evaluate( expression, context)` where PyO3 exposes `(src, evaluation_context)`, and `add_function(name, func)` where the runtime has `function`. A keyword call that type-checked failed at runtime with TypeError and the working spelling failed to type-check. The stub now mirrors the runtime, and a new test parses cel.pyi and compares every declared signature with inspect.signature of the extension, so the two cannot drift again. Context.variables and Context.functions, which the class docstring has documented as attributes all along, now exist as read-only getters returning fresh dicts. Also removes cel.evaluation_modes (dead since the evaluation-mode feature was removed in 0.5.2) and docs/requirements.txt (Read the Docs and the README use the docs dependency group), fixes the README licence line and the mkdocs site_url, and adds Python version and Typing :: Typed classifiers. Claude-Session: https://claude.ai/code/session_019WbvXZFm8Nb2LXF2kiWoWW * Run the security scan when uv.lock changes Dependabot's uv updater usually touches only uv.lock when it bumps a version already allowed by pyproject.toml, so the push filter that lists pyproject.toml and the Cargo files would have skipped the audit of exactly the change it is meant to check, leaving it to the weekly schedule. Claude-Session: https://claude.ai/code/session_019WbvXZFm8Nb2LXF2kiWoWW --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 172b505 commit d05fe76

12 files changed

Lines changed: 357 additions & 44 deletions

File tree

‎.github/dependabot.yml‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Dependabot keeps the three dependency surfaces current: the Rust crates
2+
# (Cargo.lock), the Python lockfile (uv.lock) and the Actions the workflows pin.
3+
# Minor and patch updates are grouped into one PR per ecosystem so a weekly
4+
# refresh is one review rather than a dozen; majors stay separate because they
5+
# tend to need a changelog entry (cel-rust majors change CEL behaviour).
6+
version: 2
7+
updates:
8+
- package-ecosystem: cargo
9+
directory: /
10+
schedule:
11+
interval: weekly
12+
groups:
13+
rust-minor-and-patch:
14+
update-types: [minor, patch]
15+
16+
- package-ecosystem: uv
17+
directory: /
18+
schedule:
19+
interval: weekly
20+
groups:
21+
python-minor-and-patch:
22+
update-types: [minor, patch]
23+
24+
- package-ecosystem: github-actions
25+
directory: /
26+
schedule:
27+
interval: weekly
28+
groups:
29+
actions:
30+
patterns: ["*"]

‎.github/workflows/security.yml‎

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches: [main]
66
paths:
77
- '**/pyproject.toml'
8+
- '**/uv.lock'
89
- '**/Cargo.toml'
910
- '**/Cargo.lock'
1011

@@ -38,13 +39,16 @@ jobs:
3839
- name: Set up Python
3940
run: uv python install 3.12
4041

41-
- name: Install dependencies
42-
run: uv sync --dev
43-
4442
- name: Run Python security scan
45-
# `safety check` is retired in favour of `safety scan`; run it with uvx so
46-
# the scanner is not added to this project's dev dependencies.
47-
run: uvx safety scan --detailed-output
43+
# pip-audit checks the locked dependency set against the PyPI advisory
44+
# database and needs no account. (`safety scan` requires an interactive
45+
# login since Safety 3, so it exited 1 on every run and the
46+
# continue-on-error that hid that made the step a no-op.)
47+
# `uv export` writes the fully pinned, hashed set from uv.lock, and
48+
# `--disable-pip` audits exactly those pins without resolving anything.
49+
run: |
50+
uv export --all-groups --no-emit-project --format requirements-txt --output-file requirements-audit.txt
51+
uvx pip-audit --disable-pip --requirement requirements-audit.txt
4852
continue-on-error: true # Don't fail CI on security advisories, just report
4953

5054
codeql:
@@ -59,12 +63,12 @@ jobs:
5963
uses: actions/checkout@v7
6064

6165
- name: Initialize CodeQL
62-
uses: github/codeql-action/init@v3
66+
uses: github/codeql-action/init@v4
6367
with:
6468
languages: python
6569

6670
- name: Autobuild
67-
uses: github/codeql-action/autobuild@v3
71+
uses: github/codeql-action/autobuild@v4
6872

6973
- name: Perform CodeQL Analysis
70-
uses: github/codeql-action/analyze@v3
74+
uses: github/codeql-action/analyze@v4

‎CHANGELOG.md‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- `Context.variables` and `Context.functions` are now readable from Python, as the
13+
class docstring has promised since the beginning. Each returns a fresh dict:
14+
variables converted back to Python values (through the same conversion
15+
evaluation results use) and functions as the registered callables. Mutating the
16+
returned dict does not change the context.
17+
18+
### Fixed
19+
20+
- The type stubs (`cel.pyi`) now use the parameter names the runtime actually
21+
accepts: `evaluate(src, evaluation_context)` rather than `(expression, context)`,
22+
and `Context.add_function(name, function)` rather than `func`. A keyword call
23+
that type-checked, such as `evaluate(expression=...)`, failed at runtime with
24+
`TypeError`, and the working spelling failed to type-check. `Context(variables,
25+
functions)` also accepts `functions` positionally at runtime, which the stub now
26+
reflects.
27+
- The Python dependency scan in the security workflow had been failing on every
28+
run since the switch to `safety scan`, which requires an interactive login
29+
since Safety 3; `continue-on-error` hid that, so no Python advisories have been
30+
checked since 0.8.0. The step now exports the locked dependency set with
31+
`uv export` and audits it with `pip-audit`, which needs no account.
32+
33+
### Removed
34+
35+
- `cel.evaluation_modes.EvaluationMode`, left over from the evaluation-mode
36+
feature removed in 0.5.2 and referenced by nothing.
37+
- `docs/requirements.txt`, superseded by the `docs` dependency group that Read the
38+
Docs and the README both use.
39+
40+
### Updated
41+
42+
- Dependabot now watches the Cargo lockfile, `uv.lock` and the GitHub Actions
43+
pins weekly, grouping minor and patch bumps into one pull request per
44+
ecosystem.
45+
- CodeQL analysis uses `github/codeql-action` v4; v3 is deprecated in
46+
December 2026.
47+
- Package metadata: Python 3.11 to 3.14 and `Typing :: Typed` trove classifiers;
48+
the README's licence line names Apache-2.0 rather than deferring to the crate
49+
this package once wrapped; `mkdocs.yml` points at the Read the Docs URL that
50+
actually serves the documentation.
51+
1052
## [0.9.0] - 2026-09-09
1153

1254
Upgrades to cel-rust 0.14.5, which brings native `type()`, range-checked

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ Contributions are welcome! Please see our [documentation](https://python-common-
218218

219219
## License
220220

221-
This project is licensed under the same terms as the original cel-interpreter crate.
221+
This project is licensed under the [Apache License 2.0](LICENSE).
222222

223223
## Resources
224224

‎docs/requirements.txt‎

Lines changed: 0 additions & 5 deletions
This file was deleted.

‎mkdocs.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
site_name: Python CEL
22
site_description: Common Expression Language for Python - Fast, Safe, and Simple
3-
site_url: https://python-cel.readthedocs.io
3+
site_url: https://python-common-expression-language.readthedocs.io
44
repo_url: https://github.com/hardbyte/python-common-expression-language
55
repo_name: hardbyte/python-common-expression-language
66

‎pyproject.toml‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,22 @@ authors = [
99
]
1010
requires-python = ">=3.11"
1111
classifiers = [
12+
"Development Status :: 4 - Beta",
13+
"Intended Audience :: Developers",
1214
"License :: OSI Approved :: Apache Software License",
15+
"Operating System :: OS Independent",
1316
"Programming Language :: Rust",
17+
"Programming Language :: Python :: 3",
18+
"Programming Language :: Python :: 3 :: Only",
19+
"Programming Language :: Python :: 3.11",
20+
"Programming Language :: Python :: 3.12",
21+
"Programming Language :: Python :: 3.13",
22+
"Programming Language :: Python :: 3.14",
1423
"Programming Language :: Python :: Implementation :: CPython",
1524
"Programming Language :: Python :: Implementation :: PyPy",
25+
"Topic :: Software Development :: Interpreters",
26+
"Topic :: Software Development :: Libraries :: Python Modules",
27+
"Typing :: Typed",
1628
]
1729
dynamic = ["version"]
1830
dependencies = [

‎python/cel/cel.pyi‎

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,46 @@
11
"""
22
Type stubs for the CEL Rust extension module.
3+
4+
Parameter names match the runtime signatures exposed by PyO3 (see
5+
``cel.evaluate.__text_signature__``), so keyword calls that type-check also
6+
run, and vice versa.
37
"""
48

5-
from typing import Any, Callable, Dict, Literal, Optional, Union, overload
9+
from typing import Any, Callable, Dict, Optional, Union
610

711
class Context:
812
"""CEL evaluation context for variables and functions."""
913

10-
@overload
11-
def __init__(self) -> None: ...
12-
@overload
13-
def __init__(self, variables: Dict[str, Any]) -> None: ...
14-
@overload
1514
def __init__(
1615
self,
1716
variables: Optional[Dict[str, Any]] = None,
18-
*,
1917
functions: Optional[Dict[str, Callable[..., Any]]] = None,
2018
) -> None: ...
19+
@property
20+
def variables(self) -> Dict[str, Any]:
21+
"""The registered variables, as a new dict of Python values.
22+
23+
Values come back through the same CEL-to-Python conversion evaluation
24+
results use, so a variable added as a tuple reads back as a list.
25+
Mutating the returned dict does not change the context; use
26+
``add_variable()`` or ``update()``.
27+
"""
28+
...
29+
30+
@property
31+
def functions(self) -> Dict[str, Callable[..., Any]]:
32+
"""The registered functions, as a new dict of name to callable.
33+
34+
Mutating the returned dict does not change the context; use
35+
``add_function()`` or ``update()``.
36+
"""
37+
...
38+
2139
def add_variable(self, name: str, value: Any) -> None:
2240
"""Add a variable to the context."""
2341
...
2442

25-
def add_function(self, name: str, func: Callable[..., Any]) -> None:
43+
def add_function(self, name: str, function: Callable[..., Any]) -> None:
2644
"""Add a function to the context."""
2745
...
2846

@@ -35,7 +53,7 @@ class Context:
3553
...
3654

3755
def update(self, variables: Dict[str, Any]) -> None:
38-
"""Update context with variables from a dictionary."""
56+
"""Update context with variables (and callables, as functions) from a dictionary."""
3957
...
4058

4159
class Program:
@@ -88,15 +106,15 @@ class OptionalValue:
88106
def or_optional(self, other: OptionalValue) -> OptionalValue: ...
89107

90108
def evaluate(
91-
expression: str,
92-
context: Optional[Union[Dict[str, Any], Context]] = None,
109+
src: str,
110+
evaluation_context: Optional[Union[Dict[str, Any], Context]] = None,
93111
) -> Any:
94112
"""
95113
Evaluate a CEL expression.
96114
97115
Args:
98-
expression: The CEL expression to evaluate
99-
context: Optional context with variables and functions
116+
src: The CEL expression to evaluate
117+
evaluation_context: Optional context with variables and functions
100118
101119
Returns:
102120
The result of evaluating the expression

‎python/cel/evaluation_modes.py‎

Lines changed: 0 additions & 11 deletions
This file was deleted.

‎src/context.rs‎

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use ::cel::Value;
33
use pyo3::exceptions::PyValueError;
44
use pyo3::prelude::*;
55
use pyo3::types::PyDict;
6+
use pyo3::IntoPyObjectExt;
67
use std::collections::HashMap;
78

89
#[pyo3::pyclass]
@@ -27,10 +28,13 @@ use std::collections::HashMap;
2728
/// - Optimize performance for applications with frequent CEL evaluations
2829
///
2930
/// Attributes:
30-
/// variables (dict): A dictionary mapping variable names (str) to their
31-
/// values (automatically converted to appropriate CEL types).
32-
/// functions (dict): A dictionary mapping function names (str) to their
33-
/// corresponding Python callable objects.
31+
/// variables (dict): A read-only snapshot mapping variable names (str) to
32+
/// their values, converted back from CEL types to Python (so a tuple
33+
/// added as a variable reads back as a list). Modify the context with
34+
/// ``add_variable()`` or ``update()``, not by mutating this dict.
35+
/// functions (dict): A read-only snapshot mapping function names (str) to
36+
/// the registered Python callables. Modify the context with
37+
/// ``add_function()`` or ``update()``.
3438
///
3539
/// Thread Safety:
3640
/// Context objects are not thread-safe. Create separate Context instances
@@ -207,6 +211,36 @@ impl Context {
207211
self.functions.insert(name, function);
208212
}
209213

214+
/// The registered variables, converted back to Python values.
215+
///
216+
/// Returns a new dict on every access; mutating it does not affect the
217+
/// context. Values go through the same conversion as evaluation results,
218+
/// so CEL-only distinctions are lost (a `uint` reads back as `int`).
219+
#[getter]
220+
fn variables<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
221+
let dict = PyDict::new(py);
222+
for (name, value) in &self.variables {
223+
dict.set_item(
224+
name,
225+
crate::RustyCelType(value.clone()).into_bound_py_any(py)?,
226+
)?;
227+
}
228+
Ok(dict)
229+
}
230+
231+
/// The registered functions, by name.
232+
///
233+
/// Returns a new dict on every access; mutating it does not affect the
234+
/// context.
235+
#[getter]
236+
fn functions<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyDict>> {
237+
let dict = PyDict::new(py);
238+
for (name, function) in &self.functions {
239+
dict.set_item(name, function.bind(py))?;
240+
}
241+
Ok(dict)
242+
}
243+
210244
/// Registers a Python callable for lazy variable resolution.
211245
///
212246
/// When evaluating an expression, CEL will call `resolver(name)` for each

0 commit comments

Comments
 (0)