reeval-python 0.10.0__tar.gz → 0.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reeval_python-0.10.0 → reeval_python-0.14.0}/.gitignore +2 -0
- reeval_python-0.14.0/PKG-INFO +169 -0
- reeval_python-0.14.0/README.md +145 -0
- {reeval_python-0.10.0 → reeval_python-0.14.0}/pyproject.toml +13 -3
- reeval_python-0.14.0/src/reeval_python/__init__.py +57 -0
- reeval_python-0.14.0/src/reeval_python/__main__.py +9 -0
- reeval_python-0.14.0/src/reeval_python/_wire.py +1867 -0
- reeval_python-0.14.0/src/reeval_python/aio.py +22 -0
- reeval_python-0.14.0/src/reeval_python/binaries.json +55 -0
- reeval_python-0.14.0/src/reeval_python/checks.py +277 -0
- reeval_python-0.14.0/src/reeval_python/cli.py +505 -0
- reeval_python-0.14.0/src/reeval_python/context.py +395 -0
- reeval_python-0.14.0/src/reeval_python/declare.py +410 -0
- reeval_python-0.14.0/src/reeval_python/environment.py +187 -0
- reeval_python-0.14.0/src/reeval_python/errors.py +26 -0
- reeval_python-0.14.0/src/reeval_python/expect.py +285 -0
- reeval_python-0.14.0/src/reeval_python/options.py +54 -0
- reeval_python-0.14.0/src/reeval_python/runner.py +543 -0
- reeval_python-0.14.0/src/reeval_python/types.py +121 -0
- reeval_python-0.14.0/src/reeval_python/wire.py +174 -0
- reeval_python-0.14.0/tests/context/abort.eval.py +6 -0
- reeval_python-0.14.0/tests/context/abort.sync.eval.py +6 -0
- reeval_python-0.14.0/tests/context/exec-retries.eval.py +7 -0
- reeval_python-0.14.0/tests/context/exec-retries.sync.eval.py +7 -0
- reeval_python-0.14.0/tests/context/exec.eval.py +10 -0
- reeval_python-0.14.0/tests/context/exec.sync.eval.py +10 -0
- reeval_python-0.14.0/tests/context/expect-fail.eval.py +12 -0
- reeval_python-0.14.0/tests/context/expect-fail.sync.eval.py +11 -0
- reeval_python-0.14.0/tests/context/expect.eval.py +16 -0
- reeval_python-0.14.0/tests/context/expect.sync.eval.py +14 -0
- reeval_python-0.14.0/tests/context/files.eval.py +16 -0
- reeval_python-0.14.0/tests/context/files.sync.eval.py +16 -0
- reeval_python-0.14.0/tests/context/judge.eval.py +10 -0
- reeval_python-0.14.0/tests/context/judge.sync.eval.py +10 -0
- reeval_python-0.14.0/tests/context/record.async.eval.py +14 -0
- reeval_python-0.14.0/tests/context/record.eval.py +14 -0
- reeval_python-0.14.0/tests/context/send.eval.py +8 -0
- reeval_python-0.14.0/tests/context/send.sync.eval.py +8 -0
- reeval_python-0.14.0/tests/context/sync-async.async.eval.py +7 -0
- reeval_python-0.14.0/tests/context/sync-async.eval.py +6 -0
- reeval_python-0.14.0/tests/declare/compare-questions.eval.py +43 -0
- reeval_python-0.14.0/tests/declare/compare-variants.eval.py +19 -0
- reeval_python-0.14.0/tests/declare/each-families.eval.py +25 -0
- reeval_python-0.14.0/tests/declare/egress-cascade.eval.py +39 -0
- reeval_python-0.14.0/tests/declare/environment-extend.eval.py +13 -0
- reeval_python-0.14.0/tests/declare/environment-plugin-marketplace.eval.py +19 -0
- reeval_python-0.14.0/tests/declare/environment-plugins.eval.py +35 -0
- reeval_python-0.14.0/tests/declare/groups-skip-todo.eval.py +16 -0
- reeval_python-0.14.0/tests/declare/questions-attempts.eval.py +39 -0
- reeval_python-0.14.0/tests/declare/questions-group.eval.py +47 -0
- reeval_python-0.14.0/tests/declare/questions-state.eval.py +66 -0
- reeval_python-0.14.0/tests/declare/tags.eval.py +20 -0
- reeval_python-0.14.0/tests/runner_process.py +90 -0
- reeval_python-0.14.0/tests/stdio_host.py +171 -0
- reeval_python-0.14.0/tests/test_api.py +254 -0
- reeval_python-0.14.0/tests/test_checks.py +293 -0
- reeval_python-0.14.0/tests/test_cli.py +626 -0
- reeval_python-0.14.0/tests/test_conformance_context.py +70 -0
- reeval_python-0.14.0/tests/test_conformance_declare.py +46 -0
- reeval_python-0.14.0/tests/test_patterns.py +148 -0
- reeval_python-0.14.0/tests/test_python_wheels.py +277 -0
- reeval_python-0.14.0/tests/test_runner_attempts.py +121 -0
- reeval_python-0.14.0/tests/test_runner_checks.py +287 -0
- reeval_python-0.14.0/tests/test_runner_isolation.py +368 -0
- reeval_python-0.14.0/tests/test_runner_misuse.py +199 -0
- reeval_python-0.14.0/tests/test_runner_sdk.py +198 -0
- reeval_python-0.14.0/tests/test_runner_stdio.py +102 -0
- reeval_python-0.14.0/tests/test_types.py +130 -0
- {reeval_python-0.10.0 → reeval_python-0.14.0}/uv.lock +1 -1
- reeval_python-0.10.0/PKG-INFO +0 -85
- reeval_python-0.10.0/README.md +0 -62
- reeval_python-0.10.0/src/reeval_python/__init__.py +0 -23
- reeval_python-0.10.0/src/reeval_python/__main__.py +0 -4
- reeval_python-0.10.0/src/reeval_python/assertion.py +0 -168
- reeval_python-0.10.0/src/reeval_python/context.py +0 -163
- reeval_python-0.10.0/src/reeval_python/declare.py +0 -263
- reeval_python-0.10.0/src/reeval_python/errors.py +0 -7
- reeval_python-0.10.0/src/reeval_python/expect.py +0 -95
- reeval_python-0.10.0/src/reeval_python/runner.py +0 -190
- reeval_python-0.10.0/src/reeval_python/types.py +0 -97
- reeval_python-0.10.0/src/reeval_python/wire.py +0 -81
- reeval_python-0.10.0/tests/evals/__init__.py +0 -0
- reeval_python-0.10.0/tests/evals/tags_mirror.py +0 -14
- reeval_python-0.10.0/tests/test_assertion.py +0 -44
- reeval_python-0.10.0/tests/test_conformance_assert.py +0 -103
- reeval_python-0.10.0/tests/test_conformance_declare.py +0 -25
- reeval_python-0.10.0/tests/test_conformance_sync_async.py +0 -89
- {reeval_python-0.10.0 → reeval_python-0.14.0}/CHANGELOG.md +0 -0
- {reeval_python-0.10.0 → reeval_python-0.14.0}/LICENSE +0 -0
- {reeval_python-0.10.0 → reeval_python-0.14.0}/package.json +0 -0
- {reeval_python-0.10.0 → reeval_python-0.14.0}/src/reeval_python/py.typed +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: reeval-python
|
|
3
|
+
Version: 0.14.0
|
|
4
|
+
Summary: Python authoring SDK for reeval (*.eval.py eval files)
|
|
5
|
+
Project-URL: Homepage, https://reeval.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/invertase/reeval
|
|
7
|
+
Project-URL: Documentation, https://github.com/invertase/reeval/tree/main/packages/reeval-python#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/invertase/reeval/issues
|
|
9
|
+
Author-email: Invertase <oss@invertase.io>
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.11
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# reeval-python
|
|
26
|
+
|
|
27
|
+
Python authoring SDK for [reeval](https://github.com/invertase/reeval). Write `*.eval.py` files,
|
|
28
|
+
declare evals with the same options as TypeScript, and run them through reeval's language runner
|
|
29
|
+
(over its stdin and stdout, runner protocol 3). Use the reeval-python version that matches your
|
|
30
|
+
reeval: each says which runner protocol it speaks, and refuses another.
|
|
31
|
+
|
|
32
|
+
Import as `reeval_python` (`from reeval_python import evaluate`). The PyPI distribution is
|
|
33
|
+
**reeval-python** (the name `reeval` is already taken on PyPI by another project).
|
|
34
|
+
|
|
35
|
+
With reeval installed, default discovery includes `evals/**/*.eval.py`. When your project has
|
|
36
|
+
`pyproject.toml` or `uv.lock` and [uv](https://docs.astral.sh/uv/) is on `PATH`, reeval starts the
|
|
37
|
+
runner with `uv run --project <root> python -m reeval_python`. Otherwise it uses `<root>/.venv/bin/python`
|
|
38
|
+
when present, then `python3` (`python` on Windows). Set `REEVAL_PYTHON` to override the interpreter.
|
|
39
|
+
The standalone `reeval` binary works the same way and needs only uv besides; install the
|
|
40
|
+
reeval-python release made for your reeval, since a runner on another protocol stops the run.
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv add reeval-python
|
|
46
|
+
# or: pip install reeval-python
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then `uv run reeval run`. reeval-python's `reeval` command runs the reeval binary of its own
|
|
50
|
+
version: the first run downloads it from the GitHub release, checks it against the sha256 this
|
|
51
|
+
release recorded, and caches it in `<cache>/reeval/bin/<version>/`.
|
|
52
|
+
|
|
53
|
+
Monorepo development:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd packages/reeval-python
|
|
57
|
+
uv sync --dev
|
|
58
|
+
uv run pytest
|
|
59
|
+
uv run ruff check .
|
|
60
|
+
uv run pyright
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Declare trees are checked against `packages/reeval/test/conformance/declare/` goldens. From the
|
|
64
|
+
reeval package, `test/python-sdk.test.ts` runs the same suite in CI.
|
|
65
|
+
|
|
66
|
+
## Writing evals
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from reeval_python import SyncAttemptContext, evaluate, expect
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@evaluate("lands", {"runs": 3, "timeout": 300})
|
|
73
|
+
def lands(c: SyncAttemptContext) -> None:
|
|
74
|
+
c.send("Create greet.py that prints hi")
|
|
75
|
+
assert c.exec("test -f greet.py").ok
|
|
76
|
+
expect(c.judge("Does greet.py print hi?")).to_pass()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Plain `assert` is first-class, as in pytest: each one in an eval file is a check in the run
|
|
80
|
+
record, named by its test as written (`assert c.exec("test -f greet.py").ok` is
|
|
81
|
+
`assert c.exec("test -f greet.py").ok`). A pass is recorded when it runs. A failure ends the body,
|
|
82
|
+
as in Python, and is recorded with its message and both sides of a comparison as the reason
|
|
83
|
+
(`greet.py exists: assert 1 == 2`). An `AssertionError` the body catches isn't a failed check, so
|
|
84
|
+
a retry loop can assert; one the escaping error carries (each failure in a `TaskGroup`'s
|
|
85
|
+
`ExceptionGroup`, or one an error was raised from or while handling) is. Use typed results (`ExecResult.ok`, `Verdict.passed`, `Turn.text`, …) and `c.file(path)`
|
|
86
|
+
for quick checks. Keep `expect` for reeval matchers (`to_have_file`, `to_succeed`, soft checks,
|
|
87
|
+
labels) — Playwright-style.
|
|
88
|
+
|
|
89
|
+
Matchers that take a pattern (`to_have_file`, `to_have_run`, `to_have_searched`, `to_have_fetched`,
|
|
90
|
+
`to_have_called_tool`, `to_have_edited`, `to_have_changed`) take a `str` or a compiled `re.Pattern`,
|
|
91
|
+
`str` or `bytes`, with any flags. Python matches it with `pattern.search`, so anything `re` accepts
|
|
92
|
+
works (lookbehind, `re.VERBOSE`, named groups). The record shows the pattern's `repr`.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
import re
|
|
96
|
+
|
|
97
|
+
expect(c.turns[0]).to_have_run(re.compile(r"^(npm|pnpm) t(est)?$"))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Use snake_case on the context (`read_file`, `write_file`, `file`) and on matchers (`to_succeed`,
|
|
101
|
+
`to_pass`, `to_equal`, `to_contain`, …). Matcher failures raise `ReevalAssertionError`.
|
|
102
|
+
|
|
103
|
+
Options are typed (`EvalOptions`, and `JudgeOptions` for `c.judge(…)`), so pyright refuses a
|
|
104
|
+
misspelt key such as `runz`. They take reeval's keys in snake_case at any depth (`min_lift`,
|
|
105
|
+
`delay_ms`); the keys of `env`, `headers` and the other maps of your own keep their spelling. Every
|
|
106
|
+
duration is seconds or a `timedelta`: `{"timeout": 90}`, `docker(start_timeout=timedelta(minutes=2))`,
|
|
107
|
+
`expect.poll(…, timeout=10)`. `docker()`, `fake_harness()`, `fake_judge()` and `plugin.*` return
|
|
108
|
+
values to pass as options, not data to edit.
|
|
109
|
+
|
|
110
|
+
### `def` and `async def` bodies
|
|
111
|
+
|
|
112
|
+
A `def` body gets a blocking context: `c.exec(…)` runs the command and returns its `ExecResult`,
|
|
113
|
+
and `c.read_file(…)` returns the contents. Use `expect` from `reeval_python`, whose matchers check
|
|
114
|
+
at once.
|
|
115
|
+
|
|
116
|
+
An `async def` body gets the async context: every `c.*` call is a coroutine, and `expect` comes
|
|
117
|
+
from `reeval_python.aio`, whose matchers you await. A call or matcher the body never awaits never
|
|
118
|
+
runs, so it fails the attempt, and so does a task the body started and left running when it
|
|
119
|
+
returns: await each one (`asyncio.gather`, a `TaskGroup`). Each `expect` refuses to run in the
|
|
120
|
+
other kind of body, and a body that `yield`s is refused where it's declared.
|
|
121
|
+
|
|
122
|
+
Annotate `c` (`SyncAttemptContext` or `AttemptContext`) and pyright checks the body too: a missing
|
|
123
|
+
`await`, the wrong context for the kind of body, a generator or a returned value. An unannotated
|
|
124
|
+
body is checked only when it runs.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from reeval_python import AttemptContext, evaluate
|
|
128
|
+
from reeval_python.aio import expect
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@evaluate("lands, async")
|
|
132
|
+
async def lands(c: AttemptContext) -> None:
|
|
133
|
+
await c.send("Create greet.py that prints hi")
|
|
134
|
+
await expect(await c.exec("python greet.py")).to_succeed()
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`c.exec(cmd, retries=2, interval=2)` runs a command that exits non-zero again, up to 2 more
|
|
138
|
+
times, 2 seconds apart (`interval` is seconds or a timedelta, one second by default), and returns
|
|
139
|
+
the last try. Use it for a check that can fail on a transient, such as a test that calls a model:
|
|
140
|
+
`expect(c.exec("pytest -q tests/hidden", retries=2)).to_succeed()`. Any non-zero exit retries,
|
|
141
|
+
and the record and output say `passed on try 2 of 3`.
|
|
142
|
+
|
|
143
|
+
`expect.poll` retries a matcher until it passes, every `interval` until `timeout` (one second by
|
|
144
|
+
default). Hand it a function that returns one `c.read_file(…)` or `c.exec(…)`, and reeval reads or
|
|
145
|
+
runs it again on each try; it works the same in both kinds of body. Check the output with the
|
|
146
|
+
matcher, not in the function: `lambda: c.exec("cat x").stdout` is refused.
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
expect.poll(lambda: c.read_file("ready.txt"), timeout=10).to_be("ready\n")
|
|
150
|
+
expect.poll(c, "the server wrote its log").to_have_file("server.log")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Names of `evaluate_each` cases
|
|
154
|
+
|
|
155
|
+
`evaluate_each(cases, name)` names each eval from `name`: `%#` is the case's index, `%s` the case
|
|
156
|
+
as `str()` writes it, and `$field` a dict case's field. Each placeholder is filled once, so a value
|
|
157
|
+
that contains `$` or `%` stays as it is, and a `$field` the case doesn't have is left as written.
|
|
158
|
+
Two evals with one name in a file are refused where they're declared.
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
@evaluate_each([{"lang": "en"}, {"lang": "fr"}], "greets in $lang (#%#)")
|
|
162
|
+
def greets(c: SyncAttemptContext, case: dict[str, str]) -> None: ...
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Examples
|
|
166
|
+
|
|
167
|
+
- Feature smoke: `packages/reeval/test/features/python-sdk/hello/` (uv project with a project dependency)
|
|
168
|
+
- Plan case: `packages/reeval/test/plan/python/`
|
|
169
|
+
- Contributor guide: `.agents/skills/add-a-language/SKILL.md` in the reeval repo
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# reeval-python
|
|
2
|
+
|
|
3
|
+
Python authoring SDK for [reeval](https://github.com/invertase/reeval). Write `*.eval.py` files,
|
|
4
|
+
declare evals with the same options as TypeScript, and run them through reeval's language runner
|
|
5
|
+
(over its stdin and stdout, runner protocol 3). Use the reeval-python version that matches your
|
|
6
|
+
reeval: each says which runner protocol it speaks, and refuses another.
|
|
7
|
+
|
|
8
|
+
Import as `reeval_python` (`from reeval_python import evaluate`). The PyPI distribution is
|
|
9
|
+
**reeval-python** (the name `reeval` is already taken on PyPI by another project).
|
|
10
|
+
|
|
11
|
+
With reeval installed, default discovery includes `evals/**/*.eval.py`. When your project has
|
|
12
|
+
`pyproject.toml` or `uv.lock` and [uv](https://docs.astral.sh/uv/) is on `PATH`, reeval starts the
|
|
13
|
+
runner with `uv run --project <root> python -m reeval_python`. Otherwise it uses `<root>/.venv/bin/python`
|
|
14
|
+
when present, then `python3` (`python` on Windows). Set `REEVAL_PYTHON` to override the interpreter.
|
|
15
|
+
The standalone `reeval` binary works the same way and needs only uv besides; install the
|
|
16
|
+
reeval-python release made for your reeval, since a runner on another protocol stops the run.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv add reeval-python
|
|
22
|
+
# or: pip install reeval-python
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Then `uv run reeval run`. reeval-python's `reeval` command runs the reeval binary of its own
|
|
26
|
+
version: the first run downloads it from the GitHub release, checks it against the sha256 this
|
|
27
|
+
release recorded, and caches it in `<cache>/reeval/bin/<version>/`.
|
|
28
|
+
|
|
29
|
+
Monorepo development:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cd packages/reeval-python
|
|
33
|
+
uv sync --dev
|
|
34
|
+
uv run pytest
|
|
35
|
+
uv run ruff check .
|
|
36
|
+
uv run pyright
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Declare trees are checked against `packages/reeval/test/conformance/declare/` goldens. From the
|
|
40
|
+
reeval package, `test/python-sdk.test.ts` runs the same suite in CI.
|
|
41
|
+
|
|
42
|
+
## Writing evals
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from reeval_python import SyncAttemptContext, evaluate, expect
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@evaluate("lands", {"runs": 3, "timeout": 300})
|
|
49
|
+
def lands(c: SyncAttemptContext) -> None:
|
|
50
|
+
c.send("Create greet.py that prints hi")
|
|
51
|
+
assert c.exec("test -f greet.py").ok
|
|
52
|
+
expect(c.judge("Does greet.py print hi?")).to_pass()
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Plain `assert` is first-class, as in pytest: each one in an eval file is a check in the run
|
|
56
|
+
record, named by its test as written (`assert c.exec("test -f greet.py").ok` is
|
|
57
|
+
`assert c.exec("test -f greet.py").ok`). A pass is recorded when it runs. A failure ends the body,
|
|
58
|
+
as in Python, and is recorded with its message and both sides of a comparison as the reason
|
|
59
|
+
(`greet.py exists: assert 1 == 2`). An `AssertionError` the body catches isn't a failed check, so
|
|
60
|
+
a retry loop can assert; one the escaping error carries (each failure in a `TaskGroup`'s
|
|
61
|
+
`ExceptionGroup`, or one an error was raised from or while handling) is. Use typed results (`ExecResult.ok`, `Verdict.passed`, `Turn.text`, …) and `c.file(path)`
|
|
62
|
+
for quick checks. Keep `expect` for reeval matchers (`to_have_file`, `to_succeed`, soft checks,
|
|
63
|
+
labels) — Playwright-style.
|
|
64
|
+
|
|
65
|
+
Matchers that take a pattern (`to_have_file`, `to_have_run`, `to_have_searched`, `to_have_fetched`,
|
|
66
|
+
`to_have_called_tool`, `to_have_edited`, `to_have_changed`) take a `str` or a compiled `re.Pattern`,
|
|
67
|
+
`str` or `bytes`, with any flags. Python matches it with `pattern.search`, so anything `re` accepts
|
|
68
|
+
works (lookbehind, `re.VERBOSE`, named groups). The record shows the pattern's `repr`.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import re
|
|
72
|
+
|
|
73
|
+
expect(c.turns[0]).to_have_run(re.compile(r"^(npm|pnpm) t(est)?$"))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Use snake_case on the context (`read_file`, `write_file`, `file`) and on matchers (`to_succeed`,
|
|
77
|
+
`to_pass`, `to_equal`, `to_contain`, …). Matcher failures raise `ReevalAssertionError`.
|
|
78
|
+
|
|
79
|
+
Options are typed (`EvalOptions`, and `JudgeOptions` for `c.judge(…)`), so pyright refuses a
|
|
80
|
+
misspelt key such as `runz`. They take reeval's keys in snake_case at any depth (`min_lift`,
|
|
81
|
+
`delay_ms`); the keys of `env`, `headers` and the other maps of your own keep their spelling. Every
|
|
82
|
+
duration is seconds or a `timedelta`: `{"timeout": 90}`, `docker(start_timeout=timedelta(minutes=2))`,
|
|
83
|
+
`expect.poll(…, timeout=10)`. `docker()`, `fake_harness()`, `fake_judge()` and `plugin.*` return
|
|
84
|
+
values to pass as options, not data to edit.
|
|
85
|
+
|
|
86
|
+
### `def` and `async def` bodies
|
|
87
|
+
|
|
88
|
+
A `def` body gets a blocking context: `c.exec(…)` runs the command and returns its `ExecResult`,
|
|
89
|
+
and `c.read_file(…)` returns the contents. Use `expect` from `reeval_python`, whose matchers check
|
|
90
|
+
at once.
|
|
91
|
+
|
|
92
|
+
An `async def` body gets the async context: every `c.*` call is a coroutine, and `expect` comes
|
|
93
|
+
from `reeval_python.aio`, whose matchers you await. A call or matcher the body never awaits never
|
|
94
|
+
runs, so it fails the attempt, and so does a task the body started and left running when it
|
|
95
|
+
returns: await each one (`asyncio.gather`, a `TaskGroup`). Each `expect` refuses to run in the
|
|
96
|
+
other kind of body, and a body that `yield`s is refused where it's declared.
|
|
97
|
+
|
|
98
|
+
Annotate `c` (`SyncAttemptContext` or `AttemptContext`) and pyright checks the body too: a missing
|
|
99
|
+
`await`, the wrong context for the kind of body, a generator or a returned value. An unannotated
|
|
100
|
+
body is checked only when it runs.
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from reeval_python import AttemptContext, evaluate
|
|
104
|
+
from reeval_python.aio import expect
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@evaluate("lands, async")
|
|
108
|
+
async def lands(c: AttemptContext) -> None:
|
|
109
|
+
await c.send("Create greet.py that prints hi")
|
|
110
|
+
await expect(await c.exec("python greet.py")).to_succeed()
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`c.exec(cmd, retries=2, interval=2)` runs a command that exits non-zero again, up to 2 more
|
|
114
|
+
times, 2 seconds apart (`interval` is seconds or a timedelta, one second by default), and returns
|
|
115
|
+
the last try. Use it for a check that can fail on a transient, such as a test that calls a model:
|
|
116
|
+
`expect(c.exec("pytest -q tests/hidden", retries=2)).to_succeed()`. Any non-zero exit retries,
|
|
117
|
+
and the record and output say `passed on try 2 of 3`.
|
|
118
|
+
|
|
119
|
+
`expect.poll` retries a matcher until it passes, every `interval` until `timeout` (one second by
|
|
120
|
+
default). Hand it a function that returns one `c.read_file(…)` or `c.exec(…)`, and reeval reads or
|
|
121
|
+
runs it again on each try; it works the same in both kinds of body. Check the output with the
|
|
122
|
+
matcher, not in the function: `lambda: c.exec("cat x").stdout` is refused.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
expect.poll(lambda: c.read_file("ready.txt"), timeout=10).to_be("ready\n")
|
|
126
|
+
expect.poll(c, "the server wrote its log").to_have_file("server.log")
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Names of `evaluate_each` cases
|
|
130
|
+
|
|
131
|
+
`evaluate_each(cases, name)` names each eval from `name`: `%#` is the case's index, `%s` the case
|
|
132
|
+
as `str()` writes it, and `$field` a dict case's field. Each placeholder is filled once, so a value
|
|
133
|
+
that contains `$` or `%` stays as it is, and a `$field` the case doesn't have is left as written.
|
|
134
|
+
Two evals with one name in a file are refused where they're declared.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
@evaluate_each([{"lang": "en"}, {"lang": "fr"}], "greets in $lang (#%#)")
|
|
138
|
+
def greets(c: SyncAttemptContext, case: dict[str, str]) -> None: ...
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Examples
|
|
142
|
+
|
|
143
|
+
- Feature smoke: `packages/reeval/test/features/python-sdk/hello/` (uv project with a project dependency)
|
|
144
|
+
- Plan case: `packages/reeval/test/plan/python/`
|
|
145
|
+
- Contributor guide: `.agents/skills/add-a-language/SKILL.md` in the reeval repo
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "reeval-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.14.0"
|
|
4
4
|
description = "Python authoring SDK for reeval (*.eval.py eval files)"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
7
7
|
license = "Apache-2.0"
|
|
8
8
|
license-files = ["LICENSE"]
|
|
9
|
-
authors = [{ name = "Invertase" }]
|
|
9
|
+
authors = [{ name = "Invertase", email = "oss@invertase.io" }]
|
|
10
10
|
classifiers = [
|
|
11
11
|
"Development Status :: 4 - Beta",
|
|
12
12
|
"Intended Audience :: Developers",
|
|
@@ -15,11 +15,15 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3.11",
|
|
16
16
|
"Programming Language :: Python :: 3.12",
|
|
17
17
|
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
18
19
|
"Topic :: Software Development :: Testing",
|
|
19
20
|
"Typing :: Typed",
|
|
20
21
|
]
|
|
21
22
|
dependencies = []
|
|
22
23
|
|
|
24
|
+
[project.scripts]
|
|
25
|
+
reeval = "reeval_python.cli:main"
|
|
26
|
+
|
|
23
27
|
[project.urls]
|
|
24
28
|
Homepage = "https://reeval.ai"
|
|
25
29
|
Repository = "https://github.com/invertase/reeval"
|
|
@@ -40,6 +44,8 @@ pythonpath = ["."]
|
|
|
40
44
|
[tool.ruff]
|
|
41
45
|
line-length = 100
|
|
42
46
|
target-version = "py311"
|
|
47
|
+
# Generated by packages/reeval/scripts/sdk-types.ts.
|
|
48
|
+
extend-exclude = ["src/reeval_python/_wire.py"]
|
|
43
49
|
|
|
44
50
|
[tool.ruff.lint]
|
|
45
51
|
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -52,8 +58,12 @@ pythonVersion = "3.11"
|
|
|
52
58
|
reportUnknownLambdaType = false
|
|
53
59
|
|
|
54
60
|
[build-system]
|
|
55
|
-
requires = ["hatchling"]
|
|
61
|
+
requires = ["hatchling>=1.27,<2"]
|
|
56
62
|
build-backend = "hatchling.build"
|
|
57
63
|
|
|
64
|
+
# binary.yml writes the release's binary hashes here before `uv build`; it isn't checked in.
|
|
65
|
+
[tool.hatch.build]
|
|
66
|
+
artifacts = ["src/reeval_python/binaries.json"]
|
|
67
|
+
|
|
58
68
|
[tool.hatch.build.targets.wheel]
|
|
59
69
|
packages = ["src/reeval_python"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Python eval authoring API for reeval (*.eval.py)."""
|
|
2
|
+
|
|
3
|
+
from reeval_python.context import AttemptContext, SyncAttemptContext
|
|
4
|
+
from reeval_python.declare import evaluate, evaluate_each, group, todo
|
|
5
|
+
from reeval_python.environment import (
|
|
6
|
+
DEFAULT_EGRESS_ALLOW,
|
|
7
|
+
DockerSandbox,
|
|
8
|
+
Environment,
|
|
9
|
+
FakeHarness,
|
|
10
|
+
FakeJudge,
|
|
11
|
+
Plugin,
|
|
12
|
+
PluginBuild,
|
|
13
|
+
define_environment,
|
|
14
|
+
docker,
|
|
15
|
+
fake_harness,
|
|
16
|
+
fake_judge,
|
|
17
|
+
plugin,
|
|
18
|
+
)
|
|
19
|
+
from reeval_python.errors import ReevalAssertionError, ReevalCallError
|
|
20
|
+
from reeval_python.expect import Expectation, expect, soft
|
|
21
|
+
from reeval_python.options import EvalOptions, JudgeDefaults, JudgeOptions
|
|
22
|
+
from reeval_python.types import BackgroundHandle, Diff, ExecResult, FileChange, Turn, Verdict
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"DEFAULT_EGRESS_ALLOW",
|
|
26
|
+
"AttemptContext",
|
|
27
|
+
"BackgroundHandle",
|
|
28
|
+
"Diff",
|
|
29
|
+
"DockerSandbox",
|
|
30
|
+
"Environment",
|
|
31
|
+
"EvalOptions",
|
|
32
|
+
"ExecResult",
|
|
33
|
+
"Expectation",
|
|
34
|
+
"FakeHarness",
|
|
35
|
+
"FakeJudge",
|
|
36
|
+
"FileChange",
|
|
37
|
+
"JudgeDefaults",
|
|
38
|
+
"JudgeOptions",
|
|
39
|
+
"Plugin",
|
|
40
|
+
"PluginBuild",
|
|
41
|
+
"ReevalAssertionError",
|
|
42
|
+
"ReevalCallError",
|
|
43
|
+
"SyncAttemptContext",
|
|
44
|
+
"Turn",
|
|
45
|
+
"Verdict",
|
|
46
|
+
"define_environment",
|
|
47
|
+
"docker",
|
|
48
|
+
"evaluate",
|
|
49
|
+
"evaluate_each",
|
|
50
|
+
"expect",
|
|
51
|
+
"fake_harness",
|
|
52
|
+
"fake_judge",
|
|
53
|
+
"group",
|
|
54
|
+
"plugin",
|
|
55
|
+
"soft",
|
|
56
|
+
"todo",
|
|
57
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
if __name__ == "__main__":
|
|
4
|
+
# reeval starts its runner with REEVAL_RUNNER set; anyone else gets the `reeval` command.
|
|
5
|
+
if os.environ.get("REEVAL_RUNNER"):
|
|
6
|
+
from reeval_python.runner import main
|
|
7
|
+
else:
|
|
8
|
+
from reeval_python.cli import main
|
|
9
|
+
main()
|