-
-
Notifications
You must be signed in to change notification settings - Fork 298
/
core.py
428 lines (327 loc) · 14.7 KB
/
core.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# Copyright 2017 Camptocamp SA
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
import logging
from collections import OrderedDict, defaultdict
from contextlib import ExitStack
from marshmallow import INCLUDE
from odoo.api import Environment
from odoo.tools import LastOrderedSet, OrderedSet
_logger = logging.getLogger(__name__)
try:
import marshmallow
from marshmallow_objects.models import Model as MarshmallowModel, ModelMeta
except ImportError:
_logger.debug("Cannot import 'marshmallow_objects'.")
# The Cache size represents the number of items, so the number
# of datamodels (include abstract datamodels) we will keep in the LRU
# cache. We would need stats to know what is the average but this is a bit
# early.
DEFAULT_CACHE_SIZE = 512
# this is duplicated from odoo.models.MetaModel._get_addon_name() which we
# unfortunately can't use because it's an instance method and should have been
# a @staticmethod
def _get_addon_name(full_name):
# The (Odoo) module name can be in the ``odoo.addons`` namespace
# or not. For instance, module ``sale`` can be imported as
# ``odoo.addons.sale`` (the right way) or ``sale`` (for backward
# compatibility).
module_parts = full_name.split(".")
if len(module_parts) > 2 and module_parts[:2] == ["odoo", "addons"]:
addon_name = module_parts[2]
else:
addon_name = module_parts[0]
return addon_name
def _get_nested_schemas(schema):
res = [schema]
for field in schema.fields.values():
if getattr(field, "schema", None):
res += _get_nested_schemas(field.schema)
return res
class DatamodelDatabases(dict):
"""Holds a registry of datamodels for each database"""
class DatamodelRegistry(object):
"""Store all the datamodel and allow to retrieve them by name
The key is the ``_name`` of the datamodels.
This is an OrderedDict, because we want to keep the registration order of
the datamodels, addons loaded first have their datamodels found first.
The :attr:`ready` attribute must be set to ``True`` when all the datamodels
are loaded.
"""
def __init__(self, cachesize=DEFAULT_CACHE_SIZE):
self._datamodels = OrderedDict()
self._loaded_modules = set()
self.ready = False
def __getitem__(self, key):
return self._datamodels[key]
def __setitem__(self, key, value):
self._datamodels[key] = value
def __contains__(self, key):
return key in self._datamodels
def get(self, key, default=None):
return self._datamodels.get(key, default)
def __iter__(self):
return iter(self._datamodels)
def load_datamodels(self, module):
if module in self._loaded_modules:
return
for datamodel_class in MetaDatamodel._modules_datamodels[module]:
datamodel_class._build_datamodel(self)
self._loaded_modules.add(module)
# We will store a DatamodeltRegistry per database here,
# it will be cleared and updated when the odoo's registry is rebuilt
_datamodel_databases = DatamodelDatabases()
@marshmallow.post_load
def __make_object__(self, data, **kwargs):
datamodel = self._env.datamodels[self._datamodel_name]
return datamodel(__post_load__=True, __schema__=self, **data)
class MetaDatamodel(ModelMeta):
"""Metaclass for Datamodel
Every new :class:`Datamodel` will be added to ``_modules_datamodels``,
that will be used by the datamodel builder.
"""
_modules_datamodels = defaultdict(list)
def __init__(self, name, bases, attrs):
if not self._register:
self._register = True
super(MetaDatamodel, self).__init__(name, bases, attrs)
return
# If datamodels are declared in tests, exclude them from the
# "datamodels of the addon" list. If not, when we use the
# "load_datamodels" method, all the test datamodels would be loaded.
# This should never be an issue when running the app normally, as the
# Python tests should never be executed. But this is an issue when a
# test creates a test datamodels for the purpose of the test, then a
# second tests uses the "load_datamodels" to load all the addons of the
# module: it will load the datamodel of the previous test.
if "tests" in self.__module__.split("."):
return
if not hasattr(self, "_module"):
self._module = _get_addon_name(self.__module__)
self._modules_datamodels[self._module].append(self)
def __call__(self, *args, **kwargs):
"""Allow to set any field (including 'dump_only') at instantiation
This is not an issue thanks to cleanup during (de)serialization
"""
kwargs["unknown"] = kwargs.get("unknown", INCLUDE)
return super().__call__(*args, **kwargs)
class Datamodel(MarshmallowModel, metaclass=MetaDatamodel):
"""Main Datamodel Model
All datamodels have a Python inheritance either on
:class:`Datamodel`.
Inheritance mechanism
The inheritance mechanism is like the Odoo's one for Models. Each
datamodel has a ``_name``. This is the absolute minimum in a Datamodel
class.
::
from marshmallow import fields
from odoo.addons.datamodel.core import Datamodel
class MyDatamodel(Datamodel):
_name = 'my.datamodel'
name = fields.String()
Every datamodel implicitly inherit from the `'base'` datamodel.
There are two close but distinct inheritance types, which look
familiar if you already know Odoo. The first uses ``_inherit`` with
an existing name, the name of the datamodel we want to extend. With
the following example, ``my.datamodel`` is now able to speak and to
yell.
::
class MyDatamodel(Datamodel): # name of the class does not matter
_inherit = 'my.datamodel'
The second has a different ``_name``, it creates a new datamodel,
including the behavior of the inherited datamodel, but without
modifying it.
::
class AnotherDatamodel(Datamodel):
_name = 'another.datamodel'
_inherit = 'my.datamodel'
age = fields.Int()
"""
_register = False
_env = None # Odoo Environment
# used for inheritance
_name = None #: Name of the datamodel
#: Name or list of names of the datamodel(s) to inherit from
_inherit = None
def __init__(self, context=None, partial=None, env=None, **kwargs):
self._env = env or type(self)._env
super().__init__(context=context, partial=partial, **kwargs)
@property
def env(self):
return self._env
@classmethod
def get_schema(cls, **kwargs):
"""
Get a marshmallow schema instance
:param kwargs:
:return:
"""
return cls.__get_schema_class__(**kwargs)
@classmethod
def validate(cls, data, context=None, many=None, partial=None, unknown=None):
schema = cls.__get_schema_class__(
context=context, partial=partial, unknown=unknown
)
all_schemas = _get_nested_schemas(schema)
with ExitStack() as stack:
# propagate 'unknown' to each nested schema during validate
for nested_schema in all_schemas:
stack.enter_context(cls.propagate_unknwown(nested_schema, unknown))
return schema.validate(data, many=many, partial=partial)
@classmethod
def _build_datamodel(cls, registry):
"""Instantiate a given Datamodel in the datamodels registry.
This method is called at the end of the Odoo's registry build. The
caller is :meth:`datamodel.builder.DatamodelBuilder.load_datamodels`.
It generates new classes, which will be the Datamodel classes we will
be using. The new classes are generated following the inheritance
of ``_inherit``. It ensures that the ``__bases__`` of the generated
Datamodel classes follow the ``_inherit`` chain.
Once a Datamodel class is created, it adds it in the Datamodel Registry
(:class:`DatamodelRegistry`), so it will be available for
lookups.
At the end of new class creation, a hook method
:meth:`_complete_datamodel_build` is called, so you can customize
further the created datamodels.
The following code is roughly the same than the Odoo's one for
building Models.
"""
# In the simplest case, the datamodel's registry class inherits from
# cls and the other classes that define the datamodel in a flat
# hierarchy. The registry contains the instance ``datamodel`` (on the
# left). Its class, ``DatamodelClass``, carries inferred metadata that
# is shared between all the datamodel's instances for this registry
# only.
#
# class A1(Datamodel): Datamodel
# _name = 'a' / | \
# A3 A2 A1
# class A2(Datamodel): \ | /
# _inherit = 'a' DatamodelClass
#
# class A3(Datamodel):
# _inherit = 'a'
#
# When a datamodel is extended by '_inherit', its base classes are
# modified to include the current class and the other inherited
# datamodel classes.
# Note that we actually inherit from other ``DatamodelClass``, so that
# extensions to an inherited datamodel are immediately visible in the
# current datamodel class, like in the following example:
#
# class A1(Datamodel):
# _name = 'a' Datamodel
# / / \ \
# class B1(Datamodel): / A2 A1 \
# _name = 'b' / \ / \
# B2 DatamodelA B1
# class B2(Datamodel): \ | /
# _name = 'b' \ | /
# _inherit = ['b', 'a'] \ | /
# DatamodelB
# class A2(Datamodel):
# _inherit = 'a'
# determine inherited datamodels
parents = cls._inherit
if isinstance(parents, str):
parents = [parents]
elif parents is None:
parents = []
if cls._name in registry and not parents:
raise TypeError(
"Datamodel %r (in class %r) already exists. "
"Consider using _inherit instead of _name "
"or using a different _name." % (cls._name, cls)
)
# determine the datamodel's name
name = cls._name or (len(parents) == 1 and parents[0])
if not name:
raise TypeError("Datamodel %r must have a _name" % cls)
# all datamodels except 'base' implicitly inherit from 'base'
if name != "base":
parents = list(parents) + ["base"]
# create or retrieve the datamodel's class
if name in parents:
if name not in registry:
raise TypeError("Datamodel %r does not exist in registry." % name)
# determine all the classes the datamodel should inherit from
bases = LastOrderedSet([cls])
for parent in parents:
if parent not in registry:
raise TypeError(
"Datamodel %r inherits from non-existing datamodel %r."
% (name, parent)
)
parent_class = registry[parent]
if parent == name:
for base in parent_class.__bases__:
bases.add(base)
else:
bases.add(parent_class)
parent_class._inherit_children.add(name)
if name in parents:
DatamodelClass = registry[name]
# Add the new bases to the existing model since the class into
# the registry could already be used into an inherit
DatamodelClass.__bases__ = tuple(bases)
# We must update the marshmallow schema on the existing datamodel
# class to include those inherited
parent_schemas = []
for parent in bases:
if issubclass(parent, MarshmallowModel):
parent_schemas.append(parent.__schema_class__)
schema_class = type(name + "Schema", tuple(parent_schemas), {})
DatamodelClass.__schema_class__ = schema_class
else:
attrs = {
"_name": name,
"_register": False,
# names of children datamodel
"_inherit_children": OrderedSet(),
}
if name == "base":
attrs["_registry"] = registry
DatamodelClass = type(name, tuple(bases), attrs)
setattr(DatamodelClass.__schema_class__, "_registry", registry) # noqa: B010
setattr(DatamodelClass.__schema_class__, "_datamodel_name", name) # noqa: B010
setattr( # noqa: B010
DatamodelClass.__schema_class__, "__make_object__", __make_object__
)
DatamodelClass._complete_datamodel_build()
registry[name] = DatamodelClass
return DatamodelClass
@classmethod
def _complete_datamodel_build(cls):
"""Complete build of the new datamodel class
After the datamodel has been built from its bases, this method is
called, and can be used to customize the class before it can be used.
Nothing is done in the base Datamodel, but a Datamodel can inherit
the method to add its own behavior.
"""
# makes the datamodels registry available on env
class DataModelFactory(object):
"""Factory for datamodels
This factory ensures the propagation of the environment to the
instanciated datamodels and related schema.
"""
__slots__ = ("env", "registry")
def __init__(self, env, registry):
self.env = env
self.registry = registry
def __getitem__(self, key):
model = self.registry[key]
model._env = self.env
@classmethod
def __get_schema_class__(cls, **kwargs):
cls = cls.__schema_class__(**kwargs)
cls._env = self.env
return cls
model.__get_schema_class__ = __get_schema_class__
return model
@property
def datamodels(self):
if not hasattr(self, "_datamodels_factory"):
factory = DataModelFactory(self, _datamodel_databases.get(self.cr.dbname))
self._datamodels_factory = factory
return self._datamodels_factory
Environment.datamodels = datamodels