Skip to content

Extract _DecoderResolver for Seam B#77

Merged
lesnik512 merged 5 commits into
mainfrom
refactor/decoder-resolver-extraction
Jun 23, 2026
Merged

Extract _DecoderResolver for Seam B#77
lesnik512 merged 5 commits into
mainfrom
refactor/decoder-resolver-extraction

Conversation

@lesnik512

Copy link
Copy Markdown
Member

Deepening candidate #3 from the architecture review.

Problem

The decoder seam (Seam B) is real — pydantic + msgspec adapters — but the logic for using it was copy-pasted across four send/send_with_response methods on the two clients (client.py:185, 213, 1200, 1228): walk _decoders, raise MissingDecoderError(registered_names=…) pre-flight if none claims the model, then decoder.decode(...) wrapping failures as DecodeError. Plus _dispatch_decoder defined twice.

Change

One _DecoderResolver (+ generic _BoundDecoder) in a new decoders/_resolver.py owns it:

  • resolve(model) -> _BoundDecoder[T] — first can_decode wins; none → MissingDecoderError (pre-flight, before the HTTP call).
  • _BoundDecoder.decode(response) -> T — wraps except Exception as DecodeError, with the decoder + model sealed together so a mismatch is unrepresentable.

The four sites collapse to resolve → dispatch → bound.decode. Both clients keep self._decoders (tested attribute) and gain self._decoder_resolver; both _dispatch_decoder methods removed. Fully synchronous → one resolver serves both clients. Behaviour byte-identical.

Tests

  • Existing decoder/client suites stay green (parity net).
  • New tests/test_decoder_resolver.py drives resolve/bound.decode directly: resolution, MissingDecoderError with registered_names + ordering, first-match-wins, DecodeError wrapping.

Verification

  • just lint clean (ruff + ty), just test → 723 passed, 100% coverage
  • grep -rE 'httpx2\._' src/httpware/ clean

Design bundle: planning/changes/2026-06-23.02-decoder-resolver-extraction/. Promoted into architecture/decoders.md.

🤖 Generated with Claude Code

lesnik512 and others added 5 commits June 23, 2026 15:40
Full-lane design.md + plan.md for extracting a _DecoderResolver (+ generic
_BoundDecoder) behind Seam B, collapsing the 4-site resolve/raise/decode/
wrap smear in client.py. Design only — no source changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the 4-site resolve/raise/decode/wrap smear in client.py behind one
_DecoderResolver (+ generic _BoundDecoder) in decoders/_resolver.py. resolve()
returns a _BoundDecoder sealing decoder+model together; the four send/
send_with_response sites collapse to resolve -> dispatch -> bound.decode.
Both clients keep self._decoders and gain self._decoder_resolver; both
_dispatch_decoder methods removed. Behaviour byte-identical.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Direct decision-matrix tests for the resolver — no client, no MockTransport:
resolve->bound->decode, no-claimer MissingDecoderError with registered_names
+ ordering, empty-tuple names, first-match-wins, and decode failure wrapped
as DecodeError with response/model/original/__cause__.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update Seam B contract in architecture/decoders.md: resolution + pre-flight
MissingDecoderError live in _DecoderResolver.resolve; DecodeError wrapping in
_BoundDecoder.decode. Replaces the _dispatch_decoder / send-wraps framing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 merged commit b084e6c into main Jun 23, 2026
5 checks passed
@lesnik512 lesnik512 deleted the refactor/decoder-resolver-extraction branch June 23, 2026 13:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant