Skip to content

Commit 3ec5690

Browse files
committed
docs: fix stale comments and cross-doc contradictions from audit
BulkheadFullError docstring named AsyncBulkhead specifically though both bulkheads raise it; AsyncClient.stream()'s "for v1" caveat contradicted the sync docstring and architecture/client.md; architecture/errors.md misstated where DecodeError wrapping happens (inline in send() rather than _BoundDecoder.decode); docs/testing.md said "httpx" where it meant the httpx2 package.
1 parent 60f7072 commit 3ec5690

6 files changed

Lines changed: 186 additions & 4 deletions

File tree

architecture/errors.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The error-mapping table (what `httpx2` exception maps to which `httpware` except
1414

1515
`TimeoutError` inherits from both `httpware.ClientError` and `builtins.TimeoutError` so `except builtins.TimeoutError` (the form `asyncio.wait_for` uses) also catches httpware-raised timeouts.
1616

17-
`DecodeError` covers the case where `response_model=` is set, the HTTP call itself succeeded, but the active `ResponseDecoder` raised. The wrap happens at the seam in `Client.send` / `AsyncClient.send``except Exception` translates any decoder-side failure into `DecodeError(response=..., model=..., original=...)` with `raise ... from exc` chaining. The `original` attribute exposes the underlying library exception (e.g., `pydantic.ValidationError`, `msgspec.ValidationError`); `__cause__` carries the same reference.
17+
`DecodeError` covers the case where `response_model=` is set, the HTTP call itself succeeded, but the active `ResponseDecoder` raised. The wrap happens in `_BoundDecoder.decode` (`decoders/_resolver.py`)`except Exception` translates any decoder-side failure into `DecodeError(response=..., model=..., original=...)` with `raise ... from exc` chaining. The `original` attribute exposes the underlying library exception (e.g., `pydantic.ValidationError`, `msgspec.ValidationError`); `__cause__` carries the same reference.
1818

1919
The "no `__init__` override" rule scopes only to `StatusError` subclasses. Non-status `ClientError` subclasses — `DecodeError`, `MissingDecoderError`, `BulkheadFullError`, `RetryBudgetExhaustedError`, `CircuitOpenError`, `ResponseTooLargeError` — deliberately define `__init__` with keyword-only fields.
2020

docs/testing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ For middleware with state-keeping (counters, circuit-breaker state), assert on i
107107

108108
## Why not `respx`?
109109

110-
`httpware` deliberately uses `httpx2.MockTransport` instead of `respx` for its own tests. `MockTransport` is the public test seam in `httpx` — supported by the maintainers, stable across versions, lives in the public API surface. `respx` patches private internals and has historically broken across `httpx` major versions. Stick with `MockTransport` unless you have a specific reason not to.
110+
`httpware` deliberately uses `httpx2.MockTransport` instead of `respx` for its own tests. `MockTransport` is the public test seam in `httpx2` — supported by the maintainers, stable across versions, lives in the public API surface. `respx` patches private internals and has historically broken across `httpx2` major versions. Stick with `MockTransport` unless you have a specific reason not to.
111111

112112
## See also
113113

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# httpware docs & comments audit — 2026-07-13
2+
3+
**Status:** complete
4+
**Scope:** source-level docstrings/comments (`src/httpware/**/*.py`, 23 files) and
5+
the living documentation surface (`README.md`, `docs/**`, `architecture/**`).
6+
`planning/` was excluded — it is a kept historical record, not living truth,
7+
per the planning convention. This is the first audit to cover source comments
8+
and `architecture/` directly; the 2026-06-13 docs audit covered only the
9+
user-facing `docs/`/README surface and used `architecture/` purely as a
10+
reference.
11+
**Method:** two parallel first-pass agents (one over `src/`, on a cheap model;
12+
one over docs/architecture, on the standard model), each returning a compact
13+
candidate list. Every candidate was then independently re-verified against the
14+
real source before being written up here — several early candidates turned out
15+
correct on inspection and are recorded under Verified correct rather than as
16+
findings.
17+
18+
## Summary
19+
20+
- Stale/inconsistent comments or docstrings: **2** (both in `src/`)
21+
- Cross-doc contradictions: **4** (all in `docs/`/`architecture/`)
22+
- Duplication / compaction candidates: **3**
23+
24+
**Verdict on your compaction/dedup question:** worth doing, but narrowly — the
25+
`src/` comment surface is already lean (the first-pass sweep found zero
26+
comments that just restate obvious code; every comment/docstring earns its
27+
keep). The dedup opportunity is entirely on the docs side, and it's
28+
concentrated: `ResponseTooLargeError`'s behavior is spelled out near-verbatim
29+
in **three** places (D1) and the "why not respx" paragraph in two (D2, tangled
30+
up with the `httpx`/`httpx2` slip in I4) — both good candidates for "full
31+
account in one place, cross-reference from the rest," the same pattern
32+
`architecture/` already uses elsewhere. The `CircuitBreaker` overlap (D3) is a
33+
lower-priority, mostly-intentional tutorial-vs-reference depth split.
34+
35+
## Findings
36+
37+
### Stale/inconsistent (source)
38+
39+
**C1 — `errors.py:184``BulkheadFullError` docstring says "AsyncBulkhead" but the error is shared by both bulkheads.**
40+
Docstring: `"Raised when acquire_timeout elapses before an AsyncBulkhead slot becomes available."`
41+
Both `AsyncBulkhead.acquire` (`bulkhead.py:133`) and sync `Bulkhead.acquire`
42+
(`bulkhead.py:175`) raise it via the shared `_emit_bulkhead_rejected` helper
43+
(`bulkhead.py:50-68`) — it is not async-specific.
44+
*Fix:* drop "Async" — `"Raised when acquire_timeout elapses before a bulkhead slot becomes available."`
45+
**Resolved** (`2026-07-13.07`).
46+
47+
**C2 — `client.py:1037` vs `client.py:2009``stream()` docstrings disagree on whether the middleware-bypass is version-scoped.**
48+
`AsyncClient.stream()`: `"Bypasses the middleware chain (...) for v1 — see architecture/client.md for the contract."`
49+
`Client.stream()`: `"Bypasses the middleware chain (...) — matches AsyncClient.stream() behavior."` (no "for v1").
50+
`architecture/client.md:23` states the bypass as a permanent design choice ("Both bypass the middleware chain by design"), not a v1-only caveat — so the async docstring's "for v1" doesn't match the truth home either, and the two client docstrings say different things about the same shared behavior.
51+
*Fix:* drop "for v1" from the async docstring (or, if the bypass genuinely is meant to be revisited post-v1, say so in `architecture/client.md` too and mirror the caveat into the sync docstring).
52+
**Resolved** (`2026-07-13.07`) — "for v1" dropped; both client docstrings now agree with `architecture/client.md`.
53+
54+
### Cross-doc contradictions
55+
56+
**I1 — `architecture/errors.md:17` misstates where `DecodeError` wrapping happens.**
57+
`errors.md`: `"The wrap happens at the seam in Client.send / AsyncClient.send — except Exception translates any decoder-side failure into DecodeError(...)."`
58+
Verified against source: the `try/except Exception: raise DecodeError(...)` lives in `_BoundDecoder.decode` (`decoders/_resolver.py:38-43`), called from `client.py` as `bound.decode(response)` — not inline in `send`. `architecture/decoders.md:12` already states this correctly ("Any exception is wrapped by `_BoundDecoder.decode`"). The two truth-home files contradict each other on the same fact.
59+
*Fix:* correct `errors.md:17` to point at `_BoundDecoder.decode` (`decoders/_resolver.py`), matching `decoders.md`.
60+
**Resolved** (`2026-07-13.07`).
61+
62+
**I2 — `docs/dev/contributing.md:28` vs `architecture/conventions.md:24-25` — contradictory docstring requirement.**
63+
`contributing.md`: `"Module docstrings are required; per-method docstrings only when types alone are insufficient."` (conditional)
64+
`conventions.md`: `"Module / class / public-method docstrings are required ..."` (unconditional)
65+
*Fix:* pick one policy and make both files say it — recommend keeping `conventions.md`'s unconditional wording since it's the capability truth home, and updating `contributing.md` to match.
66+
67+
**I3 — `docs/dev/contributing.md:32-34` understates what CI machine-checks, vs `architecture/overview.md:9`.**
68+
`contributing.md`: `"The CI lint pass (...) catches what the linters can see (e.g. print() via ruff T201); the rest are enforced in code review."` — reads as "only `print()` is machine-checked."
69+
`overview.md`: documents two more checks contributing.md omits — `PGH003` (blanket `# type: ignore`) is machine-checked, and `SLF001` partially checks the `httpx2._` ban (attribute access, not import).
70+
This is a fresh drift, not a re-flag of the 2026-06-13 audit's I1 (that finding was about a since-removed "CI grep gates" phrase, already fixed) — `overview.md`'s finer breakdown was apparently added after `contributing.md`'s wording was last touched.
71+
*Fix:* replace `contributing.md`'s "the rest are enforced in code review" with the same three-tier breakdown `overview.md` uses (machine-checked / partially-checked / review-only), or have it link to `overview.md` instead of restating.
72+
73+
**I4 — `docs/testing.md:110` says `httpx` where it means `httpx2`.**
74+
`"MockTransport is the public test seam in httpx — supported by the maintainers, stable across versions ... respx patches private internals and has historically broken across httpx major versions."`
75+
Every other reference in the same file (line 3) and in `architecture/testing.md:4` correctly says `httpx2` — a real, separate PyPI package (`httpx2>=2.0.0,<3.0`, maintained by Pydantic Services Inc.), not an alias for the original `httpx`. This line reads as a leftover phrase from before the `httpx2` rename, and as written it inaccurately implies `respx`'s breakage history is about `httpx2` specifically.
76+
*Fix:* change both `httpx` occurrences on that line to `httpx2` (verify against `respx`'s actual `httpx2` support status if this section is touched — it may be that `respx` doesn't support `httpx2` at all, which is a stronger reason to use `MockTransport` than "it breaks across versions").
77+
**Resolved** (`2026-07-13.07`) — both occurrences corrected to `httpx2`; the underlying "does respx support httpx2 at all" question is left as-is, not part of this mechanical fix.
78+
79+
### Duplication / compaction candidates
80+
81+
**D1 — `ResponseTooLargeError` behavior is spelled out near-verbatim in three files.**
82+
`docs/errors.md:193-204`, `architecture/errors.md:23`, and `architecture/client.md:36` all restate the same handful of facts (status-agnostic, counts decoded bytes, fires from the non-streaming terminal and `stream()`'s error pre-read but not user-driven iteration, the `"declared"`/`"streamed"` reason split, "neither StatusError, NetworkError, nor TimeoutError — not retried, doesn't count toward the circuit breaker") in matching or near-matching phrasing. `docs/errors.md` has the fullest account.
83+
*Suggest:* keep the full account in `docs/errors.md` (or `architecture/errors.md`, whichever is meant to be canonical for this fact), compress the other two to a one-line cross-reference.
84+
85+
**D2 — "why not respx" is duplicated between `docs/testing.md:108-110` and `architecture/testing.md:4`.**
86+
Same argument, ~3 near-identical sentences in each. Bundle this cleanup with the I4 fix (same lines) — reconcile the `httpx`/`httpx2` wording and de-duplicate the reasoning in the same edit, keeping the fuller version in one file.
87+
88+
**D3 — `CircuitBreaker` states/failure-classification/rate-mode overlap between `docs/resilience.md:158-212` and `architecture/resilience.md:17,21`.** Lower priority: this is mostly a legitimate tutorial-vs-compressed-reference depth split, not verbatim duplication, but several exact clauses ("4xx including 429 count as successes," the `window_seconds=30.0`/`minimum_calls=20` defaults) are copied rather than merely covering the same ground. Worth a light pass if `resilience.md` is next revised for another reason — not worth a dedicated change on its own.
89+
90+
## Verified correct (negative results)
91+
92+
- **`src/` comment/docstring redundancy:** zero comments found that merely restate what well-named code already makes obvious — every inline comment explains a non-obvious constraint or invariant (e.g. wire-body header stripping, coverage pragmas, semaphore behavior).
93+
- **README ↔ `docs/index.md` duplication (regression check):** the ~70% prose duplication fixed in change `2026-06-14.01` has **not** crept back. The only verbatim overlap remaining is short, non-prose boilerplate (the Pre-1.0 status line, the "Part of `modern-python`" footer) — not the capability-description duplication the prior fix targeted. The install-extras blocks differ in *coverage* (README documents the `[all]` extra, `docs/index.md` doesn't) rather than in duplicated content.
94+
- Terminology elsewhere (`AsyncMiddleware`/`Middleware`, `AsyncNext`/`Next`, "terminal", "Seam A/B/C", phase-decorator names) is used consistently across every file checked — I4 is an isolated slip, not a pattern.
95+
96+
## Spawned changes
97+
98+
- **`2026-07-13.07-docs-comments-audit-fixes`** (lightweight) — fixes C1, C2,
99+
I1, I4. Verified against source; `just lint-ci`, `mkdocs build --strict`,
100+
and `just test` (780 passed, 100% coverage) all clean.
101+
102+
## Deferred / next steps
103+
104+
- **Needs your call:** I3's fix direction (restate the three-tier breakdown in
105+
`contributing.md`, or replace it with a link to `overview.md`) and I2's
106+
policy choice (unconditional vs conditional method-docstring requirement) —
107+
both are wording contradictions where either side could be "the fix,"
108+
not a clear code-vs-doc mismatch. Not yet scheduled.
109+
- **Compaction:** D1 (`ResponseTooLargeError` triplication) and D2 (bundled
110+
with I4's `httpx`/`httpx2` fix — the "why not respx" duplication itself
111+
wasn't touched by `2026-07-13.07`, only the terminology slip within it) are
112+
still open. D3 is a defer-until-touched item, not worth its own change.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
summary: Fixed 4 verified stale/contradictory comments and docs from the docs-and-comments audit — BulkheadFullError docstring, stream() version caveat, DecodeError wrap-site claim, testing.md httpx/httpx2 slip.
3+
---
4+
5+
# Change: Fix mechanical findings from the docs-and-comments audit
6+
7+
**Lane:** lightweight — touches 4 files (above the usual ≤2 guard, but per the
8+
`2026-06-13.04` precedent, the file-count guard proxies *code* risk; these are
9+
mechanical corrections whose design thinking already lives in the audit). No
10+
new file, no public-API change. Spec is the audit, not a `design.md`.
11+
12+
Spec: [`planning/audits/2026-07-13-docs-comments-audit.md`](../../audits/2026-07-13-docs-comments-audit.md)
13+
(findings C1, C2, I1, I4).
14+
15+
## Goal
16+
17+
Correct the four verified stale/contradictory comments and docs so source
18+
docstrings match the code they annotate and the `architecture/` truth home is
19+
internally consistent. Excludes I2 and I3 (wording contradictions needing a
20+
maintainer policy call, not a code-vs-doc mismatch) and D1/D2 (compaction —
21+
separate scope decision).
22+
23+
## Approach
24+
25+
Each edit is pinned to a finding verified against source in the audit:
26+
27+
- **C1** `src/httpware/errors.py``BulkheadFullError` docstring says
28+
"AsyncBulkhead slot"; the error is raised by both `Bulkhead.acquire` and
29+
`AsyncBulkhead.acquire` via the shared `_emit_bulkhead_rejected` helper.
30+
Drop "Async" from the docstring.
31+
- **C2** `src/httpware/client.py``AsyncClient.stream()`'s docstring says
32+
the middleware bypass is "for v1"; `Client.stream()`'s docstring has no such
33+
caveat, and `architecture/client.md:23` states the bypass as permanent
34+
("by design"). Drop "for v1" from the async docstring so both client
35+
docstrings agree with the truth home.
36+
- **I1** `architecture/errors.md` — states the `DecodeError` wrap happens
37+
"at the seam in `Client.send` / `AsyncClient.send`"; the actual
38+
`try/except Exception: raise DecodeError(...)` lives in
39+
`_BoundDecoder.decode` (`decoders/_resolver.py:38-43`), matching what
40+
`architecture/decoders.md:12` already says. Rewrite the sentence to point at
41+
`_BoundDecoder.decode`.
42+
- **I4** `docs/testing.md` — the "why not respx" paragraph says
43+
`"MockTransport is the public test seam in httpx"` and `"breaks across
44+
httpx major versions"`; every other reference in the file and in
45+
`architecture/testing.md:4` says `httpx2`, which is a distinct package
46+
(`httpx2>=2.0.0,<3.0`), not an alias for `httpx`. Change both `httpx`
47+
occurrences on that line to `httpx2`.
48+
49+
No `architecture/` promotion needed beyond I1 itself — these are corrections
50+
that bring stale prose back into line with the current truth home, not
51+
behavior changes.
52+
53+
## Files
54+
55+
- `src/httpware/errors.py` — C1 `BulkheadFullError` docstring
56+
- `src/httpware/client.py` — C2 `AsyncClient.stream()` docstring
57+
- `architecture/errors.md` — I1 `DecodeError` wrap-site correction
58+
- `docs/testing.md` — I4 `httpx``httpx2`
59+
60+
## Verification
61+
62+
- [x] Each edit matches its cited source line (C1↔`bulkhead.py:133,175`,
63+
C2↔`architecture/client.md:23`, I1↔`decoders/_resolver.py:38-43`,
64+
I4↔`architecture/testing.md:4` + `pyproject.toml:36`).
65+
- [x] `mkdocs build --strict` succeeds (no broken links/refs introduced).
66+
- [x] `just lint-ci` — clean.
67+
- [x] `just test` — 780 passed, 100% coverage.
68+
- [x] Final read-through — no residual "AsyncBulkhead slot" / "for v1" /
69+
"Client.send / AsyncClient.send" wrap claim / bare `httpx` on the
70+
respx line remains.

src/httpware/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1035,7 +1035,7 @@ async def stream( # noqa: PLR0913 — mirrors httpx2 per-method signatures; kwa
10351035
is closed when the context exits.
10361036
10371037
Bypasses the middleware chain (no AsyncRetry, no AsyncBulkhead, no user-installed
1038-
middleware) for v1 — see architecture/client.md for the contract.
1038+
middleware) — see architecture/client.md for the contract.
10391039
10401040
Auto-raises StatusError subclasses on 4xx/5xx (NotFoundError,
10411041
ServiceUnavailableError, etc.) — consistent with client.get()/post()/etc.

src/httpware/errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def __init__(
181181

182182

183183
class BulkheadFullError(_KeywordReduceMixin, ClientError):
184-
"""Raised when ``acquire_timeout`` elapses before an AsyncBulkhead slot becomes available.
184+
"""Raised when ``acquire_timeout`` elapses before a bulkhead slot becomes available.
185185
186186
Carries the configured caps for caller logging/alerting.
187187
"""

0 commit comments

Comments
 (0)