Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/serde/model.py: 56%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

139 statements  

1""" 

2This module defines the core `~serde.Model` class. 

3""" 

4 

5import inspect 

6import json 

7from collections import OrderedDict 

8 

9from serde.exceptions import ContextError, add_context 

10from serde.fields import Field, _resolve 

11from serde.utils import dict_partition, zip_until_right 

12 

13 

14__all__ = ['Model'] 

15 

16 

17class Fields(OrderedDict): 

18 """ 

19 An `~collections.OrderedDict` that allows value access with dot notation. 

20 """ 

21 

22 def __getattr__(self, name): 

23 """ 

24 Return values in the dictionary using attribute access with keys. 

25 """ 

26 try: 

27 return self[name] 

28 except KeyError: 

29 return super(Fields, self).__getattribute__(name) 

30 

31 

32class ModelType(type): 

33 """ 

34 A metaclass for a `Model`. 

35 

36 This metaclass pulls `~serde.fields.Field` attributes off the defined class. 

37 These can be accessed using the ``__fields__`` attribute on the class. Model 

38 methods use the ``__fields__`` attribute to instantiate, serialize, 

39 deserialize, normalize, and validate models. 

40 """ 

41 

42 @staticmethod 

43 def _pop_meta(attrs): 

44 """ 

45 Handle the Meta class attributes. 

46 """ 

47 abstract = False 

48 tag = None 

49 

50 if 'Meta' in attrs: 

51 meta = attrs.pop('Meta').__dict__ 

52 if 'abstract' in meta: 

53 abstract = meta['abstract'] 

54 if 'tag' in meta: 

55 tag = meta['tag'] 

56 

57 return abstract, tag 

58 

59 def __new__(cls, cname, bases, attrs): 

60 """ 

61 Create a new `Model` class. 

62 

63 Args: 

64 cname (str): the class name. 

65 bases (tuple): the base classes. 

66 attrs (dict): the attributes for this class. 

67 

68 Returns: 

69 Model: a new model class. 

70 """ 

71 parent = None 

72 abstract, tag = cls._pop_meta(attrs) 

73 

74 # Split the attrs into Fields and non-Fields. 

75 fields, final_attrs = dict_partition(attrs, lambda k, v: isinstance(v, Field)) 

76 

77 if '__annotations__' in attrs: 

78 if fields: 

79 raise ContextError( 

80 'simultaneous use of annotations and class attributes ' 

81 'for field definitions' 

82 ) 

83 fields = OrderedDict( 

84 (k, _resolve(v)) for k, v in attrs.pop('__annotations__').items() 

85 ) 

86 

87 # Create our Model class. 

88 model_cls = super(ModelType, cls).__new__(cls, cname, bases, final_attrs) 

89 

90 # Bind the Model to the Fields. 

91 for name, field in fields.items(): 

92 field._bind(model_cls, name=name) 

93 # Bind the Model to the Tags. 

94 if tag: 

95 tag._bind(model_cls) 

96 tags = [tag] 

97 else: 

98 tags = [] 

99 

100 # Loop though the base classes, and pull Fields and Tags off. 

101 for base in inspect.getmro(model_cls)[1:]: 

102 if getattr(base, '__class__', None) is cls: 

103 fields.update( 

104 [ 

105 (name, field) 

106 for name, field in base.__fields__.items() 

107 if name not in attrs 

108 ] 

109 ) 

110 tags = base.__tags__ + tags 

111 if not parent: 

112 parent = base 

113 

114 # Assign all the things to the Model! 

115 model_cls._abstract = abstract 

116 model_cls._parent = parent 

117 model_cls._fields = Fields(sorted(fields.items(), key=lambda x: x[1].id)) 

118 model_cls._tag = tag 

119 model_cls._tags = tags 

120 

121 return model_cls 

122 

123 @property 

124 def __abstract__(cls): 

125 """ 

126 Whether this model class is abstract or not. 

127 """ 

128 return cls._abstract 

129 

130 @property 

131 def __parent__(cls): 

132 """ 

133 This model class's parent model class. 

134 """ 

135 return cls._parent 

136 

137 @property 

138 def __fields__(cls): 

139 """ 

140 A map of attribute name to field instance. 

141 """ 

142 return cls._fields.copy() 

143 

144 @property 

145 def __tag__(cls): 

146 """ 

147 The model class's tag (or None). 

148 """ 

149 return cls._tag 

150 

151 @property 

152 def __tags__(cls): 

153 """ 

154 The model class's tag and all parent class's tags. 

155 """ 

156 return cls._tags[:] 

157 

158 

159class Model(object, metaclass=ModelType): 

160 """ 

161 The base model. 

162 """ 

163 

164 def __init__(self, *args, **kwargs): 

165 """ 

166 Create a new model. 

167 

168 Args: 

169 *args: positional arguments values for each field on the model. If 

170 these are given they will be interpreted as corresponding to the 

171 fields in the order they are defined on the model class. 

172 **kwargs: keyword argument values for each field on the model. 

173 """ 

174 if self.__class__.__abstract__: 

175 raise TypeError( 

176 f'unable to instantiate abstract model {self.__class__.__name__!r}' 

177 ) 

