Coverage for tests/test_jsonable_encoder.py: 100%
198 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-08 03:53 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-08-08 03:53 +0000
1from collections import deque 1abcde
2from dataclasses import dataclass 1abcde
3from datetime import datetime, timezone 1abcde
4from decimal import Decimal 1abcde
5from enum import Enum 1abcde
6from pathlib import PurePath, PurePosixPath, PureWindowsPath 1abcde
7from typing import Optional 1abcde
9import pytest 1abcde
10from fastapi._compat import PYDANTIC_V2, Undefined 1abcde
11from fastapi.encoders import jsonable_encoder 1abcde
12from pydantic import BaseModel, Field, ValidationError 1abcde
14from .utils import needs_pydanticv1, needs_pydanticv2 1abcde
17class Person: 1abcde
18 def __init__(self, name: str): 1abcde
19 self.name = name 1abcde
22class Pet: 1abcde
23 def __init__(self, owner: Person, name: str): 1abcde
24 self.owner = owner 1abcde
25 self.name = name 1abcde
28@dataclass 1abcde
29class Item: 1abcde
30 name: str 1abcde
31 count: int 1abcde
34class DictablePerson(Person): 1abcde
35 def __iter__(self): 1abcde
36 return ((k, v) for k, v in self.__dict__.items()) 1abcde
39class DictablePet(Pet): 1abcde
40 def __iter__(self): 1abcde
41 return ((k, v) for k, v in self.__dict__.items()) 1abcde
44class Unserializable: 1abcde
45 def __iter__(self): 1abcde
46 raise NotImplementedError() 1abcde
48 @property 1abcde
49 def __dict__(self): 1abcde
50 raise NotImplementedError() 1abcde
53class RoleEnum(Enum): 1abcde
54 admin = "admin" 1abcde
55 normal = "normal" 1abcde
58class ModelWithConfig(BaseModel): 1abcde
59 role: Optional[RoleEnum] = None 1abcde
61 if PYDANTIC_V2: 1abcde
62 model_config = {"use_enum_values": True} 1abcde
63 else:
65 class Config: 1abcde
66 use_enum_values = True 1abcde
69class ModelWithAlias(BaseModel): 1abcde
70 foo: str = Field(alias="Foo") 1abcde
73class ModelWithDefault(BaseModel): 1abcde
74 foo: str = ... # type: ignore 1abcde
75 bar: str = "bar" 1abcde
76 bla: str = "bla" 1abcde
79def test_encode_dict(): 1abcde
80 pet = {"name": "Firulais", "owner": {"name": "Foo"}} 1abcde
81 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 1abcde
82 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1abcde
83 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 1abcde
84 assert jsonable_encoder(pet, include={}) == {} 1abcde
85 assert jsonable_encoder(pet, exclude={}) == { 1abcde
86 "name": "Firulais",
87 "owner": {"name": "Foo"},
88 }
91def test_encode_class(): 1abcde
92 person = Person(name="Foo") 1abcde
93 pet = Pet(owner=person, name="Firulais") 1abcde
94 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 1abcde
95 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1abcde
96 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 1abcde
97 assert jsonable_encoder(pet, include={}) == {} 1abcde
98 assert jsonable_encoder(pet, exclude={}) == { 1abcde
99 "name": "Firulais",
100 "owner": {"name": "Foo"},
101 }
104def test_encode_dictable(): 1abcde
105 person = DictablePerson(name="Foo") 1abcde
106 pet = DictablePet(owner=person, name="Firulais") 1abcde
107 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 1abcde
108 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1abcde
109 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 1abcde
110 assert jsonable_encoder(pet, include={}) == {} 1abcde
111 assert jsonable_encoder(pet, exclude={}) == { 1abcde
112 "name": "Firulais",
113 "owner": {"name": "Foo"},
114 }
117def test_encode_dataclass(): 1abcde
118 item = Item(name="foo", count=100) 1abcde
119 assert jsonable_encoder(item) == {"name": "foo", "count": 100} 1abcde
120 assert jsonable_encoder(item, include={"name"}) == {"name": "foo"} 1abcde
121 assert jsonable_encoder(item, exclude={"count"}) == {"name": "foo"} 1abcde
122 assert jsonable_encoder(item, include={}) == {} 1abcde
123 assert jsonable_encoder(item, exclude={}) == {"name": "foo", "count": 100} 1abcde
126def test_encode_unsupported(): 1abcde
127 unserializable = Unserializable() 1abcde
128 with pytest.raises(ValueError): 1abcde
129 jsonable_encoder(unserializable) 1abcde
132@needs_pydanticv2 1abcde
133def test_encode_custom_json_encoders_model_pydanticv2(): 1abcde
134 from pydantic import field_serializer 1abcde
136 class ModelWithCustomEncoder(BaseModel): 1abcde
137 dt_field: datetime 1abcde
139 @field_serializer("dt_field") 1abcde
140 def serialize_dt_field(self, dt): 1abcde
141 return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat() 1abcde
143 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1abcde
144 pass 1abcde
146 model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) 1abcde
147 assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1abcde
148 subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) 1abcde
149 assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1abcde
152# TODO: remove when deprecating Pydantic v1
153@needs_pydanticv1 1abcde
154def test_encode_custom_json_encoders_model_pydanticv1(): 1abcde
155 class ModelWithCustomEncoder(BaseModel): 1abcde
156 dt_field: datetime 1abcde
158 class Config: 1abcde
159 json_encoders = { 1abcde
160 datetime: lambda dt: dt.replace(
161 microsecond=0, tzinfo=timezone.utc
162 ).isoformat()
163 }
165 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1abcde
166 class Config: 1abcde
167 pass 1abcde
169 model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) 1abcde
170 assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1abcde
171 subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) 1abcde
172 assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1abcde
175def test_encode_model_with_config(): 1abcde
176 model = ModelWithConfig(role=RoleEnum.admin) 1abcde
177 assert jsonable_encoder(model) == {"role": "admin"} 1abcde
180def test_encode_model_with_alias_raises(): 1abcde
181 with pytest.raises(ValidationError): 1abcde
182 ModelWithAlias(foo="Bar") 1abcde
185def test_encode_model_with_alias(): 1abcde
186 model = ModelWithAlias(Foo="Bar") 1abcde
187 assert jsonable_encoder(model) == {"Foo": "Bar"} 1abcde
190def test_encode_model_with_default(): 1abcde
191 model = ModelWithDefault(foo="foo", bar="bar") 1abcde
192 assert jsonable_encoder(model) == {"foo": "foo", "bar": "bar", "bla": "bla"} 1abcde
193 assert jsonable_encoder(model, exclude_unset=True) == {"foo": "foo", "bar": "bar"} 1abcde
194 assert jsonable_encoder(model, exclude_defaults=True) == {"foo": "foo"} 1abcde
195 assert jsonable_encoder(model, exclude_unset=True, exclude_defaults=True) == { 1abcde
196 "foo": "foo"
197 }
198 assert jsonable_encoder(model, include={"foo"}) == {"foo": "foo"} 1abcde
199 assert jsonable_encoder(model, exclude={"bla"}) == {"foo": "foo", "bar": "bar"} 1abcde
200 assert jsonable_encoder(model, include={}) == {} 1abcde
201 assert jsonable_encoder(model, exclude={}) == { 1abcde
202 "foo": "foo",
203 "bar": "bar",
204 "bla": "bla",
205 }
208@needs_pydanticv1 1abcde
209def test_custom_encoders(): 1abcde
210 class safe_datetime(datetime): 1abcde
211 pass 1abcde
213 class MyModel(BaseModel): 1abcde
214 dt_field: safe_datetime 1abcde
216 instance = MyModel(dt_field=safe_datetime.now()) 1abcde
218 encoded_instance = jsonable_encoder( 1abcde
219 instance, custom_encoder={safe_datetime: lambda o: o.isoformat()}
220 )
221 assert encoded_instance["dt_field"] == instance.dt_field.isoformat() 1abcde
224def test_custom_enum_encoders(): 1abcde
225 def custom_enum_encoder(v: Enum): 1abcde
226 return v.value.lower() 1abcde
228 class MyEnum(Enum): 1abcde
229 ENUM_VAL_1 = "ENUM_VAL_1" 1abcde
231 instance = MyEnum.ENUM_VAL_1 1abcde
233 encoded_instance = jsonable_encoder( 1abcde
234 instance, custom_encoder={MyEnum: custom_enum_encoder}
235 )
236 assert encoded_instance == custom_enum_encoder(instance) 1abcde
239def test_encode_model_with_pure_path(): 1abcde
240 class ModelWithPath(BaseModel): 1abcde
241 path: PurePath 1abcde
243 if PYDANTIC_V2: 1abcde
244 model_config = {"arbitrary_types_allowed": True} 1abcde
245 else:
247 class Config: 1abcde
248 arbitrary_types_allowed = True 1abcde
250 test_path = PurePath("/foo", "bar") 1abcde
251 obj = ModelWithPath(path=test_path) 1abcde
252 assert jsonable_encoder(obj) == {"path": str(test_path)} 1abcde
255def test_encode_model_with_pure_posix_path(): 1abcde
256 class ModelWithPath(BaseModel): 1abcde
257 path: PurePosixPath 1abcde
259 if PYDANTIC_V2: 1abcde
260 model_config = {"arbitrary_types_allowed": True} 1abcde
261 else:
263 class Config: 1abcde
264 arbitrary_types_allowed = True 1abcde
266 obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) 1abcde
267 assert jsonable_encoder(obj) == {"path": "/foo/bar"} 1abcde
270def test_encode_model_with_pure_windows_path(): 1abcde
271 class ModelWithPath(BaseModel): 1abcde
272 path: PureWindowsPath 1abcde
274 if PYDANTIC_V2: 1abcde
275 model_config = {"arbitrary_types_allowed": True} 1abcde
276 else:
278 class Config: 1abcde
279 arbitrary_types_allowed = True 1abcde
281 obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) 1abcde
282 assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} 1abcde
285@needs_pydanticv1 1abcde
286def test_encode_root(): 1abcde
287 class ModelWithRoot(BaseModel): 1abcde
288 __root__: str 1abcde
290 model = ModelWithRoot(__root__="Foo") 1abcde
291 assert jsonable_encoder(model) == "Foo" 1abcde
294@needs_pydanticv2 1abcde
295def test_decimal_encoder_float(): 1abcde
296 data = {"value": Decimal(1.23)} 1abcde
297 assert jsonable_encoder(data) == {"value": 1.23} 1abcde
300@needs_pydanticv2 1abcde
301def test_decimal_encoder_int(): 1abcde
302 data = {"value": Decimal(2)} 1abcde
303 assert jsonable_encoder(data) == {"value": 2} 1abcde
306def test_encode_deque_encodes_child_models(): 1abcde
307 class Model(BaseModel): 1abcde
308 test: str 1abcde
310 dq = deque([Model(test="test")]) 1abcde
312 assert jsonable_encoder(dq)[0]["test"] == "test" 1abcde
315@needs_pydanticv2 1abcde
316def test_encode_pydantic_undefined(): 1abcde
317 data = {"value": Undefined} 1abcde
318 assert jsonable_encoder(data) == {"value": None} 1abcde