Coverage for tests/test_jsonable_encoder.py: 100%

198 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-13 13:38 +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 1ufvgwhxiyj

20 

21 

22class Pet: 1abcde

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

24 self.owner = owner 1ufvgwhxiyj

25 self.name = name 1ufvgwhxiyj

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()) 1fghij

37 

38 

39class DictablePet(Pet): 1abcde

40 def __iter__(self): 1abcde

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

42 

43 

44class Unserializable: 1abcde

45 def __iter__(self): 1abcde

46 raise NotImplementedError() 1%'()*

47 

48 @property 1abcde

49 def __dict__(self): 1abcde

50 raise NotImplementedError() 1%'()*

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"}} 134567

81 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 134567

82 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 134567

83 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 134567

84 assert jsonable_encoder(pet, include={}) == {} 134567

85 assert jsonable_encoder(pet, exclude={}) == { 134567

86 "name": "Firulais", 

87 "owner": {"name": "Foo"}, 

88 } 

89 

90 

91def test_encode_class(): 1abcde

92 person = Person(name="Foo") 1uvwxy

93 pet = Pet(owner=person, name="Firulais") 1uvwxy

94 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 1uvwxy

95 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1uvwxy

96 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 1uvwxy

97 assert jsonable_encoder(pet, include={}) == {} 1uvwxy

98 assert jsonable_encoder(pet, exclude={}) == { 1uvwxy

99 "name": "Firulais", 

100 "owner": {"name": "Foo"}, 

101 } 

102 

103 

104def test_encode_dictable(): 1abcde

105 person = DictablePerson(name="Foo") 1fghij

106 pet = DictablePet(owner=person, name="Firulais") 1fghij

107 assert jsonable_encoder(pet) == {"name": "Firulais", "owner": {"name": "Foo"}} 1fghij

108 assert jsonable_encoder(pet, include={"name"}) == {"name": "Firulais"} 1fghij

109 assert jsonable_encoder(pet, exclude={"owner"}) == {"name": "Firulais"} 1fghij

110 assert jsonable_encoder(pet, include={}) == {} 1fghij

111 assert jsonable_encoder(pet, exclude={}) == { 1fghij

112 "name": "Firulais", 

113 "owner": {"name": "Foo"}, 

114 } 

115 

116 

117def test_encode_dataclass(): 1abcde

118 item = Item(name="foo", count=100) 189!#$

119 assert jsonable_encoder(item) == {"name": "foo", "count": 100} 189!#$

120 assert jsonable_encoder(item, include={"name"}) == {"name": "foo"} 189!#$

121 assert jsonable_encoder(item, exclude={"count"}) == {"name": "foo"} 189!#$

122 assert jsonable_encoder(item, include={}) == {} 189!#$

123 assert jsonable_encoder(item, exclude={}) == {"name": "foo", "count": 100} 189!#$

124 

125 

126def test_encode_unsupported(): 1abcde

127 unserializable = Unserializable() 1%'()*

128 with pytest.raises(ValueError): 1%'()*

129 jsonable_encoder(unserializable) 1%'()*

130 

131 

132@needs_pydanticv2 1abcde

133def test_encode_custom_json_encoders_model_pydanticv2(): 1abcde

134 from pydantic import field_serializer 1klmno

135 

136 class ModelWithCustomEncoder(BaseModel): 1klmno

137 dt_field: datetime 1klmno

138 

139 @field_serializer("dt_field") 1klmno

140 def serialize_dt_field(self, dt): 1klmno

141 return dt.replace(microsecond=0, tzinfo=timezone.utc).isoformat() 1klmno

142 

143 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1klmno

144 pass 1klmno

145 

146 model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) 1klmno

147 assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1klmno

148 subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) 1klmno

149 assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1klmno

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): 1pqrst

156 dt_field: datetime 1pqrst

157 

158 class Config: 1pqrst

159 json_encoders = { 1pqrst

160 datetime: lambda dt: dt.replace( 

161 microsecond=0, tzinfo=timezone.utc 

162 ).isoformat() 

163 } 

164 

165 class ModelWithCustomEncoderSubclass(ModelWithCustomEncoder): 1pqrst

166 class Config: 1pqrst

167 pass 1pqrst

168 

169 model = ModelWithCustomEncoder(dt_field=datetime(2019, 1, 1, 8)) 1pqrst