178 

179 try: 

180 for name, value in zip_until_right(self.__class__.__fields__.keys(), args): 

181 if name in kwargs: 

182 raise TypeError( 

183 f'__init__() got multiple values for keyword argument {name!r}' 

184 ) 

185 kwargs[name] = value 

186 except ValueError: 

187 max_args = len(self.__class__.__fields__) + 1 

188 given_args = len(args) + 1 

189 raise TypeError( 

190 f'__init__() takes a maximum of {max_args!r} ' 

191 f'positional arguments but {given_args!r} were given' 

192 ) 

193 

194 for field in self.__class__.__fields__.values(): 

195 with add_context(field): 

196 field._instantiate_with(self, kwargs) 

197 

198 if kwargs: 

199 kwarg = next(iter(kwargs.keys())) 

200 raise TypeError(f'__init__() got an unexpected keyword argument {kwarg!r}') 

201 

202 self._normalize() 

203 self._validate() 

204 

205 def __eq__(self, other): 

206 """ 

207 Whether two models are the same. 

208 """ 

209 return isinstance(other, self.__class__) and all( 

210 getattr(self, name) == getattr(other, name) 

211 for name in self.__class__.__fields__.keys() 

212 ) 

213 

214 def __hash__(self): 

215 """ 

216 Return a hash value for this model. 

217 """ 

218 return hash( 

219 tuple( 

220 (name, getattr(self, name)) for name in self.__class__.__fields__.keys() 

221 ) 

222 ) 

223 

224 def __repr__(self): 

225 """ 

226 Return the canonical string representation of this model. 

227 """ 

228 return '<{module}.{name} model at 0x{id:x}>'.format( 

229 module=self.__class__.__module__, 

230 name=self.__class__.__qualname__, 

231 id=id(self), 

232 ) 

233 

234 def to_dict(self): 

235 """ 

236 Convert this model to a dictionary. 

237 

238 Returns: 

239 ~collections.OrderedDict: the model serialized as a dictionary. 

240 """ 

241 d = OrderedDict() 

242 

243 for field in self.__class__.__fields__.values(): 

244 with add_context(field): 

245 d = field._serialize_with(self, d) 

246 

247 for tag in reversed(self.__class__.__tags__): 

248 with add_context(tag): 

249 d = tag._serialize_with(self, d) 

250 

251 return d 

252 

253 def to_json(self, **kwargs): 

254 """ 

255 Dump the model as a JSON string. 

256 

257 Args: 

258 **kwargs: extra keyword arguments to pass directly to `json.dumps`. 

259 

260 Returns: 

261 str: a JSON representation of this model. 

262 """ 

263 return json.dumps(self.to_dict(), **kwargs) 

264 

265 @classmethod 

266 def from_dict(cls, d): 

267 """ 

268 Convert a dictionary to an instance of this model. 

269 

270 Args: 

271 d (dict): a serialized version of this model. 

272 

273 Returns: 

274 Model: an instance of this model. 

275 """ 

276 model = cls.__new__(cls) 

277 

278 model_cls = None 

279 tag = model.__class__.__tag__ 

280 while tag and model_cls is not model.__class__: 

281 model_cls = model.__class__ 

282 with add_context(tag): 

283 model, d = tag._deserialize_with(model, d) 

284 tag = model.__class__.__tag__ 

285 

286 for field in reversed(model.__class__.__fields__.values()): 

287 with add_context(field): 

288 model, d = field._deserialize_with(model, d) 

289 

290 model._normalize() 

291 model._validate() 

292 

293 return model 

294 

295 @classmethod 

296 def from_json(cls, s, **kwargs): 

297 """ 

298 Load the model from a JSON string. 

299 

300 Args: 

301 s (str): the JSON string. 

302 **kwargs: extra keyword arguments to pass directly to `json.loads`. 

303 

304 Returns: 

305 Model: an instance of this model. 

306 """ 

307 return cls.from_dict(json.loads(s, **kwargs)) 

308 

309 def _normalize(self): 

310 """ 

311 Normalize all fields on this model, and the model itself. 

312 

313 This is called by the model constructor and on deserialization, so this 

314 is only needed if you modify attributes directly and want to renormalize 

315 the model instance. 

316 """ 

317 for field in self.__class__.__fields__.values(): 

318 with add_context(field): 

319 field._normalize_with(self) 

320 self.normalize() 

321 

322 def normalize(self): 

323 """ 

324 Normalize this model. 

325 

326 Override this method to add any additional normalization to the model. 

327 This will be called after all fields have been normalized. 

328 """ 

329 pass 

330 

331 def _validate(self): 

332 """ 

333 Validate all fields on this model, and the model itself. 

334 

335 This is called by the model constructor and on deserialization, so this 

336 is only needed if you modify attributes directly and want to revalidate 

337 the model instance. 

338 """ 

339 for field in self.__class__.__fields__.values(): 

340 with add_context(field): 

341 field._validate_with(self) 

342 self.validate() 

343 

344 def validate(self): 

345 """ 

346 Validate this model. 

347 

348 Override this method to add any additional validation to the model. This 

349 will be called after all fields have been validated. 

350 """ 

351 pass