fix(evals): reject non-positive max_concurrency#6228
Conversation
📝 WalkthroughWalkthroughThe change adds validation to Changes
Sequence Diagram(s)sequenceDiagram
participant Caller
participant Dataset as Dataset.evaluate
Caller->>Dataset: evaluate(max_concurrency)
alt max_concurrency < 1
Dataset-->>Caller: raise ValueError
else valid
Dataset->>Dataset: proceed with concurrency setup
end
Related Issues: None referenced. Related PRs: None referenced. Suggested labels: pydantic_evals Suggested reviewers: None identified. A rabbit hops through concurrency's gate, 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/evals/test_dataset.py (1)
508-517: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing VCR-skip rationale.
This test pins a pre-request guard (validation short-circuits before the task ever runs), which is exactly the case the guideline calls out for requiring an explanation of why it's not a VCR test.
📝 Suggested docstring
`@pytest.mark.parametrize`('max_concurrency', [0, -1]) async def test_evaluate_with_invalid_max_concurrency( example_dataset: Dataset[TaskInput, TaskOutput, TaskMetadata], max_concurrency: int ): + """Not a VCR test: validation raises before the task/API call is ever made.""" async def mock_task(inputs: TaskInput) -> TaskOutput: # pragma: no cover return TaskOutput(answer=inputs.query)As per coding guidelines, "Each unit test should explain why it is not (or cannot be) a VCR test, especially when it pins internal behavior, pre-request guards, or request payload shape that cassettes would not reliably catch."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/evals/test_dataset.py` around lines 508 - 517, Add a short docstring to test_evaluate_with_invalid_max_concurrency explaining why this is not a VCR test: it validates the pre-request guard in Dataset.evaluate, so the task never runs and no HTTP interaction is recorded. Mention the key symbol names evaluate and max_concurrency so future readers understand this test intentionally pins internal validation behavior rather than cassette-backed I/O.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/evals/test_dataset.py`:
- Around line 508-517: Add a short docstring to
test_evaluate_with_invalid_max_concurrency explaining why this is not a VCR
test: it validates the pre-request guard in Dataset.evaluate, so the task never
runs and no HTTP interaction is recorded. Mention the key symbol names evaluate
and max_concurrency so future readers understand this test intentionally pins
internal validation behavior rather than cassette-backed I/O.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 8f30ca1c-d2c6-43b7-9204-518c9ec06199
📒 Files selected for processing (2)
pydantic_evals/pydantic_evals/dataset.pytests/evals/test_dataset.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
pydantic/logfire(auto-detected)pydantic/pydantic(auto-detected)
Make
Dataset.evaluate()fail fast for non-positivemax_concurrencyvalues instead of hanging on an impossible semaphore acquisition.No issue; small focused bug fix.
Checklist
What Problem This Solves
Dataset.evaluate()exposesmax_concurrencyas a user-facing limit for how many case evaluations may run at the same time. The valid meanings are:None: no explicit limit; run all cases concurrently as before.0is neither of those meanings. It does not mean unbounded concurrency, because that is already represented byNone; and it does not mean “run zero evaluations”, becauseDataset.evaluate()still builds the case tasks and tries to execute them.Today,
max_concurrency=0is passed directly intoanyio.Semaphore(0). Once evaluation starts, every case task waits for a permit, but no permit can ever become available. The result is not a clear configuration error; the evaluation simply makes no progress until something external times out.A minimal local reproduction before this change timed out:
That failure mode is especially confusing because the apparent symptom is a hung evaluation, slow task, stuck evaluator, retry issue, or event-loop problem. The actual root cause is much simpler: a non-positive concurrency limit cannot schedule any work. Failing fast at the
Dataset.evaluate()boundary gives the caller an immediate and actionable error before any tasks, evaluators, lifecycle hooks, or progress reporting are started.This PR only rejects explicit non-positive integer limits. It does not change the existing unbounded
max_concurrency=Nonebehavior, positive concurrency limits such as1, task execution, evaluator execution, retries, lifecycle handling, or result aggregation.Change
Add an early validation guard in
Dataset.evaluate():The existing
Nonebehavior remains unchanged and still means all cases can run concurrently. Positive integer limits, including1, continue to work as before.Evidence
Before this change, this call hung until wrapped by an external timeout:
After this change, invalid values fail fast:
Regression coverage was added for both
0and-1.Possible call chain / impact
This PR only validates explicit non-positive integer limits. It does not change task execution, evaluator execution, retry behavior, lifecycle hooks,
repeat, progress rendering, or themax_concurrency=Noneunbounded path.Validation
uv run pytest tests/evals/test_dataset.py::test_evaluate_with_concurrency tests/evals/test_dataset.py::test_evaluate_with_invalid_max_concurrency -p no:cacheprovider- passed, 3 testsuv run ruff check pydantic_evals/pydantic_evals/dataset.py tests/evals/test_dataset.py- passedgit diff --check- passed0and-1- passed