oca-technical/odoo-bringout-oca-rest-framework-datamodel/datamodel/tests/test_build_datamodel.py
2025-08-29 15:43:03 +02:00

486 lines
17 KiB
Python

# Copyright 2017 Camptocamp SA
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
from unittest import mock
from marshmallow_objects.models import Model as MarshmallowModel
from odoo import SUPERUSER_ID, api
from .. import fields
from ..core import Datamodel
from .common import DatamodelRegistryCase, TransactionDatamodelCase
class TestBuildDatamodel(DatamodelRegistryCase):
"""Test build of datamodels
All the tests in this suite are based on the same principle with
variations:
* Create new Datamodels (classes inheriting from
:class:`datamodel.core.Datamodel`
* Call :meth:`datamodel.core.Datamodel._build_datamodel` on them
in order to build the 'final class' composed from all the ``_inherit``
and push it in the datamodels registry (``self.datamodel_registry`` here)
* Assert that classes are built, registered, have correct ``__bases__``...
"""
def test_type(self):
"""Ensure that a datamodels are instances of
marshomallow_objects.Model"""
class Datamodel1(Datamodel):
_name = "datamodel1"
class Datamodel2(Datamodel):
_name = "datamodel2"
_inherit = "datamodel1"
self.assertIsInstance(Datamodel1(), MarshmallowModel)
self.assertIsInstance(Datamodel2(), MarshmallowModel)
def test_no_name(self):
"""Ensure that a datamodel has a _name"""
class Datamodel1(Datamodel):
pass
msg = ".*must have a _name.*"
with self.assertRaisesRegex(TypeError, msg):
Datamodel1._build_datamodel(self.datamodel_registry)
def test_register(self):
"""Able to register datamodels in datamodels registry"""
class Datamodel1(Datamodel):
_name = "datamodel1"
class Datamodel2(Datamodel):
_name = "datamodel2"
# build the 'final classes' for the datamodels and check that we find
# them in the datamodels registry
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
self.assertEqual(
["base", "datamodel1", "datamodel2"], list(self.datamodel_registry)
)
# pylint: disable=R7980
def test_inherit_bases(self):
"""Check __bases__ of Datamodel with _inherit"""
class Datamodel1(Datamodel):
_name = "datamodel1"
field_str1 = fields.String(load_default="field_str1")
class Datamodel2(Datamodel):
_inherit = "datamodel1" # pylint:disable=R8180
field_str2 = fields.String(load_default="field_str2")
class Datamodel3(Datamodel):
_inherit = "datamodel1" # pylint:disable=R8180
field_str3 = fields.String(load_default="field_str3")
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
Datamodel3._build_datamodel(self.datamodel_registry)
self.assertEqual(
(Datamodel3, Datamodel2, Datamodel1, self.env.datamodels["base"]),
self.env.datamodels["datamodel1"].__bases__,
)
def test_prototype_inherit_bases(self):
"""Check __bases__ of Datamodel with _inherit and different _name"""
class Datamodel1(Datamodel):
_name = "datamodel1"
field_int = fields.Int(load_default=1)
class Datamodel2(Datamodel):
_name = "datamodel2"
_inherit = "datamodel1"
field_boolean = fields.Boolean(load_default=True)
field_int = fields.Int(load_default=2)
class Datamodel3(Datamodel):
_name = "datamodel3"
_inherit = "datamodel1"
field_float = fields.Float(load_default=0.3)
class Datamodel4(Datamodel):
_name = "datamodel4"
_inherit = ["datamodel2", "datamodel3"]
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
Datamodel3._build_datamodel(self.datamodel_registry)
Datamodel4._build_datamodel(self.datamodel_registry)
self.assertEqual(
(Datamodel1, self.env.datamodels["base"]),
self.env.datamodels["datamodel1"].__bases__,
)
self.assertEqual(
(
Datamodel2,
self.env.datamodels["datamodel1"],
self.env.datamodels["base"],
),
self.env.datamodels["datamodel2"].__bases__,
)
self.assertEqual(
(
Datamodel3,
self.env.datamodels["datamodel1"],
self.env.datamodels["base"],
),
self.env.datamodels["datamodel3"].__bases__,
)
self.assertEqual(
(
Datamodel4,
self.env.datamodels["datamodel2"],
self.env.datamodels["datamodel3"],
self.env.datamodels["base"],
),
self.env.datamodels["datamodel4"].__bases__,
)
def test_final_class_schema(self):
"""Check the MArshmallow schema of the final class"""
class Datamodel1(Datamodel):
_name = "datamodel1"
field_int = fields.Int(load_default=1)
class Datamodel2(Datamodel):
_name = "datamodel2"
_inherit = "datamodel1"
field_boolean = fields.Boolean(load_default=True)
field_int = fields.Int(load_default=2)
class Datamodel3(Datamodel):
_name = "datamodel3"
_inherit = "datamodel1"
field_float = fields.Float(load_default=0.3)
class Datamodel4(Datamodel):
_name = "datamodel4"
_inherit = ["datamodel2", "datamodel3"]
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
Datamodel3._build_datamodel(self.datamodel_registry)
Datamodel4._build_datamodel(self.datamodel_registry)
Datamodel1 = self.env.datamodels["datamodel1"]
Datamodel2 = self.env.datamodels["datamodel2"]
Datamodel3 = self.env.datamodels["datamodel3"]
Datamodel4 = self.env.datamodels["datamodel4"]
self.assertEqual(Datamodel1().dump(), {"field_int": 1})
self.assertDictEqual(
Datamodel2().dump(), {"field_boolean": True, "field_int": 2}
)
self.assertDictEqual(Datamodel3().dump(), {"field_float": 0.3, "field_int": 1})
self.assertDictEqual(
Datamodel4().dump(),
{"field_boolean": True, "field_int": 2, "field_float": 0.3},
)
def test_custom_build(self):
"""Check that we can hook at the end of a Datamodel build"""
class Datamodel1(Datamodel):
_name = "datamodel1"
@classmethod
def _complete_datamodel_build(cls):
# This method should be called after the Datamodel
# is built, and before it is pushed in the registry
cls._build_done = True
Datamodel1._build_datamodel(self.datamodel_registry)
# we inspect that our custom build has been executed
self.assertTrue(self.env.datamodels["datamodel1"]._build_done)
# pylint: disable=W8110
def test_inherit_attrs(self):
"""Check attributes inheritance of Datamodels with _inherit"""
class Datamodel1(Datamodel):
_name = "datamodel1"
msg = "ping"
def say(self):
return "foo"
class Datamodel2(Datamodel):
_name = "datamodel2"
_inherit = "datamodel1"
msg = "pong"
def say(self):
return super(Datamodel2, self).say() + " bar"
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
# we initialize the datamodels, normally we should pass
# an instance of WorkContext, but we don't need a real one
# for this test
datamodel1 = self.env.datamodels["datamodel1"](mock.Mock())
datamodel2 = self.env.datamodels["datamodel2"](mock.Mock())
self.assertEqual("ping", datamodel1.msg)
self.assertEqual("pong", datamodel2.msg)
self.assertEqual("foo", datamodel1.say())
self.assertEqual("foo bar", datamodel2.say())
def test_duplicate_datamodel(self):
"""Check that we can't have 2 datamodels with the same name"""
class Datamodel1(Datamodel):
_name = "datamodel1"
class Datamodel2(Datamodel):
_name = "datamodel1"
Datamodel1._build_datamodel(self.datamodel_registry)
msg = "Datamodel.*already exists.*"
with self.assertRaisesRegex(TypeError, msg):
Datamodel2._build_datamodel(self.datamodel_registry)
def test_no_parent(self):
"""Ensure we can't _inherit a non-existent datamodel"""
class Datamodel1(Datamodel):
_name = "datamodel1"
_inherit = "datamodel1"
msg = "Datamodel.*does not exist in registry.*"
with self.assertRaisesRegex(TypeError, msg):
Datamodel1._build_datamodel(self.datamodel_registry)
def test_no_parent2(self):
"""Ensure we can't _inherit by prototype a non-existent datamodel"""
class Datamodel1(Datamodel):
_name = "datamodel1"
class Datamodel2(Datamodel):
_name = "datamodel2"
_inherit = ["datamodel1", "datamodel3"]
Datamodel1._build_datamodel(self.datamodel_registry)
msg = "Datamodel.*inherits from non-existing datamodel.*"
with self.assertRaisesRegex(TypeError, msg):
Datamodel2._build_datamodel(self.datamodel_registry)
def test_add_inheritance(self):
"""Ensure we can add a new inheritance"""
class Datamodel1(Datamodel):
_name = "datamodel1"
class Datamodel2(Datamodel):
_name = "datamodel2"
class Datamodel2bis(Datamodel):
_name = "datamodel2"
_inherit = ["datamodel2", "datamodel1"]
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
Datamodel2bis._build_datamodel(self.datamodel_registry)
self.assertEqual(
(
Datamodel2bis,
Datamodel2,
self.env.datamodels["datamodel1"],
self.env.datamodels.registry.get("base"),
),
self.env.datamodels["datamodel2"].__bases__,
)
def test_add_inheritance_final_schema(self):
"""Ensure that the Marshmallow schema is updated if we add a
new inheritance"""
class Datamodel1(Datamodel):
_name = "datamodel1"
field_str1 = fields.String(load_default="str1")
class Datamodel2(Datamodel):
_name = "datamodel2"
field_str2 = fields.String(load_default="str2")
class Datamodel2bis(Datamodel):
_name = "datamodel2"
_inherit = ["datamodel2", "datamodel1"]
field_str3 = fields.String(load_default="str3")
Datamodel1._build_datamodel(self.datamodel_registry)
Datamodel2._build_datamodel(self.datamodel_registry)
Datamodel2bis._build_datamodel(self.datamodel_registry)
Datamodel2 = self.env.datamodels["datamodel2"]
self.assertDictEqual(
Datamodel2().dump(),
{"field_str1": "str1", "field_str2": "str2", "field_str3": "str3"},
)
def test_recursion(self):
class Datamodel1(Datamodel):
_name = "datamodel1"
field_str = fields.String()
Datamodel1._build_datamodel(self.datamodel_registry)
for _i in range(0, 1000):
self.env.datamodels["datamodel1"](field_str="1234")
def test_nested_model(self):
"""Test nested model serialization/deserialization"""
class Parent(Datamodel):
_name = "parent"
name = fields.String()
child = fields.NestedModel("child")
class Child(Datamodel):
_name = "child"
field_str = fields.String()
Parent._build_datamodel(self.datamodel_registry)
Child._build_datamodel(self.datamodel_registry)
Parent = self.env.datamodels["parent"]
Child = self.env.datamodels["child"]
instance = Parent(name="Parent", child=Child(field_str="My other string"))
res = instance.dump()
self.assertDictEqual(
res, {"child": {"field_str": "My other string"}, "name": "Parent"}
)
new_instance = instance.load(res)
self.assertEqual(new_instance.name, instance.name)
self.assertEqual(new_instance.child.field_str, instance.child.field_str)
def test_list_nested_model(self):
"""Test list model of nested model serialization/deserialization"""
class Parent(Datamodel):
_name = "parent"
name = fields.String()
list_child = fields.List(fields.NestedModel("child"))
class Child(Datamodel):
_name = "child"
field_str = fields.String()
Parent._build_datamodel(self.datamodel_registry)
Child._build_datamodel(self.datamodel_registry)
Parent = self.env.datamodels["parent"]
Child = self.env.datamodels["child"]
childs = [
Child(field_str="My 1st other string"),
Child(field_str="My 2nd other string"),
]
instance = Parent(name="Parent", list_child=childs)
res = instance.dump()
self.assertDictEqual(
res,
{
"list_child": [
{"field_str": "My 1st other string"},
{"field_str": "My 2nd other string"},
],
"name": "Parent",
},
)
new_instance = instance.load(res)
self.assertEqual(new_instance.name, instance.name)
self.assertEqual(new_instance.list_child, instance.list_child)
def test_many(self):
"""Test loads of many"""
class Item(Datamodel):
_name = "item"
idx = fields.Integer()
Item._build_datamodel(self.datamodel_registry)
Item = self.env.datamodels["item"]
items = Item.load([{"idx": 1}, {"idx": 2}], many=True)
self.assertTrue(len(items), 2)
self.assertEqual([i.idx for i in items], [1, 2])
def test_nested_many(self):
"""Tests loads and dump of model with array of nested model"""
class Parent(Datamodel):
_name = "parent"
items = fields.NestedModel("item", many=True)
class Item(Datamodel):
_name = "item"
idx = fields.Integer()
Parent._build_datamodel(self.datamodel_registry)
Item._build_datamodel(self.datamodel_registry)
Parent = self.env.datamodels["parent"]
Item = self.env.datamodels["item"]
instance = Parent.load({"items": [{"idx": 1}, {"idx": 2}]})
res = instance.dump()
self.assertEqual(res, {"items": [{"idx": 1}, {"idx": 2}]})
new_instance = Parent.load(res)
self.assertEqual(len(new_instance.items), 2)
self.assertEqual([i.idx for i in new_instance.items], [1, 2])
new_instance.items.append(Item(idx=3))
res = new_instance.dump()
self.assertEqual(res, {"items": [{"idx": 1}, {"idx": 2}, {"idx": 3}]})
def test_env(self):
"""
Tests that the current env is always available on datamodel instances
and schema
"""
class Parent(Datamodel):
_name = "parent"
items = fields.NestedModel("item", many=True)
class Item(Datamodel):
_name = "item"
idx = fields.Integer()
Parent._build_datamodel(self.datamodel_registry)
Item._build_datamodel(self.datamodel_registry)
Parent = self.env.datamodels["parent"]
p = Parent()
self.assertEqual(p.env, self.env)
schema = Parent.get_schema()
self.assertEqual(schema._env, self.env)
instance = Parent.load({"items": [{"idx": 1}, {"idx": 2}]})
self.assertEqual(instance.items[0].env, self.env)
schema = instance.items[0].get_schema()
self.assertEqual(schema._env, self.env)
another_env = api.Environment(self.env.registry.cursor(), SUPERUSER_ID, {})
new_p = another_env.datamodels["parent"]()
self.assertEqual(new_p.env, another_env)
class TestRegistryAccess(TransactionDatamodelCase):
def test_registry_access(self):
"""Check the access to the registry directly on tnv"""
base = self.env.datamodels["base"]
self.assertIsInstance(base(), MarshmallowModel)