# 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)