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.
Files changed (91) hide show
  1. {reeval_python-0.10.0 → reeval_python-0.14.0}/.gitignore +2 -0
  2. reeval_python-0.14.0/PKG-INFO +169 -0
  3. reeval_python-0.14.0/README.md +145 -0
  4. {reeval_python-0.10.0 → reeval_python-0.14.0}/pyproject.toml +13 -3
  5. reeval_python-0.14.0/src/reeval_python/__init__.py +57 -0
  6. reeval_python-0.14.0/src/reeval_python/__main__.py +9 -0
  7. reeval_python-0.14.0/src/reeval_python/_wire.py +1867 -0
  8. reeval_python-0.14.0/src/reeval_python/aio.py +22 -0
  9. reeval_python-0.14.0/src/reeval_python/binaries.json +55 -0
  10. reeval_python-0.14.0/src/reeval_python/checks.py +277 -0
  11. reeval_python-0.14.0/src/reeval_python/cli.py +505 -0
  12. reeval_python-0.14.0/src/reeval_python/context.py +395 -0
  13. reeval_python-0.14.0/src/reeval_python/declare.py +410 -0
  14. reeval_python-0.14.0/src/reeval_python/environment.py +187 -0
  15. reeval_python-0.14.0/src/reeval_python/errors.py +26 -0
  16. reeval_python-0.14.0/src/reeval_python/expect.py +285 -0
  17. reeval_python-0.14.0/src/reeval_python/options.py +54 -0
  18. reeval_python-0.14.0/src/reeval_python/runner.py +543 -0
  19. reeval_python-0.14.0/src/reeval_python/types.py +121 -0
  20. reeval_python-0.14.0/src/reeval_python/wire.py +174 -0
  21. reeval_python-0.14.0/tests/context/abort.eval.py +6 -0
  22. reeval_python-0.14.0/tests/context/abort.sync.eval.py +6 -0
  23. reeval_python-0.14.0/tests/context/exec-retries.eval.py +7 -0
  24. reeval_python-0.14.0/tests/context/exec-retries.sync.eval.py +7 -0
  25. reeval_python-0.14.0/tests/context/exec.eval.py +10 -0
  26. reeval_python-0.14.0/tests/context/exec.sync.eval.py +10 -0
  27. reeval_python-0.14.0/tests/context/expect-fail.eval.py +12 -0
  28. reeval_python-0.14.0/tests/context/expect-fail.sync.eval.py +11 -0
  29. reeval_python-0.14.0/tests/context/expect.eval.py +16 -0
  30. reeval_python-0.14.0/tests/context/expect.sync.eval.py +14 -0
  31. reeval_python-0.14.0/tests/context/files.eval.py +16 -0
  32. reeval_python-0.14.0/tests/context/files.sync.eval.py +16 -0
  33. reeval_python-0.14.0/tests/context/judge.eval.py +10 -0
  34. reeval_python-0.14.0/tests/context/judge.sync.eval.py +10 -0
  35. reeval_python-0.14.0/tests/context/record.async.eval.py +14 -0
  36. reeval_python-0.14.0/tests/context/record.eval.py +14 -0
  37. reeval_python-0.14.0/tests/context/send.eval.py +8 -0
  38. reeval_python-0.14.0/tests/context/send.sync.eval.py +8 -0
  39. reeval_python-0.14.0/tests/context/sync-async.async.eval.py +7 -0
  40. reeval_python-0.14.0/tests/context/sync-async.eval.py +6 -0
  41. reeval_python-0.14.0/tests/declare/compare-questions.eval.py +43 -0
  42. reeval_python-0.14.0/tests/declare/compare-variants.eval.py +19 -0
  43. reeval_python-0.14.0/tests/declare/each-families.eval.py +25 -0
  44. reeval_python-0.14.0/tests/declare/egress-cascade.eval.py +39 -0
  45. reeval_python-0.14.0/tests/declare/environment-extend.eval.py +13 -0
  46. reeval_python-0.14.0/tests/declare/environment-plugin-marketplace.eval.py +19 -0
  47. reeval_python-0.14.0/tests/declare/environment-plugins.eval.py +35 -0
  48. reeval_python-0.14.0/tests/declare/groups-skip-todo.eval.py +16 -0
  49. reeval_python-0.14.0/tests/declare/questions-attempts.eval.py +39 -0
  50. reeval_python-0.14.0/tests/declare/questions-group.eval.py +47 -0
  51. reeval_python-0.14.0/tests/declare/questions-state.eval.py +66 -0
  52. reeval_python-0.14.0/tests/declare/tags.eval.py +20 -0
  53. reeval_python-0.14.0/tests/runner_process.py +90 -0
  54. reeval_python-0.14.0/tests/stdio_host.py +171 -0
  55. reeval_python-0.14.0/tests/test_api.py +254 -0
  56. reeval_python-0.14.0/tests/test_checks.py +293 -0
  57. reeval_python-0.14.0/tests/test_cli.py +626 -0
  58. reeval_python-0.14.0/tests/test_conformance_context.py +70 -0
  59. reeval_python-0.14.0/tests/test_conformance_declare.py +46 -0
  60. reeval_python-0.14.0/tests/test_patterns.py +148 -0
  61. reeval_python-0.14.0/tests/test_python_wheels.py +277 -0
  62. reeval_python-0.14.0/tests/test_runner_attempts.py +121 -0
  63. reeval_python-0.14.0/tests/test_runner_checks.py +287 -0
  64. reeval_python-0.14.0/tests/test_runner_isolation.py +368 -0
  65. reeval_python-0.14.0/tests/test_runner_misuse.py +199 -0
  66. reeval_python-0.14.0/tests/test_runner_sdk.py +198 -0
  67. reeval_python-0.14.0/tests/test_runner_stdio.py +102 -0
  68. reeval_python-0.14.0/tests/test_types.py +130 -0
  69. {reeval_python-0.10.0 → reeval_python-0.14.0}/uv.lock +1 -1
  70. reeval_python-0.10.0/PKG-INFO +0 -85
  71. reeval_python-0.10.0/README.md +0 -62
  72. reeval_python-0.10.0/src/reeval_python/__init__.py +0 -23
  73. reeval_python-0.10.0/src/reeval_python/__main__.py +0 -4
  74. reeval_python-0.10.0/src/reeval_python/assertion.py +0 -168
  75. reeval_python-0.10.0/src/reeval_python/context.py +0 -163
  76. reeval_python-0.10.0/src/reeval_python/declare.py +0 -263
  77. reeval_python-0.10.0/src/reeval_python/errors.py +0 -7
  78. reeval_python-0.10.0/src/reeval_python/expect.py +0 -95
  79. reeval_python-0.10.0/src/reeval_python/runner.py +0 -190
  80. reeval_python-0.10.0/src/reeval_python/types.py +0 -97
  81. reeval_python-0.10.0/src/reeval_python/wire.py +0 -81
  82. reeval_python-0.10.0/tests/evals/__init__.py +0 -0
  83. reeval_python-0.10.0/tests/evals/tags_mirror.py +0 -14
  84. reeval_python-0.10.0/tests/test_assertion.py +0 -44
  85. reeval_python-0.10.0/tests/test_conformance_assert.py +0 -103
  86. reeval_python-0.10.0/tests/test_conformance_declare.py +0 -25
  87. reeval_python-0.10.0/tests/test_conformance_sync_async.py +0 -89
  88. {reeval_python-0.10.0 → reeval_python-0.14.0}/CHANGELOG.md +0 -0
  89. {reeval_python-0.10.0 → reeval_python-0.14.0}/LICENSE +0 -0
  90. {reeval_python-0.10.0 → reeval_python-0.14.0}/package.json +0 -0
  91. {reeval_python-0.10.0 → reeval_python-0.14.0}/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,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.10.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()