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

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

8 

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

13 

14from .utils import needs_pydanticv1, needs_pydanticv2 1abcde

15 

16 

17class Person: 1abcde

18 def __init__(self, name: str): 1abcde

19 self.name = name 1abcde

20 

21 

22class Pet: 1abcde

23 def __init__(self, owner: Person, name: str): 1abcde

24 self.owner = owner 1abcde

25 self.name = name 1abcde

26 

27 

28@dataclass 1abcde

29class Item: 1abcde

30 name: str 1abcde

31 count: int 1abcde

32 

33 

34class DictablePerson(Person): 1abcde

35 def __iter__(self): 1abcde

36 return ((k, v) for k, v in self.__dict__.items()) 1abcde

37 

38 

39class DictablePet(Pet): 1abcde

40 def __iter__(self): 1abcde

41 return ((k, v) for k, v in self.__dict__.items()) 1abcde

42 

43 

44class Unserializable: 1abcde

45 def __iter__(self): 1abcde

46 raise NotImplementedError() 1abcde

47 

48 @property 1abcde

49 def __dict__(self): 1abcde

50 raise NotImplementedError() 1abcde

51 

52 

53class RoleEnum(Enum): 1abcde

54 admin = "admin" 1abcde

55 normal = "normal" 1abcde

56 

57 

58class ModelWithConfig(BaseModel): 1abcde

59 role: Optional[RoleEnum] = None 1abcde

60 

61 if PYDANTIC_V2: 1abcde

62 model_config = {"use_enum_values": True} 1abcde

63 else: 

64 

65 class Config: 1abcde

66 use_enum_values = True 1abcde

67 

68 

69class ModelWithAlias(BaseModel): 1abcde

70 foo: str = Field(alias="Foo") 1abcde

71 

72 

73class ModelWithDefault(BaseModel): 1abcde

74 foo: str = ... # type: ignore 1abcde

75 bar: str = "bar" 1abcde

76 bla: str = "bla" 1abcde

77 

78 

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 } 

89 

90 

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 } 

102 

103 

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 } 

115 

116 

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

124 

125 

126def test_encode_unsupported(): 1abcde

127 unserializable = Unserializable() 1abcde

128 with pytest.raises(ValueError): 1abcde

129 jsonable_encoder(unserializable) 1abcde

130 

131 

132@needs_pydanticv2 1abcde

133def test_encode_custom_json_encoders_model_pydanticv2(): 1abcde

134 from pydantic import field_serializer 1abcde

135 

136 class ModelWithCustomEncoder(BaseModel): 1abcde

137 dt_field: datetime 1abcde

138 

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

142 

143 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1abcde

144 pass 1abcde

145 

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

150 

151 

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

157 

158 class Config: 1abcde

159 json_encoders = { 1abcde

160 datetime: lambda dt: dt.replace( 

161 microsecond=0, tzinfo=timezone.utc 

162 ).isoformat() 

163 } 

164 

165 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1abcde

166 class Config: 1abcde

167 pass 1abcde

168 

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

173 

174 

175def test_encode_model_with_config(): 1abcde

176 model = ModelWithConfig(role=RoleEnum.admin) 1abcde

177 assert jsonable_encoder(model) == {"role": "admin"} 1abcde

178 

179 

180def test_encode_model_with_alias_raises(): 1abcde

181 with pytest.raises(ValidationError): 1abcde

182 ModelWithAlias(foo="Bar") 1abcde

183 

184 

185def test_encode_model_with_alias(): 1abcde

186 model = ModelWithAlias(Foo="Bar") 1abcde

187 assert jsonable_encoder(model) == {"Foo": "Bar"} 1abcde

188 

189 

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 } 

206 

207 

208@needs_pydanticv1 1abcde

209def test_custom_encoders(): 1abcde

210 class safe_datetime(datetime): 1abcde

211 pass 1abcde

212 

213 class MyModel(BaseModel): 1abcde

214 dt_field: safe_datetime 1abcde

215 

216 instance = MyModel(dt_field=safe_datetime.now()) 1abcde

217 

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

222 

223 

224def test_custom_enum_encoders(): 1abcde

225 def custom_enum_encoder(v: Enum): 1abcde

226 return v.value.lower() 1abcde

227 

228 class MyEnum(Enum): 1abcde

229 ENUM_VAL_1 = "ENUM_VAL_1" 1abcde

230 

231 instance = MyEnum.ENUM_VAL_1 1abcde

232 

233 encoded_instance = jsonable_encoder( 1abcde

234 instance, custom_encoder={MyEnum: custom_enum_encoder} 

235 ) 

236 assert encoded_instance == custom_enum_encoder(instance) 1abcde

237 

238 

239def test_encode_model_with_pure_path(): 1abcde

240 class ModelWithPath(BaseModel): 1abcde

241 path: PurePath 1abcde

242 

243 if PYDANTIC_V2: 1abcde

244 model_config = {"arbitrary_types_allowed": True} 1abcde

245 else: 

246 

247 class Config: 1abcde

248 arbitrary_types_allowed = True 1abcde

249 

250 test_path = PurePath("/foo", "bar") 1abcde

251 obj = ModelWithPath(path=test_path) 1abcde

252 assert jsonable_encoder(obj) == {"path": str(test_path)} 1abcde

253 

254 

255def test_encode_model_with_pure_posix_path(): 1abcde

256 class ModelWithPath(BaseModel): 1abcde

257 path: PurePosixPath 1abcde

258 

259 if PYDANTIC_V2: 1abcde

260 model_config = {"arbitrary_types_allowed": True} 1abcde

261 else: 

262 

263 class Config: 1abcde

264 arbitrary_types_allowed = True 1abcde

265 

266 obj = ModelWithPath(path=PurePosixPath("/foo", "bar")) 1abcde

267 assert jsonable_encoder(obj) == {"path": "/foo/bar"} 1abcde

268 

269 

270def test_encode_model_with_pure_windows_path(): 1abcde

271 class ModelWithPath(BaseModel): 1abcde

272 path: PureWindowsPath 1abcde

273 

274 if PYDANTIC_V2: 1abcde

275 model_config = {"arbitrary_types_allowed": True} 1abcde

276 else: 

277 

278 class Config: 1abcde

279 arbitrary_types_allowed = True 1abcde

280 

281 obj = ModelWithPath(path=PureWindowsPath("/foo", "bar")) 1abcde

282 assert jsonable_encoder(obj) == {"path": "\\foo\\bar"} 1abcde

283 

284 

285@needs_pydanticv1 1abcde

286def test_encode_root(): 1abcde

287 class ModelWithRoot(BaseModel): 1abcde

288 __root__: str 1abcde

289 

290 model = ModelWithRoot(__root__="Foo") 1abcde

291 assert jsonable_encoder(model) == "Foo" 1abcde

292 

293 

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

298 

299 

300@needs_pydanticv2 1abcde

301def test_decimal_encoder_int(): 1abcde

302 data = {"value": Decimal(2)} 1abcde

303 assert jsonable_encoder(data) == {"value": 2} 1abcde

304 

305 

306def test_encode_deque_encodes_child_models(): 1abcde

307 class Model(BaseModel): 1abcde

308 test: str 1abcde

309 

310 dq = deque([Model(test="test")]) 1abcde

311 

312 assert jsonable_encoder(dq)[0]["test"] == "test" 1abcde

313 

314 

315@needs_pydanticv2 1abcde

316def test_encode_pydantic_undefined(): 1abcde

317 data = {"value": Undefined} 1abcde

318 assert jsonable_encoder(data) == {"value": None} 1abcde