Skip to content

Phase 2 of the API redesign: readers are a real type - #60

Open
kelsayed17 wants to merge 22 commits into
mainfrom
claude/api-redesign-phase-2-b84ff6
Open

kelsayed17 wants to merge 22 commits into
mainfrom
claude/api-redesign-phase-2-b84ff6

Conversation

@kelsayed17

Copy link
Copy Markdown
Owner

Phase 2 of the public API redesign: section 2, Readers, of the design spec. The plan is specs/2026-11-05-phase-2-readers.md.

A reader is now a real type. A wrong expect= fails twice: it is a type error, and it raises TypeError before anything is sent. The builders for readers and writers now share one vocabulary. The two refusal checks are split by what each one sees. A body nested too deeply for its parser now raises a shape error, where it used to raise a raw RecursionError.

Breaking changes

Before After
Reader was a type alias, so any callable worked as expect= Reader is a final class. A hand-written reader is Reader(fn), or @Reader over a def
expect=str, expect=Model or a bare function was accepted Raises TypeError (expect= takes a Reader or an Expect (got the class str)). Wrap it in Reader(...) or use a builder
Expect(ok), answers=, Expect.on and cache.conditional took any callable Each refuses a non-Reader with TypeError
Expect(ok, {404: a, 409: b}) with answers of different types inferred their union Chain .on(), or spell out Expect[A | B | None]
decoder_for(codec, T, refusal_check=...) reader_for(codec, T, refuse=...)
encoder_for(codec, T) writer_for(codec, T)
headgate.msgspec.decoder / .encoder, and pydantic's headgate.msgspec.reader / .writer, and pydantic's
body_reader(decode, refusal_check=...) body_reader(decode, refuse=...)
json_reader(decode, allow_empty_body=True) body_reader(decode, empty=b"[]")
json_writer A function that returns EncodedBody
HttpOptions(refusal_check=...) HttpOptions(response_check=...)
RefusalCheck ResponseCheck
from headgate import RefusedError from headgate.errors import RefusedError

Added

  • as_json, a ready Reader[object] built on the standard library's json. It accepts only strict RFC 8259 JSON. It refuses non-UTF-8 bodies, a BOM, UTF-16, NaN/Infinity, numbers out of range and lone surrogates, each with a DecodeError that never includes the body.
  • A Reader's repr names what it reads, so Expect(...) prints something useful.

Fixed

  • A body nested too deeply for its parser now raises ResponseParseError, or the status's error on a declared non-2xx. Before, it raised a raw RecursionError, whichever reader read it. Every reader reports it as nested too deeply.

Verification

  • ruff check, ruff format --check, ty check and typos are clean.
  • pytest passes 2345 tests at 100% line and branch coverage, and all 58 snapshots pass.
  • The strict docs build passes.
  • The API snapshot diff was reviewed for each task.

Each task was reviewed against the plan, and the whole branch was reviewed once more at the end. That final review's fixes are in the last commit.

🤖 Generated with Claude Code

kelsayed17 and others added 21 commits November 5, 2026 21:08
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ting line numbers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iance note

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ct, the snapshot renders readers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… finds annotations, honest chaining checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, paginate checks expect= first, coverage of as_json

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…urrogates, docstrings current per task

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ate walk, and the last stale references

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ery codec reader, and finish the sweeps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r for every reader

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…urrent shape-error prose, tests first

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eadgate.errors

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codspeed

codspeed Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 44 untouched benchmarks


Comparing claude/api-redesign-phase-2-b84ff6 (c305d39) with main (afaea04)

Open in CodSpeed

From Python 3.14, json.loads and msgspec recurse until the real C stack
runs out, so whether 100_000 levels overflow depends on the platform:
on the ubuntu and macos runners they parsed, and three tests failed with
coverage at 99%. The tests now make the parse raise RecursionError
mid-decode (a json.loads parse_float hook, a msgspec __post_init__, a
mint that raises it), so they pass the same way everywhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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