|
1 | 1 | """Unit tests for httpware.decoders.msgspec.MsgspecDecoder.""" |
2 | 2 |
|
| 3 | +import dataclasses |
3 | 4 | from http import HTTPStatus |
| 5 | +from unittest.mock import patch |
4 | 6 |
|
5 | 7 | import httpx2 |
6 | 8 | import msgspec |
| 9 | +import pydantic |
7 | 10 | import pytest |
8 | 11 |
|
9 | 12 | from httpware import AsyncClient, DecodeError |
10 | 13 | from httpware._internal import import_checker |
11 | 14 | from httpware.decoders import ResponseDecoder |
12 | | -from httpware.decoders.msgspec import MsgspecDecoder |
| 15 | +from httpware.decoders.msgspec import MsgspecDecoder, _get_msgspec_decoder |
13 | 16 |
|
14 | 17 |
|
15 | 18 | class _Item(msgspec.Struct): |
@@ -70,3 +73,88 @@ def handler(request: httpx2.Request) -> httpx2.Response: |
70 | 73 | exc = exc_info.value |
71 | 74 | assert exc.model is _Item |
72 | 75 | assert isinstance(exc.original, (msgspec.DecodeError, msgspec.ValidationError)) |
| 76 | + |
| 77 | + |
| 78 | +class _PydanticUser(pydantic.BaseModel): |
| 79 | + id: int |
| 80 | + name: str |
| 81 | + |
| 82 | + |
| 83 | +@dataclasses.dataclass |
| 84 | +class _DC: |
| 85 | + id: int |
| 86 | + name: str |
| 87 | + |
| 88 | + |
| 89 | +@pytest.fixture(autouse=True) |
| 90 | +def _clear_msgspec_cache() -> None: |
| 91 | + _get_msgspec_decoder.cache_clear() |
| 92 | + |
| 93 | + |
| 94 | +def test_msgspec_can_decode_struct() -> None: |
| 95 | + assert MsgspecDecoder().can_decode(_Item) is True |
| 96 | + |
| 97 | + |
| 98 | +def test_msgspec_can_decode_dataclass() -> None: |
| 99 | + assert MsgspecDecoder().can_decode(_DC) is True |
| 100 | + |
| 101 | + |
| 102 | +def test_msgspec_can_decode_dict() -> None: |
| 103 | + assert MsgspecDecoder().can_decode(dict) is True |
| 104 | + |
| 105 | + |
| 106 | +def test_msgspec_can_decode_list_of_structs() -> None: |
| 107 | + assert MsgspecDecoder().can_decode(list[_Item]) is True |
| 108 | + |
| 109 | + |
| 110 | +def test_msgspec_can_decode_primitive_int() -> None: |
| 111 | + assert MsgspecDecoder().can_decode(int) is True |
| 112 | + |
| 113 | + |
| 114 | +def test_msgspec_rejects_pydantic_basemodel() -> None: |
| 115 | + assert MsgspecDecoder().can_decode(_PydanticUser) is False |
| 116 | + |
| 117 | + |
| 118 | +def test_msgspec_can_decode_uses_cache() -> None: |
| 119 | + _get_msgspec_decoder.cache_clear() |
| 120 | + decoder = MsgspecDecoder() |
| 121 | + decoder.can_decode(_Item) |
| 122 | + decoder.can_decode(_Item) |
| 123 | + info = _get_msgspec_decoder.cache_info() |
| 124 | + assert info.hits >= 1 |
| 125 | + assert info.misses == 1 |
| 126 | + |
| 127 | + |
| 128 | +def test_can_decode_returns_false_when_type_info_raises() -> None: |
| 129 | + """`type_info` failures (unrecognized type) are treated as a soft 'no'.""" |
| 130 | + with patch( |
| 131 | + "httpware.decoders.msgspec.msgspec.inspect.type_info", |
| 132 | + side_effect=TypeError("unknown"), |
| 133 | + ): |
| 134 | + assert MsgspecDecoder().can_decode(_Item) is False |
| 135 | + |
| 136 | + |
| 137 | +def test_can_decode_returns_false_when_decoder_build_raises() -> None: |
| 138 | + """A `_get_msgspec_decoder` failure after type_info-classification is a soft 'no'.""" |
| 139 | + _get_msgspec_decoder.cache_clear() |
| 140 | + with patch( |
| 141 | + "httpware.decoders.msgspec._get_msgspec_decoder", |
| 142 | + side_effect=TypeError("cannot build decoder"), |
| 143 | + ): |
| 144 | + assert MsgspecDecoder().can_decode(_Item) is False |
| 145 | + |
| 146 | + |
| 147 | +def test_unhashable_model_falls_back_to_uncached_decoder() -> None: |
| 148 | + """Unhashable `model` falls back to a direct uncached `msgspec.json.Decoder`. |
| 149 | +
|
| 150 | + Mirrors `PydanticDecoder`'s unhashable-fallback test: when `_get_msgspec_decoder` |
| 151 | + raises `TypeError` (e.g., an unhashable parameterized type), `decode` bypasses |
| 152 | + the cache so the user-visible error is `msgspec`'s own decode error, not a |
| 153 | + `functools`-internal `TypeError`. |
| 154 | + """ |
| 155 | + with patch( |
| 156 | + "httpware.decoders.msgspec._get_msgspec_decoder", |
| 157 | + side_effect=TypeError("unhashable type"), |
| 158 | + ): |
| 159 | + result = MsgspecDecoder().decode(b"42", int) |
| 160 | + assert result == 42 # noqa: PLR2004 |
0 commit comments