Skip to content

feat(story-1.6): MsgspecDecoder via the [msgspec] extra#11

Merged
lesnik512 merged 8 commits into
mainfrom
story/1-6-msgspec-decoder-via-extras
May 31, 2026
Merged

feat(story-1.6): MsgspecDecoder via the [msgspec] extra#11
lesnik512 merged 8 commits into
mainfrom
story/1-6-msgspec-decoder-via-extras

Conversation

@lesnik512

@lesnik512 lesnik512 commented May 31, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds src/httpware/decoders/msgspec.py with MsgspecDecoder, the second ResponseDecoder adapter. Backed by msgspec.json.decode(content, type=model) — single C-level parse pass, no adapter caching needed (msgspec doesn't have pydantic's TypeAdapter overhead).
  • Adds src/httpware/_internal/import_checker.py with is_msgspec_installed (find_spec-based detection — does NOT import msgspec). Pattern adapted from modern-python/lite-bootstrap. Future opt-in extras (otel in Story 5-4, etc.) extend this module.
  • Module import is safe without the extra — only MsgspecDecoder() construction raises ImportError with the install hint. Enables capability-probe code.
  • No package-root re-export. Honors seam feat(story-1.5): ResponseDecoder protocol and pydantic adapter #5 ("never import an extra at package top-level"). Consumers use from httpware.decoders.msgspec import MsgspecDecoder.
  • 7 behavioral tests + 1 subprocess-based import-isolation test (in new tests/test_optional_extras_isolation.py, which future opt-in extras will extend).
  • 208 passing total; 100% coverage on the new modules.

Note on archive AC correction: The archived epic spec claimed msgspec decodes pydantic BaseModel natively. It doesn't (msgspec accepts msgspec.Struct, dataclasses, attrs, NamedTuples, TypedDicts, and builtins). The Task 2 implementer's pydantic-fallback workaround was reverted in 5f647c4 — pydantic users continue to use PydanticDecoder. The CHANGELOG entry reflects the corrected supported-types list.

Out of scope (subsequent stories): AsyncClient wiring (Story 1-7), RecordedTransport (Story 1-8), follow-up cleanup of legacy __all__ exports in existing submodules.

Spec + plan: docs/superpowers/specs/2026-05-31-msgspec-decoder-via-extras-design.md, docs/superpowers/plans/2026-05-31-msgspec-decoder-via-extras-plan.md.

Test plan

  • just test — 208 passed, 1 deselected, 100% line coverage including the new modules.
  • just lint-ci clean.
  • tests/test_no_httpx2_leakage.py still passes.
  • tests/test_optional_extras_isolation.py::test_importing_httpware_does_not_import_msgspec passes — subprocess verifies import httpware does not load msgspec.
  • CI green on all matrix entries (3.11/3.12/3.13/3.14 + lint).

🤖 Generated with Claude Code

lesnik512 and others added 8 commits May 31, 2026 19:09
Adapts the lite-bootstrap optional-dependency pattern to httpware:
- New _internal/import_checker.py uses find_spec to detect installed extras
  without importing them.
- New decoders/msgspec.py imports msgspec only if installed; MsgspecDecoder()
  raises ImportError with an install hint at construction time when missing.
- Module is import-safe without the extra (capability-probe support).
- No package-root re-export (honors seam #5 — import httpware doesn't
  eagerly load msgspec when the extra is installed).
- No caching layer: msgspec.json.decode is a free function with no
  per-model adapter overhead.
- msgspec.ValidationError / msgspec.DecodeError propagate unchanged.

Includes 8 behavioral tests plus a subprocess-based import-isolation
test that future opt-in extras (otel) will extend.

Strict epic boundary — AsyncClient wiring (1-7), RecordedTransport (1-8)
land separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…a detection

Adds is_msgspec_installed flag computed once at module import time via
importlib.util.find_spec. No actual import of msgspec happens — only
the importlib check. Future opt-in extras (otel in Story 5-4, etc.)
extend this module with their own flags.

Pattern adapted from modern-python/lite-bootstrap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds src/httpware/decoders/msgspec.py with:
- MISSING_DEPENDENCY_MESSAGE constant (module-level, not class attribute)
- MsgspecDecoder class: __init__ raises ImportError with install hint
  if msgspec isn't installed; decode() calls msgspec.json.decode(
  content, type=model) in a single C-level parse pass.

The msgspec import is gated by import_checker.is_msgspec_installed,
so the module imports cleanly without the extra — only construction
fails. No __all__ (project convention). No caching (msgspec.json.decode
is a free function with no per-model adapter overhead). Pydantic
BaseModel types are detected via hasattr and routed to
model_validate_json as msgspec cannot natively decode them.

Eight tests cover: protocol satisfaction, decode-into-Struct,
decode-into-pydantic-model, decode-into-builtin, decode-into-list,
ValidationError propagation, DecodeError propagation, and the
construction-failure path via monkeypatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The spec inherited a wrong assumption from the archived AC: msgspec
does NOT decode into pydantic BaseModel subclasses natively (only
msgspec.Struct, dataclasses, attrs, NamedTuples, TypedDicts, and
builtins). The initial Task 2 implementation worked around this with
a hasattr(model, "model_validate_json") fallback, but that muddies
MsgspecDecoder's responsibility, defeats the single C-level pass
property, and crosses ecosystems.

Revert: MsgspecDecoder.decode is now a one-liner that just calls
msgspec.json.decode(content, type=model). Pydantic users use
PydanticDecoder. The test for pydantic decoding is dropped; the
remaining 7 tests cover msgspec.Struct, builtin, list[Struct],
ValidationError, DecodeError, protocol satisfaction, and the
missing-extra construction path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…xtras

Verifies that `import httpware` does not transitively load msgspec. msgspec
is installed in the test env (via --all-extras), so the check runs in a
fresh subprocess with a clean sys.modules. Future stories (5-4 otel) extend
this file with their own subprocess tests for each opt-in extra.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…test

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@lesnik512 lesnik512 self-assigned this May 31, 2026
@lesnik512 lesnik512 merged commit 4117384 into main May 31, 2026
5 checks passed
@lesnik512 lesnik512 deleted the story/1-6-msgspec-decoder-via-extras branch May 31, 2026 16:26
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