Split cuprum/context.py into a context/ package (#116) - #157
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughSplit the former ChangesExecution context package
Benchmark ratchet measurement
Rust pump I/O and error handling
Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 18 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (18 passed)
📋 Issue PlannerBuilt with CodeRabbit's Coding Plans for faster development and fewer bugs. View plan used: ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
52d04ee to
b564a66
Compare
b564a66 to
fa2c4bd
Compare
|
CodeScene suppression request — Code Duplication in CodeScene flags: "The module contains 2 functions with similar structure: Suggested suppression message for the CodeScene UI:
If preferred, I can instead merge the three factories behind |
fa2c4bd to
1e8f75d
Compare
1e8f75d to
685b933
Compare
685b933 to
182c9a3
Compare
182c9a3 to
7b0685f
Compare
7b0685f to
73299d1
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 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/context/__init__.py`:
- Around line 75-101: Remove the six underscore-prefixed helper names from
__all__ in cuprum.context.__init__, while leaving their explicit module bindings
and implementations unchanged so direct imports and existing tests continue to
work.
In `@cuprum/context/registration.py`:
- Around line 119-126: Update Registration.detach so _detached is set only after
_reset_context successfully restores the captured token; keep the token intact
when restoration raises ValueError, allowing a retry in the originating context.
- Around line 191-207: Update the hook type branching in __init__ to explicitly
handle "before", "after", and "observe"; raise ValueError for any other value
instead of falling through to with_observe_hook. Preserve the existing hook
casting and context installation for the three supported discriminators.
In `@cuprum/unittests/test_token_registration_stateful.py`:
- Around line 109-113: Update
cuprum/unittests/test_token_registration_stateful.py at lines 109-113 to assert
current_context() is self._baseline after teardown drains _stack, preserving the
existing LIFO detach behavior. At lines 135-154, capture the caller context
before scoped(...) and assert current_context() is that context after scope
exit, while retaining the existing out-of-order detach assertions.
- Line 101: Update all four bare assertions in
cuprum/unittests/test_token_registration_stateful.py at lines 101, 107, 147, and
154 with the specified failure messages, covering first-detach restoration,
baseline restoration, inner-overlay discard, and captured-overlay restoration.
Preserve the existing assertion conditions and use assert-with-message syntax
throughout.
🪄 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: fb8ca6a2-ced2-43c1-8c68-407095bf325e
📒 Files selected for processing (10)
cuprum/context.pycuprum/context/__init__.pycuprum/context/core.pycuprum/context/env_overlay.pycuprum/context/registration.pycuprum/context/state.pycuprum/unittests/__snapshots__/test_maturin_build.ambrcuprum/unittests/test_token_registration_stateful.pydocs/developers-guide.mdtypos.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)
💤 Files with no reviewable changes (1)
- cuprum/context.py
|
@coderabbitai resume |
✅ Action performedReviews resumed. |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (3)
cuprum/context/__init__.py (1)
75-101: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winStrip the underscore-prefixed helpers out of
__all__.
__all__still lists_merge_after_hooks,_merge_before_hooks,_merge_observe_hooks,_narrow_allowlist,_resolve_narrowed_timeout, and_validate_timeout. The formercuprum.context.__all__excluded these; keep the explicit module-level bindings (socuprum.context._validate_timeoutetc. still resolve for existing tests) but stop advertising them through the public surface / wildcard imports.🐛 Proposed fix
__all__ = [ "AfterHook", "AllowRegistration", "BeforeHook", "CuprumContext", "EnvRegistration", "ExecHook", "ForbiddenProgramError", "HookRegistration", "ScopeConfig", - "_merge_after_hooks", - "_merge_before_hooks", - "_merge_observe_hooks", - "_narrow_allowlist", - "_resolve_narrowed_timeout", - "_validate_timeout", "after",🤖 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 `@cuprum/context/__init__.py` around lines 75 - 101, Remove the underscore-prefixed helper names from the __all__ list in the module while leaving their module-level definitions and bindings unchanged, so direct access such as cuprum.context._validate_timeout continues to work without exposing them through wildcard imports.cuprum/unittests/test_token_registration_stateful.py (2)
101-101: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAttach failure messages to every remaining bare assertion.
Lines 101, 107, 147, and 154 are still bare
assertstatements. Wire in the failure messages so a violated restoration invariant is identifiable from the test output alone.Proposed assertion messages
- assert after_first is prior + assert after_first is prior, "first detach must restore the prior context"- assert current_context() is self._baseline + assert current_context() is self._baseline, ( + "an empty stack must restore the baseline context" + )- assert "CUPRUM_TEST_INNER" not in overlay + assert "CUPRUM_TEST_INNER" not in overlay, ( + "outer detach must discard the inner overlay" + )- assert leaked.get("CUPRUM_TEST_OUTER") == "outer" + assert leaked.get("CUPRUM_TEST_OUTER") == "outer", ( + "inner detach must restore its captured outer overlay" + )As per path instructions, "Use
assert …, "message"over bare asserts."Also applies to: 147-147, 154-154, 107-107
🤖 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 `@cuprum/unittests/test_token_registration_stateful.py` at line 101, Update the bare assertions in the stateful token registration tests, including those around after_first and the assertions at the other referenced locations, to use the assert condition, message form. Provide each assertion with a clear message identifying the violated restoration invariant.Source: Path instructions
109-113: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAssert restoration after automatic cleanup and scope exit.
teardown()drains_stackbut never asserts the state-machine baseline is actually restored afterwards, andtest_out_of_order_detach_restores_outer_snapshotnever checks that leaving thescoped(...)block restores the caller's own context. Either gap lets a final-step cleanup regression leak state silently.Proposed test additions
def teardown(self) -> None: """Detach any remaining handles in LIFO order.""" while self._stack: handle, _prior = self._stack.pop() handle.detach() + assert current_context() is self._baseline, ( + "teardown must restore the state machine baseline" + )def test_out_of_order_detach_restores_outer_snapshot() -> None: """...""" + caller_context = current_context() with scoped(ScopeConfig()): ... leaked = current_context().env_overlay or {} assert leaked.get("CUPRUM_TEST_OUTER") == "outer", ( "inner detach must restore its captured outer overlay" ) + assert current_context() is caller_context, ( + "scope exit must restore the caller context" + )As per coding guidelines, "New functionality and behavioral changes require substantive, non-vacuous tests" covering edge cases and functional boundaries.
Also applies to: 135-154
🤖 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 `@cuprum/unittests/test_token_registration_stateful.py` around lines 109 - 113, Update teardown() to assert that automatic cleanup restores the state-machine baseline after draining _stack, using the existing baseline/state assertion mechanism. Extend test_out_of_order_detach_restores_outer_snapshot to assert that exiting the scoped(...) block restores the caller’s original context, covering both automatic cleanup and scope-exit restoration.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.
Inline comments:
In `@cuprum/context/core.py`:
- Line 14: Update the timeout validation logic in the affected context
configuration flow to reject all non-finite values, including NaN and positive
or negative infinity, before the existing negative-value check proceeds. Use the
existing timeout validation symbols and raise ValueError consistently for these
invalid inputs.
- Around line 31-32: Define or reuse a package-level domain exception base, then
update ForbiddenProgramError to inherit from both that base and PermissionError.
Extend its initialization to accept and store the denied program and
restricted_state as attributes while preserving the existing descriptive
message.
In `@cuprum/unittests/test_token_registration_stateful.py`:
- Around line 103-107: Update stack tracking in the stateful test so each
registration stores both its prior context and installed context in self._stack.
Expand stack_depth_matches_context_nesting to assert current_context() matches
the baseline when empty and the top entry’s installed context when nested, and
adjust every self._stack.pop() unpacking site to handle the two stored values.
---
Duplicate comments:
In `@cuprum/context/__init__.py`:
- Around line 75-101: Remove the underscore-prefixed helper names from the
__all__ list in the module while leaving their module-level definitions and
bindings unchanged, so direct access such as cuprum.context._validate_timeout
continues to work without exposing them through wildcard imports.
In `@cuprum/unittests/test_token_registration_stateful.py`:
- Line 101: Update the bare assertions in the stateful token registration tests,
including those around after_first and the assertions at the other referenced
locations, to use the assert condition, message form. Provide each assertion
with a clear message identifying the violated restoration invariant.
- Around line 109-113: Update teardown() to assert that automatic cleanup
restores the state-machine baseline after draining _stack, using the existing
baseline/state assertion mechanism. Extend
test_out_of_order_detach_restores_outer_snapshot to assert that exiting the
scoped(...) block restores the caller’s original context, covering both
automatic cleanup and scope-exit restoration.
🪄 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: 6f447008-5fd5-4870-b930-c1fba8f36580
📒 Files selected for processing (10)
cuprum/context.pycuprum/context/__init__.pycuprum/context/core.pycuprum/context/env_overlay.pycuprum/context/registration.pycuprum/context/state.pycuprum/unittests/__snapshots__/test_maturin_build.ambrcuprum/unittests/test_token_registration_stateful.pydocs/developers-guide.mdtypos.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)
💤 Files with no reviewable changes (1)
- cuprum/context.py
73299d1 to
ba6091a
Compare
|
@coderabbitai Please suggest a fix for the following concern and provide an AI coding agent prompt for the fix: ❌ Failed checks (1 error)
|
This comment was marked as resolved.
This comment was marked as resolved.
Address review feedback on the documentation: - Add ADR-006 recording the decision to split `cuprum/context.py` into a `cuprum/context/` package, and index it from the developers guide. - List `ContextError` alongside `CuprumContext`, `ScopeConfig`, and `ForbiddenProgramError` in the core.py package-layout bullet, documenting it as the package-level root of the domain exception hierarchy. - Correct the retained debugging plan: the sampled commands come from the retained v2 plan and filter, not from an unchanged `ci_benchmark_ratchet_profile.py`; the current CI ratchet still raises `--runs` to 10 and interleaves each Python/Rust pair adjacently. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review feedback on the documentation: - Note in the design doc that the CI benchmark ratchet also writes a skip report when the existing `main` baseline uses an incompatible (older) benchmark profile whose sampling protocol is not comparable, not only when no prior baseline exists — matching `ratchet_rust_performance.py` and the users-guide description. - Use the `plaintext` fence language for the two hyperfine command blocks in the retained debugging plan, matching the plan's existing `plaintext` block; contents are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
dea79ea to
b657190
Compare
Collapse the double blank line left where the rebase merged the upstream "Build and test worker controls" section against this branch's canonical `_TokenRegistration` section, resolving an MD012 markdownlint violation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@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 `@docs/users-guide.md`:
- Around line 1491-1493: Update the incompatible older benchmark-profile
baseline documentation in users-guide.md to state that, in addition to skipping
comparison, the system writes a skip report. Keep the explanation about
differing sampling protocols and worker timings unchanged.
🪄 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: fdd87ab1-343c-467d-8f1f-025e2837d062
📒 Files selected for processing (24)
benchmarks/_validation.pybenchmarks/benchmark_profile.pybenchmarks/ci_benchmark_ratchet_profile.pycuprum/context.pycuprum/context/__init__.pycuprum/context/core.pycuprum/context/env_overlay.pycuprum/context/registration.pycuprum/context/state.pycuprum/unittests/__snapshots__/test_maturin_build.ambrcuprum/unittests/test_benchmark_ci_ratchet.pycuprum/unittests/test_ci_benchmark_ratchet_profile.pycuprum/unittests/test_context.pycuprum/unittests/test_rust_splice.pycuprum/unittests/test_rust_streams.pycuprum/unittests/test_token_registration_stateful.pydocs/adr-006-context-package-split.mddocs/cuprum-design.mddocs/debugging/debugging-plan-2026-07-19T17-32-12Z.mddocs/developers-guide.mddocs/users-guide.mdrust/cuprum-rust/src/errors.rstests/behaviour/test_rust_streams_behaviour.pytests/helpers/stream_pipes.py
🔗 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)
💤 Files with no reviewable changes (1)
- cuprum/context.py
State in the users guide that the benchmark ratchet writes a skip report (not only skips comparison) when the saved `main` baseline uses an older, incompatible benchmark profile shape — matching `ratchet_rust_performance.py`'s `write_incompatible_profile_report` path and the design-doc wording. The sampling-protocol/worker-timing explanation is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rebase onto main (#116/#157) introduced ADR-006 "Split cuprum/context.py into a context package", colliding with this branch's ADR-006 "Subprocess execution module boundaries". Main owns 006, so renumber the subprocess ADR to 007: rename the file and update every reference (contents.md, cuprum-design.md, developers-guide.md, and the ADR title). Both ADRs are retained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rebase onto main (#116/#157) introduced ADR-006 "Split cuprum/context.py into a context package", colliding with this branch's ADR-006 "Subprocess execution module boundaries". Main owns 006, so renumber the subprocess ADR to 007: rename the file and update every reference (contents.md, cuprum-design.md, developers-guide.md, and the ADR title). Both ADRs are retained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The rebase onto main (#116/#157) introduced ADR-006 "Split cuprum/context.py into a context package", colliding with this branch's ADR-006 "Subprocess execution module boundaries". Main owns 006, so renumber the subprocess ADR to 007: rename the file and update every reference (contents.md, cuprum-design.md, developers-guide.md, and the ADR title). Both ADRs are retained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Split _subprocess_execution.py along its stated seams (#117) `cuprum/_subprocess_execution.py` was 510 lines, carrying both a `# pylint: disable=too-many-lines` pragma and a `# TODO: refactor into smaller submodules` — the violation was suppressed rather than fixed. The module bundled stdin writing, stream-consumer spawning, timeout handling, and the top-level runner. Split along the seams the TODO already named: - `cuprum/_subprocess_stdin.py` — `_emit_stdin_error`, `_write_stdin`, `_spawn_stdin_writer`, with the `cuprum.stdin` logger now living in the module whose name matches it. - `cuprum/_subprocess_timeout.py` — the timeout dataclasses and errors, `_handle_subprocess_timeout` / `_raise_timeout_expired` / `_handle_stream_timeout`, the exit-event helpers they share with the normal completion path, and a single `_require_timeout` guard that centralises the duplicated "TimeoutError without a configured timeout" check. - `cuprum/_subprocess_execution.py` keeps the runner: `_execute_subprocess`, `_run_subprocess_with_streams`, `_spawn_subprocess`, and the stream-consumer wiring. Remove the `too-many-lines` pragma and the TODO (all three modules are now 103-274 lines), and drop the redundant `_resolve_timeout` re-export from `__all__` — `cuprum/sh.py` imports it from its definition site `cuprum._subprocess_context`. `test_observe` patches `_write_stdin` at its new home. The wheel-build snapshot reflects the new file list. * Refresh Oxford spelling policy after rebase Regenerate the Typos configuration with the current policy inputs and use `artefact` terminology in maintained prose so the spelling gate is reproducible. * Repair subprocess split after rebase (#117) Restore the helper imports and spacing required by the split modules after replaying the branch onto the current subprocess implementation. * Address subprocess split review feedback (#117) Use structural matching for timeout errors and document the accepted private module boundaries. Correct the ExecPlan style and duplicate artefact section, and record the non-reproducible Hypothesis health-check investigation. * Refresh Rust availability UI snapshot Capture the current compiler diagnostic for the intentionally non-const availability export after rebasing onto main. Remove merge-created spacing drift from the design document. * Preserve timeouts during stream cleanup (#117) Cancel stdin work before tolerant cleanup and retain timeout reporting when a stream consumer fails. Cover that interleaving, update its wheel snapshot, and apply the requested ADR and debugging-record documentation corrections. * Pin Rust UI tests to CI toolchain (#117) Align the workspace and lint job with Rust 1.85.0, then regenerate the compile-fail expectation with that compiler. Remove a redundant test closure return type so pinned Clippy remains warning-free. * Cancel blocked stdin writers on subprocess timeout (#117) Address review feedback on the subprocess-execution split. - Manage the direct-mode stdin writer separately from `_wait_for_exit_code`'s consumers: on timeout or cancellation it is now cancelled and drained before the failure is translated or propagated, so a drain wedged on an unread pipe cannot delay completion. Extract the shared cancel-and-drain step into `_cancel_stdin_writer` and reuse it on the streamed cancellation path. - Tighten the `_wait_for_exit_code` `consumers` type from `tuple[Task[Any], ...]` to `tuple[Task[None] | Task[str | None], ...]`. The timeout branch keeps gathering (not cancelling) its consumers, since after the stdin writer is managed separately the only remaining consumers are the stream readers whose partial output must survive on timeout. - Add a regression test for a direct-mode timeout with a stdin payload that wedges the writer against a child that never reads stdin. - Add a Hypothesis property test asserting `_handle_stream_timeout` upholds its cleanup contract (timeout preserved, stdin writer cancelled, consumer outcomes mapped) across arbitrary task orderings. - Give the timeout-cleanup assertions descriptive failure messages. - Debugging plan: sentence-case the section headings and add a both-hypotheses-supported branch to the termination criteria. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Refine timeout consumers type and debugging-plan accuracy (#117) Address the second round of review feedback. - Narrow the `_wait_for_exit_code` `consumers` annotation to `tuple[asyncio.Task[str | None], ...]`. After the stdin writer is managed separately, no call site passes a `Task[None]`, so the union arm was dead. Default and call-site behaviour are unchanged. - Debugging plan: state the observed `too_slow` health check without claiming the strategy caused it, and correct the H1 hypothesis to reflect that `_TAGS` is bounded and non-recursive (finite key set, bounded values, `max_size=3`). Replace `leta show _TAGS` (which needs an indexed leta workspace) with a repository-native `rg` command plus the exact focused `pytest` invocation and recorded seed, and add the missing comma between the two independent clauses in the recommended execution order. The new `_handle_stream_timeout` property test was measured at ~0.13s for its 75 examples (the investigated slow test runs in ~0.19s); it lives in an unrelated module and shares no strategies or fixtures with the `ctx_tags` generation under investigation, so it does not add to that wall-clock cost and was left unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Bundle stream-timeout property inputs into a case dataclass (#117) Resolve the CodeScene "Excess Number of Function Arguments" finding on `test_handle_stream_timeout_upholds_invariants_across_orderings` by folding its six generated `@given` arguments into a single frozen `_StreamTimeoutCase` scenario built with `st.builds`. The per-field generation ranges, settings, and every assertion (stdin cancellation, consumer draining, exception-to-None mapping, and timeout preservation) are unchanged, so property-test coverage is preserved. No production code changes and no CodeScene suppression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Share stdin-writer cleanup in stream timeout handler (#117) Address the third round of review feedback. - `_handle_stream_timeout` now delegates to the shared `_cancel_stdin_writer(stdin_task)` helper instead of duplicating the inline cancel-and-tolerant-gather logic, keeping the stdin-task lifecycle identical across the timeout and cancellation paths. - Debugging plan: re-wrap the H1 claim so every prose line stays within 80 columns, name the skipped `leta workspace add` indexing step in inline code (replacing the vague "workspace-indexing prerequisite" wording), and name the generated argument `ctx_tags` in the H1 ordering rationale so it explicitly identifies why H1 is the cheapest decisive check. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Cancel pending stream consumers on subprocess timeout (#117) Complete the `_wait_for_exit_code` timeout-cleanup finding for the stream path. - Restore the `consumers` annotation to `tuple[asyncio.Task[None] | asyncio.Task[str | None], ...]` so the parameter accepts a blocking cleanup task as well as stdout/stderr readers. - On `TimeoutError`, cancel any consumer still pending after `_terminate_process` before draining it, so a reader wedged on a pipe that never reached EOF cannot make timeout handling hang. Finished readers keep their captured output. Factor the guarded cancel loop into `_cancel_pending_consumers` and reuse it from both the timeout and cancellation branches, keeping them consistent (cancelling an already-done task is a no-op, so behaviour is unchanged). - Add a focused regression test that drives `_wait_for_exit_code` into its timeout-cleanup branch with a blocking consumer and asserts the consumer is cancelled and drained while the original `TimeoutError` propagates. The direct-mode stdin handling and its blocked-writer regression test are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Cover the cancellation cleanup path in timeout tests (#117) Address the fifth round of review feedback (test-only). - Add `test_wait_for_exit_code_cancels_pending_consumers_on_cancellation`, which runs `_wait_for_exit_code` in a task, cancels it while a consumer is still pending, and asserts that `asyncio.CancelledError` propagates and the consumer is cancelled and drained. This covers the cancellation cleanup branch distinctly from the existing timeout test. - Give the `_TimeoutWaitProcess.wait` assertion a diagnostic message spelling out the process-double invariant (terminate/kill must record `returncode` before `_exited` is set); wait/return behaviour is unchanged. - Raise `ValueError` rather than `RuntimeError` from the property test's consumer helper when its outcome is "raise"; delay and return behaviour are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Cover streamed-run cancellation cleanup end-to-end (#117) The `_wait_for_exit_code` cancellation branch was only exercised at the unit level (a direct call with a fake process) and via direct-mode (capture=False) cancellation tests; no test cancelled an in-flight streamed run. Add `test_streamed_run_cancellation_cleans_up_task`, which cancels a running `command.run(output=RunOutputOptions(capture=True))` mid-flight. Output capture routes execution through `_run_subprocess_with_streams`, so this drives the `CancelledError` cleanup with real stdout/stderr consumer tasks and asserts the run tears down within a bounded time rather than deadlocking on a pending reader. It complements the unit test `test_wait_for_exit_code_cancels_pending_consumers_on_cancellation`, which asserts the precise consumer cancel/drain state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Extract SafeCmd stdin lifecycle tests into their own module (#117) Address the CodeScene "Lines of Code in a Single File" finding on `test_safe_cmd_run.py` with a cohesive test-module extraction. - Add `cuprum/unittests/test_safe_cmd_stdin.py` holding the stdin writer lifecycle regression group moved verbatim (assertions, commands, payload size, timeout values, and async/sync coverage unchanged): `test_stdin_input_with_timeout_escalation`, `test_direct_timeout_with_blocked_stdin_writer_does_not_hang`, and `test_stdin_input_cancellation_cleans_up_task`. The module carries only the imports and local helpers (`_execute_async`/`_execute_sync`, a local `python_builder` fixture) those tests need; `collections.abc` is imported under `TYPE_CHECKING` since it is annotation-only here. - Remove those tests from `test_safe_cmd_run.py`, retaining `test_streamed_run_cancellation_cleans_up_task` (stream-consumer cleanup via captured execution) and the shared runtime helpers/imports still used there. - Refresh the maturin wheel-build snapshot for the new module. No production subprocess code changed and no CodeScene suppression added; timeout, blocked-drain, cancellation, and execution-strategy coverage are preserved. `test_safe_cmd_run.py` drops from 635 to 565 non-blank, non-comment lines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Strengthen cancellation coverage and tighten subprocess typing (#117) Address the seventh round of review feedback plus the failing Testing / Concurrency checks. Production: - Narrow the `consumers` annotation on `_cancel_pending_consumers` and `_wait_for_exit_code` to `tuple[asyncio.Task[str | None], ...]`, matching what `_spawn_stream_consumers` actually returns; stdin remains handled separately by `_cancel_stdin_writer`. - Merge the now-identical `except TimeoutError` and `except asyncio.CancelledError` branches of `_wait_for_exit_code` into a single `except (TimeoutError, asyncio.CancelledError)` clause; termination, consumer cancellation, tolerant gather, and bare re-raise are unchanged. - Replace the bare `RuntimeError` raised by `_require_timeout` with a package-scoped `_SubprocessInvariantError(RuntimeError)`, so the impossible- state guard is distinguishable from unrelated runtime failures while staying catchable as a `RuntimeError` (message and chaining preserved). Tests: - Cancellation regressions now assert real propagation with `pytest.raises(asyncio.CancelledError)` and `task.cancelled()` instead of suppressing `CancelledError`, in both the stdin (`run()`) and streamed-run tests, so they fail if `run()` ever swallows cancellation. - The stdin cancellation test now uses a child that never reads stdin plus a 1 MiB payload, so it exercises cleanup of a writer genuinely blocked in `drain()`. - The observe `stdin_error` test provokes a real EPIPE (child closes stdin + 1 MiB payload) instead of monkeypatching `_write_stdin`. - Type `_execute_async`/`_execute_sync` kwargs with a `_RunKwargs` TypedDict (no `Any`) and narrow `execution_strategy` to `Literal["async", "sync"]`; correct the module docstring (timeouts cover both strategies, cancellation only `run()`). - `fail_consumer` and the two `blocking_consumer` doubles are retyped/retargeted (`ValueError`; `-> str | None`) to match the above. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Restore import ordering after rebase (#117) The rebase onto main (CQRS refactor #118) left `from pathlib import Path` ahead of the plain `import` statements in test_safe_cmd_run.py after the stdin-test-removal conflict resolution. Reorder it below the stdlib imports to satisfy ruff's isort rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Renumber subprocess ADR to 007 after rebase (#117) The rebase onto main (#116/#157) introduced ADR-006 "Split cuprum/context.py into a context package", colliding with this branch's ADR-006 "Subprocess execution module boundaries". Main owns 006, so renumber the subprocess ADR to 007: rename the file and update every reference (contents.md, cuprum-design.md, developers-guide.md, and the ADR title). Both ADRs are retained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Reconcile stream consumers on stdin-writer failure and de-flake tests (#117) Address review feedback on the subprocess execution split: - _run_subprocess_with_streams: if `await stdin_task` raises an unexpected exception (or a cancellation lands on that await), cancel and drain the stdout/stderr consumer tasks before the error propagates, mirroring the timeout and cancellation cleanup paths, so the consumers are not abandoned. - Replace fixed-`asyncio.sleep` synchronisation hacks with deterministic readiness signals before cancellation: - test_streamed_run_cancellation_cleans_up_task waits for an observed stdout line via an observe hook. - test_stdin_input_cancellation_cleans_up_task waits for the stdin writer to begin (and wedge in drain) via a wrapped `_write_stdin`. - test_wait_for_exit_code_cancels_pending_consumers_on_cancellation awaits a new `wait_started` event on the `_TimeoutWaitProcess` double. - test_observe_emits_stdin_error_event_when_process_closes_stdin_early sizes its stdin payload from the probed pipe capacity so drain() is guaranteed to block, replacing the assumed ~64 KiB pipe-buffer race. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Drop unreproducible _TAGS timing step from H1 debug plan (#117) The H1 plan's Table 2 and prediction claimed a bounded, representative `_TAGS` timing measurement, but the documented tooling only replays the whole property test (generation plus observation construction) with no isolated sampling harness or acceptance threshold, and no such harness exists in the repo. Remove the unsupported "time representative samples" step and reconcile the prediction so the documented falsification is reproducible as written. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Cover streamed-path consumer reconciliation on stdin-writer failure (#117) Add a regression test for the streamed (capture) path: inject a failing stdin writer so `await stdin_task` raises, and assert the stdout/stderr consumer tasks are cancelled and drained rather than orphaned. The assertion runs inside the running loop (before asyncio.run tears it down and cancels leftovers) so it genuinely fails without the reconcile fix, closing the coverage gap flagged in review for the fix in 10d28c2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Split test_safe_cmd_run.py into cohesive modules (#117) test_safe_cmd_run.py was 828 lines, over the 400-line CodeScene "Lines of Code in a Single File" limit. Move tests into focused modules by concern, without changing any test behaviour, assertions, names, or async/sync coverage (the collected node-id set is unchanged): - test_safe_cmd_stdin.py (extended): stdin injection tests (text/bytes feeding, configured encoding, capture-disabled, early-close, and the forbidden-command vs stdin-encoding ordering contract) alongside the existing stdin writer lifecycle regressions. Adds a local `execution_strategy` fixture. - test_safe_cmd_context.py (new): allowlist enforcement and before/after hook integration (FIFO/LIFO order, hook arguments, cancellation skipping after hooks). - test_safe_cmd_streams.py (new): captured-stream (`capture=True`) cleanup on cancellation and stdin-writer failure, preserving the monkeypatch targets. - test_safe_cmd_run.py (slimmed to 366 lines): general execution/output/env/ cwd/timeout coverage plus the non-cooperative-kill escalation test. Regenerate the native-wheel snapshot to package the two new test modules. No production code changed; no CodeScene suppression added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Parametrize duplicated stdin-feeding tests (#117) Resolve the CodeScene duplication finding in test_safe_cmd_stdin.py: the near-identical test_input_text_feeds_stdin and test_input_bytes_feeds_raw_stdin shared their whole control flow, differing only in the child script, the StdinInput payload, and the expected stdout. Replace them with a single parametrized test_input_feeds_stdin ("text" and "raw-bytes" cases), preserving both payloads, scripts, expected output, and the run()/run_sync() coverage via the existing execution_strategy fixture. test_input_text_uses_configured_encoding stays a separate test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Align coverage job Rust toolchain with MSRV 1.85.0 (#117) The coverage job pinned Rust 1.92.0 while the MSRV pin (rust-toolchain.toml), the lint-test job, and the typecheck-test job all use 1.85.0. Build coverage with the same toolchain as the rest of CI so the native extension and test suite are exercised under the MSRV. Only the toolchain version changes; the setup-rust action pin and all other CI configuration are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Address review feedback on subprocess stdin logging and test structure (#117) - _subprocess_stdin._emit_stdin_error: record the failing exception's traceback via `_LOGGER.error(..., exc_info=exc)` and drop the now-redundant inline exception format argument, preserving the context fields and the `stdin_error` observation event. (Ruff LOG004 forbids `.exception()` in this standalone helper, so the explicit `exc_info=exc` form is used.) Update the `test_write_stdin_observes_error_events` log assertion to check `caplog.text`, since the exception detail now lives in the traceback rather than the message. - Add `tests/helpers/execution.py` with the shared `_RunKwargs` TypedDict and `ExecuteFn` alias; import them in test_safe_cmd_stdin.py and test_safe_cmd_context.py instead of duplicating the definitions. - test_safe_cmd_stdin.py: parameterise `test_input_feeds_stdin` with a frozen `_StdinFeedCase` dataclass (3 params); drop the unused `python_builder`/ `execution_strategy` fixtures from `test_input_text_and_input_bytes_conflict`; switch the four string-parametrized tests to the `execution_strategy` fixture; add diagnostic messages to the bare assertions. - test_safe_cmd_context.py: add diagnostic messages to every bare assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Tidy ADR index and subprocess module-boundary docs (#117) - contents.md: add the missing ADR-005 and ADR-006 entries to the ADR index, between ADR-004 and ADR-007, in the established reference-link format. - cuprum-design.md: move the "8.1.5 Subprocess execution module boundaries" subsection to after Figure 3's mermaid block so the figure caption and diagram stay together and 8.1.5 closes section 8.1. - developers-guide.md: replace the duplicated lifecycle-boundary preface with a pointer to cuprum-design.md §8.1.5 and ADR-007, and move the maintainer placement guidance under its own "Subprocess execution module boundaries" heading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Make the stdin failure path diagnosable in traces (#117) The `stdin_error` transition emitted by `_emit_stdin_error` was surfaced to metrics (a counter) and logs, but the tracing hook grouped it with `plan`/`stdin` and ignored it, so a stdin write/close failure left no trace, and the event carried no stable operation/error-type fields (operation was dropped entirely; error type was only embedded in the free-text `note`). - events.py / _pipeline_types.py: add optional `operation` and `error_type` fields to the public `ExecEvent` and internal `_EventDetails`, wired through `_StageObservation.emit`. - _subprocess_stdin._emit_stdin_error: populate `operation` (write/close) and `error_type` on the emitted `stdin_error` event. (Logging already records the exception traceback via `exc_info` and these fields via `extra`.) - tracing_adapter: record `stdin_error` as a `cuprum.stdin_error` span event (correlated by exec_id) carrying operation/error_type/note, leaving the span open and unmarked since the failure is non-fatal. Consolidate the near-identical output and stdin-error span-event handlers into one `_record_span_event`, keeping the module under the 400-line limit. - Tests: update the stdin_error observe expectations, extend the shared event factory to forward the new fields, and add a tracing regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Document the tracing adapter phase-dispatch and span-event patterns (#117) The tracing adapter recently consolidated its two near-identical span-event handlers into one `_record_span_event` and added a `stdin_error` phase, leaving the developers' guide out of date. Update the "Tracing adapter span lifecycle" section to standardise and document the patterns: - Phase-dispatch policy: every ExecEvent phase falls into one of four categories (span lifecycle, span event, deliberately ignored, unhandled); new phases slot into this policy rather than an ad-hoc side path. - One span-event recorder: stdout/stderr/stdin_error all route through `_record_span_event`, which copies whichever of line/operation/error_type/note are set onto a `cuprum.<phase>` event; new recording phases extend the shared field set instead of adding a bespoke method. - Non-fatal events (stdin_error) are recorded but leave the span open and unmarked; only `exit` ends the span. - `record_output` gates stdout/stderr but not stdin_error, so a stdin failure stays diagnosable when line output recording is off. Docs-only; no code change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: leynos <leynos@rohga> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
This branch splits the 763-line
cuprum/context.pyinto acuprum/context/package with every module under 400 lines, preserving the public API surface unchanged.Closes #116.
The module mixed four concerns with different audiences and change cadences. The split follows the seams named in the issue:
merge_env_overlays,resolve_env,_coerce_env_overlay); noContextVardependency.CuprumContext,ScopeConfig,ForbiddenProgramError, timeout validation, hook type aliases.ContextVarmachinery.scoped, the_TokenRegistrationbase, the handles, and the factories.__all__unchanged.Review walkthrough
__init__surface first, then skim the per-module imports.Validation
make check-fmt: passmake lint: pass (no module over 400 lines; largest isregistration.pyat 372)make typecheck: passmake test: pass (727 passed, 50 skipped; the public-API, context, env-overlay, and stateful registration suites pass unchanged; Rust suite 4 passed)make markdownlint: passcoderabbit review --agent: invoked after the final push; the CLI began analysis but returned no terminal findings reportNotes
This branch is stacked on #156 (#113) per the issue's coordination note, so
registration.pylands already de-duplicated; it should be rebased once #156 merges. The wheel-build snapshot reflects the new file list.References