Enable the ASYNC (flake8-async) ruff rule group (#221) - #223
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 54 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (19)
WalkthroughSubprocess exit waiting now separates process termination and stream-consumer draining from timeout handling. Timeout metadata is clarified, public timeout parameters retain lint suppressions, regression tests cover non-positive deadlines, and Ruff enables async checks. ChangesSubprocess timeout lifecycle
Sequence Diagram(s)sequenceDiagram
participant SafeCmd
participant WaitHelper
participant Process
participant StreamConsumers
SafeCmd->>WaitHelper: pass execution.timeout
WaitHelper->>Process: await process.wait()
WaitHelper->>Process: terminate on timeout or cancellation
WaitHelper->>StreamConsumers: cancel and gather pending tasks
StreamConsumers-->>SafeCmd: drained stream results
Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 17 | ❌ 3❌ Failed checks (3 warnings)
✅ Passed checks (17 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b58b6fab3e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Follow-up: preserve immediate expiry for non-positive timeouts ( Addressing the review concern that adopting Unlike the Both wait call sites now route through Gates green ( |
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@cuprum/unittests/test_subprocess_timeout.py`:
- Around line 13-17: Move the _SubprocessExecution import into a
TYPE_CHECKING-only block, while keeping _wait_for_exit_code and
_wait_for_exit_code_within_timeout as runtime imports. Preserve the string-based
typ.cast("_SubprocessExecution", ...) usage so no runtime dependency is
introduced.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 309ceeab-7faf-4a34-ad02-e94ede7af196
📒 Files selected for processing (5)
cuprum/_subprocess_execution.pycuprum/_subprocess_timeout.pycuprum/sh.pycuprum/unittests/test_subprocess_timeout.pypyproject.toml
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
leynos/shared-actions(auto-detected)leynos/pylint-pypy-shim(auto-detected)leynos/whitaker(auto-detected)
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
This comment was marked as resolved.
|
@coderabbitai resume |
✅ Action performedReviews resumed. |
This comment was marked as resolved.
This comment was marked as resolved.
|
Follow-up: developer documentation + timeout observability ( Completing the required documentation and observability work in this PR (not deferred to a follow-up issue). Timeout observability — implemented via the least-invasive existing mechanism: a new Three distinguishable, best-effort records:
Stable fields: Documentation — developers' guide gains a "Ruff Validation (exact commands / outcomes):
|
|
Follow-up: observe-hook timeout observability + documentation ( Reworking the timeout observability to go through the existing Event contract (smallest compatible extension). Two ancillary phases added to the public
Adapters updated: the metrics adapter (whose dispatch raises on unknown phases) counts Exception precedence. Emission is best-effort via Tests. Unit-level recording-observation tests for elapsed/immediate Documentation. Users' guide structured-events section documents the new phases/fields; developers' guide rewrites the timeout observability section for the event contract; ADR-007 records the observe events. Validation (exact commands / outcomes):
|
Move the wait deadline out of the internal subprocess helpers and onto the caller via `async with asyncio.timeout(...)`, the modern replacement for `asyncio.wait_for`. `_wait_for_exit_code` and `_run_subprocess_with_streams` no longer take a `timeout` parameter, so a deadline expiry now arrives as `CancelledError` and is torn down identically to an external cancellation, while `asyncio.timeout` re-raises expiry as `TimeoutError` at the context boundary. This composes correctly with cancellation and clears two flake8-async ASYNC109 findings on internal helpers. Rename `_handle_stream_timeout`'s `timeout` parameter to `configured_timeout` to reflect that it is carried metadata populating the raised error, not a deadline the coroutine awaits on; this clears a third ASYNC109 finding that was a false positive on the parameter name. Existing timeout, termination, and consumer-draining behaviour is unchanged; the affected unit tests are updated to own the deadline via `asyncio.timeout` and to use the renamed keyword. Refs #221 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add "ASYNC" to `[tool.ruff.lint].select` so async-correctness lints such as ASYNC109 and ASYNC240 run as part of `make lint`. With the internal helpers already refactored onto `asyncio.timeout`, the remaining findings are resolved deliberately rather than with a blanket suppression: - The public `SafeCmd.run` and `Pipeline.run` keep their `timeout` keyword — documented ergonomics that mirror `subprocess.run(timeout=…)` — carrying a per-line `# noqa: ASYNC109` with a rationale comment. - The async test scaffolding polls a PID file with asyncio-only helpers, so ASYNC109 and ASYNC240 are added to the existing `**/test_*.py` per-file-ignore with an explanatory comment (trio/anyio async paths are not in use). Closes #221 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adopting `asyncio.timeout()` in the subprocess wait helpers introduced a subtle behavioural regression for non-positive timeouts. Unlike the `asyncio.wait_for()` it replaced, `asyncio.timeout()` only schedules its cancellation for the next event-loop iteration. A fast, already-exited process whose `wait()` returns without suspending therefore raced past a zero or negative deadline and reported success, making `run(timeout=0)` timing-dependent (notably on the stream-capture path). Route both wait call sites through a new `_wait_for_exit_code_within_timeout` helper that special-cases a non-positive `execution.timeout`: it tears the process down and raises `TimeoutError` immediately, without awaiting `wait()`, restoring the deterministic expiry the previous implementation guaranteed. The shared terminate-and-drain teardown is extracted into `_terminate_and_drain_consumers` so the immediate-expiry and cancellation paths reuse identical cleanup. Add deterministic regression tests covering zero and negative timeouts against an already-exited process double, including consumer draining. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The `_SubprocessExecution` symbol is referenced only through the
string-based `typ.cast("_SubprocessExecution", ...)` calls, so it needs
no runtime binding. Move it into a `TYPE_CHECKING` block, keeping
`_wait_for_exit_code` and `_wait_for_exit_code_within_timeout` as runtime
imports, so the test module carries no unnecessary runtime dependency.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Record the timeout semantics introduced by the flake8-async work so the guides stay in step with the code. - Users' guide: clarify that a `timeout` of `0` or negative expires immediately, terminating an already-running process without waiting and raising `TimeoutExpired`. This is existing behaviour, not a change, so no migration note is added. - Developers' guide: describe the caller-owned deadline design (`asyncio.timeout()` in place of `asyncio.wait_for()`), the `_wait_for_exit_code` / `_wait_for_exit_code_within_timeout` split, the non-positive-timeout immediate-expiry special case, and the extracted `_terminate_and_drain_consumers` teardown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Surface distinguishable, best-effort diagnostics on a new `cuprum.timeout` logger for the subprocess timeout paths, following the established `cuprum.stdin` convention (structured `cuprum_*` extra fields keyed for observability integrations rather than message parsing). No public API, `ExecEvent` phase, or exit-event behaviour changes: this is the least invasive existing mechanism and avoids the adapter/snapshot churn a new `ExecPhase` would require. Three cases are now observable: - ordinary expiry: `subprocess_timeout_expired` (WARNING) with `cuprum_timeout_mode="elapsed"`; - non-positive immediate expiry: the same record with `cuprum_timeout_mode="immediate"`; - teardown drain failure: `subprocess_teardown_drain_failed` (ERROR) with `cuprum_teardown_outcome="drain_error"` when a consumer drains with an unexpected exception (absorbed to preserve the primary timeout or cancellation, but reported). Stable fields: `cuprum_operation`, `cuprum_pid`, `cuprum_timeout_s`, `cuprum_error_type`, `cuprum_timeout_mode`, and `cuprum_teardown_outcome`. Emission is strictly best-effort: `_emit_timeout_log` suppresses any logging exception so telemetry can never mask `TimeoutError`/`TimeoutExpired` or `CancelledError`, nor abort teardown. `_terminate_and_drain_consumers` now inspects drained results and reports non-cancellation failures. Tests cover elapsed and immediate expiry field contracts, the drain-failure record, and that a raising logger does not mask the timeout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Complete the developer-facing documentation for the flake8-async work and the timeout telemetry contract, and lock the required wording with documentation- contract tests. - Developers' guide: add a "Ruff `ASYNC` (flake8-async) policy" subsection explaining why the family is selected, the narrowly scoped public-API `# noqa: ASYNC109` on `SafeCmd.run` / `Pipeline.run`, and the test-scaffolding per-file-ignore for `ASYNC109` / `ASYNC240`, linked to the `pyproject.toml` comments. Add a "Subprocess timeout observability" section documenting the `cuprum.timeout` records and their stable `cuprum_*` fields. - ADR-007: add a dated addendum recording the `_wait_for_exit_code` / `_wait_for_exit_code_within_timeout` split, caller-owned `asyncio.timeout` deadlines, the non-positive fast path, the shared `_terminate_and_drain_consumers` teardown, and the invariant that no pending stream-consumer task is left behind on cancellation, expiry, or immediate expiry. - Add `test_async_timeout_docs.py` asserting the required ASYNC-policy and timeout-observability wording, mirroring the repo's documentation-contract test conventions. Regenerate the maturin wheel-manifest snapshot for the new test module. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the public ExecEvent observe contract with two ancillary phases and two fields so callers can distinguish subprocess timeout conditions through the existing sh.observe() stream rather than a parallel telemetry framework: - `timeout` — a run exceeded its deadline; - `teardown_error` — a stream consumer drained with an unexpected error; - `timeout_s` — the configured timeout in seconds; - `timeout_mode` — `elapsed_deadline` versus `non_positive_immediate`. Wire the new phases through the adapters. The metrics adapter must handle them (its dispatch raises on unknown phases): both increment counters (`cuprum_timeouts_total`, `cuprum_teardown_errors_total`) via a new counter dispatch table that also keeps `__call__` under the complexity ceiling. The tracing adapter records them as ancillary span events that leave the span open for the subsequent `exit`. The logging adapter's existing default path handles them. Regenerate the adapter projection snapshot for the two new phases and add metrics/tracing unit coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Emit the new `timeout` and `teardown_error` observe events from the subprocess timeout paths, alongside the existing best-effort `cuprum.timeout` log records (mode values unified to `elapsed_deadline` / `non_positive_immediate`). `_wait_for_exit_code_within_timeout` emits `timeout` on both the elapsed and non-positive immediate expiry routes; `_terminate_and_drain_consumers` emits `teardown_error` when a drained consumer surfaces an unexpected exception. The observation is threaded through `_wait_for_exit_code` to the shared teardown solely for this reporting; it never changes control flow. Emission is best-effort via `_safe_emit`, which swallows a synchronous observe-hook failure (including a hook raising `CancelledError`) so telemetry can never mask `TimeoutExpired` or `CancelledError`; `_StageObservation.emit` records scheduled async-hook tasks before raising, so they are still drained. The existing `start`/`exit` events and the public `TimeoutExpired` mapping are unchanged. Add deterministic tests: unit-level recording-observation tests for elapsed and immediate `timeout` events and the `teardown_error` event; end-to-end `sh.observe()` tests driving the non-positive fast path (`run_sync(timeout=0)`) that assert the event contract, the preserved start/exit events, and that a hook failing on the timeout event does not mask `TimeoutExpired`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Document the public timeout observe-event contract and lock the required wording with documentation-contract tests. - Users' guide: add the `timeout` and `teardown_error` phases and the `timeout`-event fields (including `timeout_mode` of `elapsed_deadline` / `non_positive_immediate`) to the structured-events section, noting the preserved `start`/`exit` events and `TimeoutExpired`. - Developers' guide: rewrite the timeout observability section to describe the observe events, adapter handling, and the parallel `cuprum.timeout` log records, with the unified mode values. - ADR-007: record that the timeout paths emit best-effort `timeout` / `teardown_error` observe events whose emission failures cannot mask `TimeoutExpired`. - Extend `test_async_timeout_docs.py` with the observe-event terms and add users' guide contract tests for the public run()/run_sync() timeout contract and the timeout events. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cde4651 to
59e1935
Compare
Pre-merge check warnings: already addressed at head (
|
| Branch | Observe event | Structured log |
|---|---|---|
| Ordinary expiry | timeout, timeout_mode="elapsed_deadline" |
subprocess_timeout_expired (WARNING) |
| Non-positive fast path | timeout, timeout_mode="non_positive_immediate" |
same record, distinct mode |
| Teardown/drain failure | teardown_error |
subprocess_teardown_drain_failed (ERROR) |
Stable fields are present on both: operation, pid, configured timeout (timeout_s), and error class (error_type), plus timeout_mode and cuprum_teardown_outcome.
cuprum/events.py:28-29,139-140—timeout/teardown_erroradded to the publicExecPhase;timeout_s/timeout_modefields.cuprum/_subprocess_execution.py:152-161,176-185— emission at both expiry routes;:86-88— teardown/drain failure.cuprum/_subprocess_timeout.py:29,70,99— thecuprum.timeoutlogger and both records.- Adapters updated:
cuprum_timeouts_total/cuprum_teardown_errors_totalcounters and tracing span events.
Emission is best-effort throughout, so telemetry cannot mask TimeoutExpired or CancelledError, and cleanup precedence is preserved.
Commits: dfa480b, b31a8a2, 94aaaea.
Validation at head
uv run pytest cuprum/unittests/test_subprocess_timeout.py cuprum/unittests/test_async_timeout_docs.py cuprum/unittests/test_observe.py → 87 passed, covering exactly these three areas (including documentation-contract tests that assert the required wording mechanically).
Full post-rebase gates all green: make check-fmt, make lint, make typecheck, make test (828 Python + 57 Rust), make markdownlint, make nixie. CodeRabbit's own review --agent reported 0 findings.
Could the pre-merge checks be re-run against head?
Summary
Closes #221.
The
ASYNC(flake8-async) rule group was not in ruff'sselectlist, soasync-correctness lints such as
ASYNC109andASYNC240never ran. This changeenables the group and resolves the eight findings it surfaced, treating each
finding on its merits rather than with a blanket suppression.
Changes
asyncio.timeoutin the internal wait helpers (ASYNC109×2)._wait_for_exit_codeand_run_subprocess_with_streamsno longer take atimeoutparameter; the deadline now belongs to the caller viaasync with asyncio.timeout(...), the modern replacement forasyncio.wait_for. A deadline expiry now arrives asCancelledErrorand istorn down identically to an external cancellation, while
asyncio.timeoutre-raises expiry as
TimeoutErrorat the context boundary. Existing timeout,termination, and consumer-draining behaviour is unchanged.
ASYNC109×1, false positive)._handle_stream_timeout'stimeoutparameter is renamed toconfigured_timeout, reflecting that it populates the raised error ratherthan bounding an
await.run(timeout=…)API (ASYNC109×2).SafeCmd.runandPipeline.runretain their documentedtimeoutkeyword,which mirrors
subprocess.run(timeout=…); each carries a per-line# noqa: ASYNC109with a rationale comment.ASYNC109×1,ASYNC240×2).The async PID-file polling helper uses asyncio-only primitives, so
ASYNC109and
ASYNC240are added to the existing**/test_*.pyper-file-ignore withan explanatory comment (trio/anyio async paths are not in use).
"ASYNC"is added to[tool.ruff.lint].select.Verification
make check-fmt,make lint,make typecheck, andmake testall pass.uv run ruff check --select ASYNC .reports no findings.confirming the
asyncio.timeoutrefactor preserves behaviour.coderabbit review --agent) reported zero findings.References
🤖 Generated with Claude Code