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
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
1"""
2This module defines the core `~serde.Model` class.
3"""
5import inspect
6import json
7from collections import OrderedDict
9from serde.exceptions import ContextError, add_context
10from serde.fields import Field, _resolve
11from serde.utils import dict_partition, zip_until_right
14__all__ = ['Model']
17class Fields(OrderedDict):
18 """
19 An `~collections.OrderedDict` that allows value access with dot notation.
20 """
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)
32class ModelType(type):
33 """
34 A metaclass for a `Model`.
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 """
42 @staticmethod
43 def _pop_meta(attrs):
44 """
45 Handle the Meta class attributes.
46 """
47 abstract = False
48 tag = None
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']
57 return abstract, tag
59 def __new__(cls, cname, bases, attrs):
60 """
61 Create a new `Model` class.
63 Args:
64 cname (str): the class name.
65 bases (tuple): the base classes.
66 attrs (dict): the attributes for this class.
68 Returns:
69 Model: a new model class.
70 """
71 parent = None
72 abstract, tag = cls._pop_meta(attrs)
74 # Split the attrs into Fields and non-Fields.
75 fields, final_attrs = dict_partition(attrs, lambda k, v: isinstance(v, Field))
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 )
87 # Create our Model class.
88 model_cls = super(ModelType, cls).__new__(cls, cname, bases, final_attrs)
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 = []
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
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
121 return model_cls
123 @property
124 def __abstract__(cls):
125 """
126 Whether this model class is abstract or not.
127 """
128 return cls._abstract
130 @property
131 def __parent__(cls):
132 """
133 This model class's parent model class.
134 """
135 return cls._parent
137 @property
138 def __fields__(cls):
139 """
140 A map of attribute name to field instance.
141 """
142 return cls._fields.copy()
144 @property
145 def __tag__(cls):
146 """
147 The model class's tag (or None).
148 """
149 return cls._tag
151 @property
152 def __tags__(cls):
153 """
154 The model class's tag and all parent class's tags.
155 """
156 return cls._tags[:]
159class Model(object, metaclass=ModelType):
160 """
161 The base model.
162 """
164 def __init__(self, *args, **kwargs):
165 """
166 Create a new model.
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 )
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 )
194 for field in self.__class__.__fields__.values():
195 with add_context(field):
196 field._instantiate_with(self, kwargs)
198 if kwargs:
199 kwarg = next(iter(kwargs.keys()))
200 raise TypeError(f'__init__() got an unexpected keyword argument {kwarg!r}')
202 self._normalize()
203 self._validate()
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 )
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 )
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 )
234 def to_dict(self):
235 """
236 Convert this model to a dictionary.
238 Returns:
239 ~collections.OrderedDict: the model serialized as a dictionary.
240 """
241 d = OrderedDict()
243 for field in self.__class__.__fields__.values():
244 with add_context(field):
245 d = field._serialize_with(self, d)
247 for tag in reversed(self.__class__.__tags__):
248 with add_context(tag):
249 d = tag._serialize_with(self, d)
251 return d
253 def to_json(self, **kwargs):
254 """
255 Dump the model as a JSON string.
257 Args:
258 **kwargs: extra keyword arguments to pass directly to `json.dumps`.
260 Returns:
261 str: a JSON representation of this model.
262 """
263 return json.dumps(self.to_dict(), **kwargs)
265 @classmethod
266 def from_dict(cls, d):
267 """
268 Convert a dictionary to an instance of this model.
270 Args:
271 d (dict): a serialized version of this model.
273 Returns:
274 Model: an instance of this model.
275 """
276 model = cls.__new__(cls)
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__
286 for field in reversed(model.__class__.__fields__.values()):
287 with add_context(field):
288 model, d = field._deserialize_with(model, d)
290 model._normalize()
291 model._validate()
293 return model
295 @classmethod
296 def from_json(cls, s, **kwargs):
297 """
298 Load the model from a JSON string.
300 Args:
301 s (str): the JSON string.
302 **kwargs: extra keyword arguments to pass directly to `json.loads`.
304 Returns:
305 Model: an instance of this model.
306 """
307 return cls.from_dict(json.loads(s, **kwargs))
309 def _normalize(self):
310 """
311 Normalize all fields on this model, and the model itself.
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()
322 def normalize(self):
323 """
324 Normalize this model.
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
331 def _validate(self):
332 """
333 Validate all fields on this model, and the model itself.
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()
344 def validate(self):
345 """
346 Validate this model.
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