feat(story-1.6): MsgspecDecoder via the [msgspec] extra#11
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
src/httpware/decoders/msgspec.pywithMsgspecDecoder, the secondResponseDecoderadapter. Backed bymsgspec.json.decode(content, type=model)— single C-level parse pass, no adapter caching needed (msgspec doesn't have pydantic'sTypeAdapteroverhead).src/httpware/_internal/import_checker.pywithis_msgspec_installed(find_spec-based detection — does NOT import msgspec). Pattern adapted frommodern-python/lite-bootstrap. Future opt-in extras (otel in Story 5-4, etc.) extend this module.MsgspecDecoder()construction raisesImportErrorwith the install hint. Enables capability-probe code.from httpware.decoders.msgspec import MsgspecDecoder.tests/test_optional_extras_isolation.py, which future opt-in extras will extend).Note on archive AC correction: The archived epic spec claimed msgspec decodes pydantic
BaseModelnatively. It doesn't (msgspec acceptsmsgspec.Struct, dataclasses, attrs, NamedTuples, TypedDicts, and builtins). The Task 2 implementer's pydantic-fallback workaround was reverted in5f647c4— pydantic users continue to usePydanticDecoder. The CHANGELOG entry reflects the corrected supported-types list.Out of scope (subsequent stories):
AsyncClientwiring (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-ciclean.tests/test_no_httpx2_leakage.pystill passes.tests/test_optional_extras_isolation.py::test_importing_httpware_does_not_import_msgspecpasses — subprocess verifiesimport httpwaredoes not loadmsgspec.🤖 Generated with Claude Code