170 assert jsonable_encoder(model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1pqrst

171 subclass_model = ModelWithCustomEncoderSubclass(dt_field=datetime(2019, 1, 1, 8)) 1pqrst

172 assert jsonable_encoder(subclass_model) == {"dt_field": "2019-01-01T08:00:00+00:00"} 1pqrst

173 

174 

175def test_encode_model_with_config(): 1abcde

176 model = ModelWithConfig(role=RoleEnum.admin) 1[]^_`

177 assert jsonable_encoder(model) == {"role": "admin"} 1[]^_`

178 

179 

180def test_encode_model_with_alias_raises(): 1abcde

181 with pytest.raises(ValidationError): 2{ | } ~ ab

182 ModelWithAlias(foo="Bar") 2{ | } ~ ab

183 

184 

185def test_encode_model_with_alias(): 1abcde

186 model = ModelWithAlias(Foo="Bar") 2bbcbdbebfb

187 assert jsonable_encoder(model) == {"Foo": "Bar"} 2bbcbdbebfb

188 

189 

190def test_encode_model_with_default(): 1abcde

191 model = ModelWithDefault(foo="foo", bar="bar") 1zABCD

192 assert jsonable_encoder(model) == {"foo": "foo", "bar": "bar", "bla": "bla"} 1zABCD

193 assert jsonable_encoder(model, exclude_unset=True) == {"foo": "foo", "bar": "bar"} 1zABCD

194 assert jsonable_encoder(model, exclude_defaults=True) == {"foo": "foo"} 1zABCD

195 assert jsonable_encoder(model, exclude_unset=True, exclude_defaults=True) == { 1zABCD

196 "foo": "foo" 

197 } 

198 assert jsonable_encoder(model, include={"foo"}) == {"foo": "foo"} 1zABCD

199 assert jsonable_encoder(model, exclude={"bla"}) == {"foo": "foo", "bar": "bar"} 1zABCD

200 assert jsonable_encoder(model, include={}) == {} 1zABCD

201 assert jsonable_encoder(model, exclude={}) == { 1zABCD

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): 1TUVWX

211 pass 1TUVWX

212 

213 class MyModel(BaseModel): 1TUVWX

214 dt_field: safe_datetime 1TUVWX

215 

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

217 

218 encoded_instance = jsonable_encoder( 1TUVWX

219 instance, custom_encoder={safe_datetime: lambda o: o.isoformat()} 

220 ) 

221 assert encoded_instance["dt_field"] == instance.dt_field.isoformat() 1TUVWX

222 

223 

224def test_custom_enum_encoders(): 1abcde

225 def custom_enum_encoder(v: Enum): 1YZ012

226 return v.value.lower() 1YZ012

227 

228 class MyEnum(Enum): 1YZ012

229 ENUM_VAL_1 = "ENUM_VAL_1" 1YZ012

230 

231 instance = MyEnum.ENUM_VAL_1 1YZ012

232 

233 encoded_instance = jsonable_encoder( 1YZ012

234 instance, custom_encoder={MyEnum: custom_enum_encoder} 

235 ) 

236 assert encoded_instance == custom_enum_encoder(instance) 1YZ012

237 

238 

239def test_encode_model_with_pure_path(): 1abcde

240 class ModelWithPath(BaseModel): 1EFGHI

241 path: PurePath 1EFGHI

242 

243 if PYDANTIC_V2: 1EFGHI

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

245 else: 

246 

247 class Config: 1EFGHI

248 arbitrary_types_allowed = True 1EFGHI

249 

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

251 obj = ModelWithPath(path=test_path) 1EFGHI

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

253 

254 

255def test_encode_model_with_pure_posix_path(): 1abcde

256 class ModelWithPath(BaseModel): 1JKLMN

257 path: PurePosixPath 1JKLMN

258 

259 if PYDANTIC_V2: 1JKLMN

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

261 else: 

262 

263 class Config: 1JKLMN

264 arbitrary_types_allowed = True 1JKLMN

265 

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

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

268 

269 

270def test_encode_model_with_pure_windows_path(): 1abcde

271 class ModelWithPath(BaseModel): 1OPQRS

272 path: PureWindowsPath 1OPQRS

273 

274 if PYDANTIC_V2: 1OPQRS

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

276 else: 

277 

278 class Config: 1OPQRS

279 arbitrary_types_allowed = True 1OPQRS

280 

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

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

283 

284 

285@needs_pydanticv1 1abcde

286def test_encode_root(): 1abcde

287 class ModelWithRoot(BaseModel): 1+,-./

288 __root__: str 1+,-./

289 

290 model = ModelWithRoot(__root__="Foo") 1+,-./

291 assert jsonable_encoder(model) == "Foo" 1+,-./

292 

293 

294@needs_pydanticv2 1abcde

295def test_decimal_encoder_float(): 1abcde

296 data = {"value": Decimal(1.23)} 2gbhbibjbkb

297 assert jsonable_encoder(data) == {"value": 1.23} 2gbhbibjbkb

298 

299 

300@needs_pydanticv2 1abcde

301def test_decimal_encoder_int(): 1abcde

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

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

304 

305 

306def test_encode_deque_encodes_child_models(): 1abcde

307 class Model(BaseModel): 1:;=?@

308 test: str 1:;=?@

309 

310 dq = deque([Model(test="test")]) 1:;=?@

311 

312 assert jsonable_encoder(dq)[0]["test"] == "test" 1:;=?@

313 

314 

315@needs_pydanticv2 1abcde

316def test_encode_pydantic_undefined(): 1abcde

317 data = {"value": Undefined} 2qbrbsbtbub

318 assert jsonable_encoder(data) == {"value": None} 2qbrbsbtbub