reeval-python 0.10.0__tar.gz → 0.15.1__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.
Files changed (95) hide show
  1. {reeval_python-0.10.0 → reeval_python-0.15.1}/.gitignore +2 -0
  2. reeval_python-0.15.1/PKG-INFO +198 -0
  3. reeval_python-0.15.1/README.md +174 -0
  4. {reeval_python-0.10.0 → reeval_python-0.15.1}/pyproject.toml +13 -3
  5. reeval_python-0.15.1/src/reeval_python/__init__.py +57 -0
  6. reeval_python-0.15.1/src/reeval_python/__main__.py +9 -0
  7. reeval_python-0.15.1/src/reeval_python/_wire.py +1972 -0
  8. reeval_python-0.15.1/src/reeval_python/aio.py +22 -0
  9. reeval_python-0.15.1/src/reeval_python/binaries.json +55 -0
  10. reeval_python-0.15.1/src/reeval_python/checks.py +277 -0
  11. reeval_python-0.15.1/src/reeval_python/cli.py +522 -0
  12. reeval_python-0.15.1/src/reeval_python/context.py +395 -0
  13. reeval_python-0.15.1/src/reeval_python/declare.py +619 -0
  14. reeval_python-0.15.1/src/reeval_python/environment.py +187 -0
  15. reeval_python-0.15.1/src/reeval_python/errors.py +26 -0
  16. reeval_python-0.15.1/src/reeval_python/expect.py +285 -0
  17. reeval_python-0.15.1/src/reeval_python/options.py +57 -0
  18. reeval_python-0.15.1/src/reeval_python/runner.py +547 -0
  19. reeval_python-0.15.1/src/reeval_python/types.py +121 -0
  20. reeval_python-0.15.1/src/reeval_python/wire.py +178 -0
  21. reeval_python-0.15.1/tests/context/abort.eval.py +6 -0
  22. reeval_python-0.15.1/tests/context/abort.sync.eval.py +6 -0
  23. reeval_python-0.15.1/tests/context/exec-retries.eval.py +7 -0
  24. reeval_python-0.15.1/tests/context/exec-retries.sync.eval.py +7 -0
  25. reeval_python-0.15.1/tests/context/exec.eval.py +10 -0
  26. reeval_python-0.15.1/tests/context/exec.sync.eval.py +10 -0
  27. reeval_python-0.15.1/tests/context/expect-fail.eval.py +12 -0
  28. reeval_python-0.15.1/tests/context/expect-fail.sync.eval.py +11 -0
  29. reeval_python-0.15.1/tests/context/expect.eval.py +16 -0
  30. reeval_python-0.15.1/tests/context/expect.sync.eval.py +14 -0
  31. reeval_python-0.15.1/tests/context/files.eval.py +16 -0
  32. reeval_python-0.15.1/tests/context/files.sync.eval.py +16 -0
  33. reeval_python-0.15.1/tests/context/judge.eval.py +10 -0
  34. reeval_python-0.15.1/tests/context/judge.sync.eval.py +10 -0
  35. reeval_python-0.15.1/tests/context/record.async.eval.py +14 -0
  36. reeval_python-0.15.1/tests/context/record.eval.py +14 -0
  37. reeval_python-0.15.1/tests/context/send.eval.py +8 -0
  38. reeval_python-0.15.1/tests/context/send.sync.eval.py +8 -0
  39. reeval_python-0.15.1/tests/context/sync-async.async.eval.py +7 -0
  40. reeval_python-0.15.1/tests/context/sync-async.eval.py +6 -0
  41. reeval_python-0.15.1/tests/declare/compare-questions.eval.py +43 -0
  42. reeval_python-0.15.1/tests/declare/compare-variants.eval.py +19 -0
  43. reeval_python-0.15.1/tests/declare/each-families.eval.py +25 -0
  44. reeval_python-0.15.1/tests/declare/egress-cascade.eval.py +39 -0
  45. reeval_python-0.15.1/tests/declare/environment-extend.eval.py +13 -0
  46. reeval_python-0.15.1/tests/declare/environment-plugin-marketplace.eval.py +19 -0
  47. reeval_python-0.15.1/tests/declare/environment-plugins.eval.py +35 -0
  48. reeval_python-0.15.1/tests/declare/groups-skip-todo.eval.py +16 -0
  49. reeval_python-0.15.1/tests/declare/questions-attempts.eval.py +39 -0
  50. reeval_python-0.15.1/tests/declare/questions-group.eval.py +47 -0
  51. reeval_python-0.15.1/tests/declare/questions-state.eval.py +66 -0
  52. reeval_python-0.15.1/tests/declare/revision.eval.py +20 -0
  53. reeval_python-0.15.1/tests/declare/tags.eval.py +20 -0
  54. reeval_python-0.15.1/tests/runner_process.py +90 -0
  55. reeval_python-0.15.1/tests/stdio_host.py +181 -0
  56. reeval_python-0.15.1/tests/test_api.py +311 -0
  57. reeval_python-0.15.1/tests/test_checks.py +293 -0
  58. reeval_python-0.15.1/tests/test_cli.py +644 -0
  59. reeval_python-0.15.1/tests/test_conformance_context.py +70 -0
  60. reeval_python-0.15.1/tests/test_conformance_declare.py +52 -0
  61. reeval_python-0.15.1/tests/test_load.py +96 -0
  62. reeval_python-0.15.1/tests/test_patterns.py +148 -0
  63. reeval_python-0.15.1/tests/test_python_wheels.py +277 -0
  64. reeval_python-0.15.1/tests/test_runner_attempts.py +121 -0
  65. reeval_python-0.15.1/tests/test_runner_checks.py +287 -0
  66. reeval_python-0.15.1/tests/test_runner_isolation.py +368 -0
  67. reeval_python-0.15.1/tests/test_runner_json.py +92 -0
  68. reeval_python-0.15.1/tests/test_runner_misuse.py +199 -0
  69. reeval_python-0.15.1/tests/test_runner_sdk.py +198 -0
  70. reeval_python-0.15.1/tests/test_runner_stdio.py +102 -0
  71. reeval_python-0.15.1/tests/test_source.py +191 -0
  72. reeval_python-0.15.1/tests/test_types.py +130 -0
  73. {reeval_python-0.10.0 → reeval_python-0.15.1}/uv.lock +1 -1
  74. reeval_python-0.10.0/PKG-INFO +0 -85
  75. reeval_python-0.10.0/README.md +0 -62
  76. reeval_python-0.10.0/src/reeval_python/__init__.py +0 -23
  77. reeval_python-0.10.0/src/reeval_python/__main__.py +0 -4
  78. reeval_python-0.10.0/src/reeval_python/assertion.py +0 -168
  79. reeval_python-0.10.0/src/reeval_python/context.py +0 -163
  80. reeval_python-0.10.0/src/reeval_python/declare.py +0 -263
  81. reeval_python-0.10.0/src/reeval_python/errors.py +0 -7
  82. reeval_python-0.10.0/src/reeval_python/expect.py +0 -95
  83. reeval_python-0.10.0/src/reeval_python/runner.py +0 -190
  84. reeval_python-0.10.0/src/reeval_python/types.py +0 -97
  85. reeval_python-0.10.0/src/reeval_python/wire.py +0 -81
  86. reeval_python-0.10.0/tests/evals/__init__.py +0 -0
  87. reeval_python-0.10.0/tests/evals/tags_mirror.py +0 -14
  88. reeval_python-0.10.0/tests/test_assertion.py +0 -44
  89. reeval_python-0.10.0/tests/test_conformance_assert.py +0 -103
  90. reeval_python-0.10.0/tests/test_conformance_declare.py +0 -25
  91. reeval_python-0.10.0/tests/test_conformance_sync_async.py +0 -89
  92. {reeval_python-0.10.0 → reeval_python-0.15.1}/CHANGELOG.md +0 -0
  93. {reeval_python-0.10.0 → reeval_python-0.15.1}/LICENSE +0 -0
  94. {reeval_python-0.10.0 → reeval_python-0.15.1}/package.json +0 -0
  95. {reeval_python-0.10.0 → reeval_python-0.15.1}/src/reeval_python/py.typed +0 -0
