Skip to content

Commit 2c82f7f

Browse files
committed
feat(client): add _build_default_decoders helper for installed-extras probe
1 parent a46b72a commit 2c82f7f

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

‎src/httpware/client.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,27 @@ def _default_pydantic_decoder() -> ResponseDecoder:
4545
return PydanticDecoder()
4646

4747

48+
def _build_default_decoders() -> tuple[ResponseDecoder, ...]:
49+
"""Construct the default decoder tuple based on installed extras.
50+
51+
Pydantic-first when both extras are present; either-only when only one is
52+
installed; empty tuple when neither is installed. Imports the concrete
53+
decoder modules lazily so missing extras never trip `find_spec`-guarded
54+
import paths. Called by `AsyncClient.__init__` and `Client.__init__` when
55+
`decoders=None` (the default).
56+
"""
57+
decoders: list[ResponseDecoder] = []
58+
if import_checker.is_pydantic_installed:
59+
from httpware.decoders.pydantic import PydanticDecoder # noqa: PLC0415 — lazy by design (Seam C)
60+
61+
decoders.append(PydanticDecoder())
62+
if import_checker.is_msgspec_installed:
63+
from httpware.decoders.msgspec import MsgspecDecoder # noqa: PLC0415 — lazy by design (Seam C)
64+
65+
decoders.append(MsgspecDecoder())
66+
return tuple(decoders)
67+
68+
4869
@contextlib.asynccontextmanager
4970
async def _httpx2_exception_mapper() -> AsyncIterator[None]:
5071
"""Map httpx2 exceptions to httpware exceptions. Shared by AsyncClient._terminal and stream()."""

‎tests/test_client_construction.py‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
"""Tests for AsyncClient construction and ownership semantics."""
22

3+
from unittest.mock import patch
4+
35
import httpx2
46
import pytest
57

68
from httpware import AsyncClient
9+
from httpware.client import _build_default_decoders
10+
from httpware.decoders.msgspec import MsgspecDecoder
711
from httpware.decoders.pydantic import PydanticDecoder
812

913

@@ -90,3 +94,38 @@ async def __call__(self, request, next) -> httpx2.Response: # noqa: A002, ANN00
9094
client = AsyncClient(middleware=(_Tag(),))
9195
assert client._user_middleware == (client._user_middleware[0],) # noqa: SLF001
9296
assert len(client._user_middleware) == 1 # noqa: SLF001
97+
98+
99+
def test_build_default_decoders_both_extras_installed() -> None:
100+
result = _build_default_decoders()
101+
assert len(result) == 2 # noqa: PLR2004
102+
assert isinstance(result[0], PydanticDecoder)
103+
assert isinstance(result[1], MsgspecDecoder)
104+
105+
106+
def test_build_default_decoders_pydantic_only() -> None:
107+
with patch("httpware._internal.import_checker.is_msgspec_installed", False):
108+
result = _build_default_decoders()
109+
assert len(result) == 1
110+
assert isinstance(result[0], PydanticDecoder)
111+
112+
113+
def test_build_default_decoders_msgspec_only() -> None:
114+
with patch("httpware._internal.import_checker.is_pydantic_installed", False):
115+
result = _build_default_decoders()
116+
assert len(result) == 1
117+
assert isinstance(result[0], MsgspecDecoder)
118+
119+
120+
def test_build_default_decoders_neither_installed() -> None:
121+
with (
122+
patch("httpware._internal.import_checker.is_pydantic_installed", False),
123+
patch("httpware._internal.import_checker.is_msgspec_installed", False),
124+
):
125+
result = _build_default_decoders()
126+
assert result == ()
127+
128+
129+
def test_build_default_decoders_returns_tuple() -> None:
130+
result = _build_default_decoders()
131+
assert isinstance(result, tuple)

0 commit comments

Comments
 (0)