@@ -4,3 +4,5 @@ __pycache__/
4
4
  *.egg-info/
5
5
  dist/
6
6
  .venv/
7
+ .venv-*/
8
+ src/reeval_python/binaries.json
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.5
2
+ Name: reeval-python
3
+ Version: 0.15.1
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
+ #### What a caught failure counts as
90
+
91
+ A failed `expect` is a check the moment it fails: reeval records it before
92
+ `ReevalAssertionError` reaches the body. One the body catches still fails the attempt when the
93
+ body ends, as `expect.soft` does, and its error says the eval caught it. A plain `assert` the body
94
+ catches records nothing, as in Python.
95
+
96
+ | The body catches | Recorded | Fails the attempt |
97
+ |---|---|---|
98
+ | a failed `expect(…)` | a failed check | yes, when the body ends |
99
+ | a failed `assert` | nothing | no |
100
+ | neither (it escapes) | a failed check | yes |
101
+
102
+ So retry with `expect.poll`, which retries the matcher and records only its final outcome, or with
103
+ a plain `assert` or `if` in the loop, never `expect` in `try`/`except`:
104
+
105
+ ```python
106
+ # Retries the command until it succeeds, for up to 10 seconds: one check.
107
+ expect.poll(lambda: c.exec("curl -sf localhost:3000"), timeout=10).to_succeed()
108
+
109
+ # A plain assert in a retry loop: only the try that held is recorded.
110
+ for _ in range(3):
111
+ try:
112
+ assert c.exec("test -f ready.txt").ok
113
+ break
114
+ except AssertionError:
115
+ time.sleep(1)
116
+ ```
117
+
118
+ Matchers that take a pattern (`to_have_file`, `to_have_run`, `to_have_searched`, `to_have_fetched`,
119
+ `to_have_called_tool`, `to_have_edited`, `to_have_changed`) take a `str` or a compiled `re.Pattern`,
120
+ `str` or `bytes`, with any flags. Python matches it with `pattern.search`, so anything `re` accepts
121
+ works (lookbehind, `re.VERBOSE`, named groups). The record shows the pattern's `repr`.
122
+
123
+ ```python
124
+ import re
125
+
126
+ expect(c.turns[0]).to_have_run(re.compile(r"^(npm|pnpm) t(est)?$"))
127
+ ```
128
+
129
+ Use snake_case on the context (`read_file`, `write_file`, `file`) and on matchers (`to_succeed`,
130
+ `to_pass`, `to_equal`, `to_contain`, …). Matcher failures raise `ReevalAssertionError`.
131
+
132
+ Options are typed (`EvalOptions`, and `JudgeOptions` for `c.judge(…)`), so pyright refuses a
133
+ misspelled key such as `runz`. They take reeval's keys in snake_case at any depth (`min_lift`,
134
+ `delay_ms`); the keys of `env`, `headers` and the other maps of your own keep their spelling. Every
135
+ duration is seconds or a `timedelta`: `{"timeout": 90}`, `docker(start_timeout=timedelta(minutes=2))`,
136
+ `expect.poll(…, timeout=10)`. `docker()`, `fake_harness()`, `fake_judge()` and `plugin.*` return
137
+ values to pass as options, not data to edit.
138
+
139
+ ### `def` and `async def` bodies
140
+
141
+ A `def` body gets a blocking context: `c.exec(…)` runs the command and returns its `ExecResult`,
142
+ and `c.read_file(…)` returns the contents. Use `expect` from `reeval_python`, whose matchers check
143
+ at once.
144
+
145
+ An `async def` body gets the async context: every `c.*` call is a coroutine, and `expect` comes
146
+ from `reeval_python.aio`, whose matchers you await. A call or matcher the body never awaits never
147
+ runs, so it fails the attempt, and so does a task the body started and left running when it
148
+ returns: await each one (`asyncio.gather`, a `TaskGroup`). Each `expect` refuses to run in the
149
+ other kind of body, and a body that `yield`s is refused where it's declared.
150
+
151
+ Annotate `c` (`SyncAttemptContext` or `AttemptContext`) and pyright checks the body too: a missing
152
+ `await`, the wrong context for the kind of body, a generator or a returned value. An unannotated
153
+ body is checked only when it runs.
154
+
155
+ ```python
156
+ from reeval_python import AttemptContext, evaluate
157
+ from reeval_python.aio import expect
158
+
159
+
160
+ @evaluate("lands, async")
161
+ async def lands(c: AttemptContext) -> None:
162
+ await c.send("Create greet.py that prints hi")
163
+ await expect(await c.exec("python greet.py")).to_succeed()
164
+ ```
165
+
166
+ `c.exec(cmd, retries=2, interval=2)` runs a command that exits non-zero again, up to 2 more
167
+ times, 2 seconds apart (`interval` is seconds or a timedelta, one second by default), and returns
168
+ the last try. Use it for a check that can fail on a transient, such as a test that calls a model:
169
+ `expect(c.exec("pytest -q tests/hidden", retries=2)).to_succeed()`. Any non-zero exit retries,
170
+ and the record and output say `passed on try 2 of 3`.
171
+
172
+ `expect.poll` retries a matcher until it passes, every `interval` until `timeout` (one second by
173
+ default). Hand it a function that returns one `c.read_file(…)` or `c.exec(…)`, and reeval reads or
174
+ runs it again on each try; it works the same in both kinds of body. Check the output with the
175
+ matcher, not in the function: `lambda: c.exec("cat x").stdout` is refused.
176
+
177
+ ```python
178
+ expect.poll(lambda: c.read_file("ready.txt"), timeout=10).to_be("ready\n")
179
+ expect.poll(c, "the server wrote its log").to_have_file("server.log")
180
+ ```
181
+
182
+ ### Names of `evaluate_each` cases
183
+
184
+ `evaluate_each(cases, name)` names each eval from `name`: `%#` is the case's index, `%s` the case
185
+ as `str()` writes it, and `$field` a dict case's field. Each placeholder is filled once, so a value
186
+ that contains `$` or `%` stays as it is, and a `$field` the case doesn't have is left as written.
187
+ Two evals with one name in a file are refused where they're declared.
188
+
189
+ ```python
190
+ @evaluate_each([{"lang": "en"}, {"lang": "fr"}], "greets in $lang (#%#)")
191
+ def greets(c: SyncAttemptContext, case: dict[str, str]) -> None: ...
192
+ ```
193
+
194
+ ## Examples
195
+
196
+ - Feature smoke: `packages/reeval/test/features/python-sdk/hello/` (uv project with a project dependency)
197
+ - Plan case: `packages/reeval/test/plan/python/`
198
+ - Contributor guide: `.agents/skills/add-a-language/SKILL.md` in the reeval repo
@@ -0,0 +1,174 @@
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
+ #### What a caught failure counts as
66
+
67
+ A failed `expect` is a check the moment it fails: reeval records it before
68
+ `ReevalAssertionError` reaches the body. One the body catches still fails the attempt when the
69
+ body ends, as `expect.soft` does, and its error says the eval caught it. A plain `assert` the body
70
+ catches records nothing, as in Python.
71
+
72
+ | The body catches | Recorded | Fails the attempt |
73
+ |---|---|---|
74
+ | a failed `expect(…)` | a failed check | yes, when the body ends |
75
+ | a failed `assert` | nothing | no |
76
+ | neither (it escapes) | a failed check | yes |
77
+
78
+ So retry with `expect.poll`, which retries the matcher and records only its final outcome, or with
79
+ a plain `assert` or `if` in the loop, never `expect` in `try`/`except`:
80
+
81
+ ```python
82
+ # Retries the command until it succeeds, for up to 10 seconds: one check.
83
+ expect.poll(lambda: c.exec("curl -sf localhost:3000"), timeout=10).to_succeed()
84
+
85
+ # A plain assert in a retry loop: only the try that held is recorded.
86
+ for _ in range(3):
87
+ try:
88
+ assert c.exec("test -f ready.txt").ok
89
+ break
90
+ except AssertionError:
91
+ time.sleep(1)
92
+ ```
93
+
94
+ Matchers that take a pattern (`to_have_file`, `to_have_run`, `to_have_searched`, `to_have_fetched`,
95
+ `to_have_called_tool`, `to_have_edited`, `to_have_changed`) take a `str` or a compiled `re.Pattern`,
96
+ `str` or `bytes`, with any flags. Python matches it with `pattern.search`, so anything `re` accepts
97
+ works (lookbehind, `re.VERBOSE`, named groups). The record shows the pattern's `repr`.
98
+
99
+ ```python
100
+ import re
101
+
102
+ expect(c.turns[0]).to_have_run(re.compile(r"^(npm|pnpm) t(est)?$"))
103
+ ```
104
+
105
+ Use snake_case on the context (`read_file`, `write_file`, `file`) and on matchers (`to_succeed`,
106
+ `to_pass`, `to_equal`, `to_contain`, …). Matcher failures raise `ReevalAssertionError`.
107
+
108
+ Options are typed (`EvalOptions`, and `JudgeOptions` for `c.judge(…)`), so pyright refuses a
109
+ misspelled key such as `runz`. They take reeval's keys in snake_case at any depth (`min_lift`,
110
+ `delay_ms`); the keys of `env`, `headers` and the other maps of your own keep their spelling. Every
111
+ duration is seconds or a `timedelta`: `{"timeout": 90}`, `docker(start_timeout=timedelta(minutes=2))`,
112
+ `expect.poll(…, timeout=10)`. `docker()`, `fake_harness()`, `fake_judge()` and `plugin.*` return
113
+ values to pass as options, not data to edit.
114
+
115
+ ### `def` and `async def` bodies
116
+
117
+ A `def` body gets a blocking context: `c.exec(…)` runs the command and returns its `ExecResult`,
118
+ and `c.read_file(…)` returns the contents. Use `expect` from `reeval_python`, whose matchers check
119
+ at once.
120
+
121
+ An `async def` body gets the async context: every `c.*` call is a coroutine, and `expect` comes
122
+ from `reeval_python.aio`, whose matchers you await. A call or matcher the body never awaits never
123
+ runs, so it fails the attempt, and so does a task the body started and left running when it
124
+ returns: await each one (`asyncio.gather`, a `TaskGroup`). Each `expect` refuses to run in the
125
+ other kind of body, and a body that `yield`s is refused where it's declared.
126
+
127
+ Annotate `c` (`SyncAttemptContext` or `AttemptContext`) and pyright checks the body too: a missing
128
+ `await`, the wrong context for the kind of body, a generator or a returned value. An unannotated
129
+ body is checked only when it runs.
130
+
131
+ ```python
132
+ from reeval_python import AttemptContext, evaluate
133
+ from reeval_python.aio import expect
134
+
135
+
136
+ @evaluate("lands, async")
137
+ async def lands(c: AttemptContext) -> None:
138
+ await c.send("Create greet.py that prints hi")
139
+ await expect(await c.exec("python greet.py")).to_succeed()
140
+ ```
141
+
142
+ `c.exec(cmd, retries=2, interval=2)` runs a command that exits non-zero again, up to 2 more
143
+ times, 2 seconds apart (`interval` is seconds or a timedelta, one second by default), and returns
144
+ the last try. Use it for a check that can fail on a transient, such as a test that calls a model:
145
+ `expect(c.exec("pytest -q tests/hidden", retries=2)).to_succeed()`. Any non-zero exit retries,
146
+ and the record and output say `passed on try 2 of 3`.
147
+
148
+ `expect.poll` retries a matcher until it passes, every `interval` until `timeout` (one second by
149
+ default). Hand it a function that returns one `c.read_file(…)` or `c.exec(…)`, and reeval reads or
150
+ runs it again on each try; it works the same in both kinds of body. Check the output with the
151
+ matcher, not in the function: `lambda: c.exec("cat x").stdout` is refused.
152
+
153
+ ```python
154
+ expect.poll(lambda: c.read_file("ready.txt"), timeout=10).to_be("ready\n")
155
+ expect.poll(c, "the server wrote its log").to_have_file("server.log")
156
+ ```
157
+
158
+ ### Names of `evaluate_each` cases
159
+
160
+ `evaluate_each(cases, name)` names each eval from `name`: `%#` is the case's index, `%s` the case
161
+ as `str()` writes it, and `$field` a dict case's field. Each placeholder is filled once, so a value
162
+ that contains `$` or `%` stays as it is, and a `$field` the case doesn't have is left as written.
163
+ Two evals with one name in a file are refused where they're declared.
164
+
165
+ ```python
166
+ @evaluate_each([{"lang": "en"}, {"lang": "fr"}], "greets in $lang (#%#)")
167
+ def greets(c: SyncAttemptContext, case: dict[str, str]) -> None: ...
168
+ ```
169
+
170
+ ## Examples
171
+
172
+ - Feature smoke: `packages/reeval/test/features/python-sdk/hello/` (uv project with a project dependency)
173
+ - Plan case: `packages/reeval/test/plan/python/`
174
+ - 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.10.0"
3
+ version = "0.15.1"
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()