@@ -386,22 +386,18 @@
Pydantic
To define your own pydantic model you just need to create a class that inherits from
-odoo.addons.pydantic.models.BaseModel
+
odoo.addons.pydantic.models.BaseModel or a subclass of.
from odoo.addons.pydantic.models import BaseModel
from pydantic import Field
class PartnerShortInfo(BaseModel):
- _name = "partner.short.info"
id: str
name: str
class PartnerInfo(BaseModel):
- _name = "partner.info"
- _inherit = "partner.short.info"
-
street: str
street2: str = None
zip_code: str = None
@@ -409,35 +405,79 @@
phone: str = None
is_componay : bool = Field(None)
-
As for odoo models, you can extend the base pydantic model by inheriting of base.
+
In the preceding code, 2 new models are created, one for each class. If you
+want to extend an existing model, you must pass the extended pydantic model
+trough the extends parameter on class declaration.
+
+class Coordinate(models.BaseModel):
+ lat = 0.1
+ lng = 10.1
+
+class PartnerInfoWithCoordintate(PartnerInfo, extends=PartnerInfo):
+ coordinate: Coordinate = None
+
+
PartnerInfoWithCoordintate extends PartnerInfo. IOW, Base class are now the
+same and define the same fields and methods. They can be used indifferently into
+the code. All the logic will be provided by the aggregated class.
+
+partner1 = PartnerInfo.construct()
+partner2 = PartnerInfoWithCoordintate.construct()
+
+assert partner1.__class__ == partner2.__class__
+assert PartnerInfo.schema() == PartnerInfoWithCoordintate.schema()
+
+
+
Note
+
Since validation occurs on instance creation, it’s important to avoid to
+create an instance of a Pydantic class by usign the normal instance
+constructor partner = PartnerInfo(..). In such a case, if the class is
+extended by an other addon and a required field is added, this code will
+no more work. It’s therefore a good practice to use the cosntruct() class
+method to create a pydantic instance.
+
+
In contrast to Odoo, access to a Pydantic class is not done through a specific
+registry. To use a Pydantic class, you just have to import it in your module
+and write your code like in any other python application.
-class Base(BaseModel):
- _inherit = "base"
+from odoo.addons.my_addons.datamodels import PartnerInfo
+from odoo import models
- def _my_method(self):
- pass
+class ResPartner(models.Basemodel):
+ _inherit = "res.partner"
+
+ def to_json(self):
+ return [i._to_partner_info().json() for i in self]
+
+ def _to_partner_info(self):
+ self.ensure_one()
+ pInfo = PartnerInfo.construct(id=self.id, name=self.name, street=self.street, city=self.city)
+ return pInfo
-
Pydantic model classes are available through the pydantic_registry registry provided by the Odoo’s environment.
To support pydantic models that map to Odoo models, Pydantic model instances can
be created from arbitrary odoo model instances by mapping fields from odoo
-models to fields defined by the pydantic model. To ease the mapping,
-your pydantic model should inherit from ‘odoo_orm_mode’
+models to fields defined by the pydantic model. To ease the mapping, the addon
+provide a utility class
odoo.addons.pydantic.utils.GenericOdooGetter.
-class UserInfo(models.BaseModel):
- _name = "user"
- _inherit = "odoo_orm_mode"
+import pydantic
+from odoo.addons.pydantic import models, utils
+
+class Group(models.BaseModel):
name: str
- groups: List["group"] = pydantic.Field(alias="groups_id")
+ class Config:
+ orm_mode = True
+ getter_dict = utils.GenericOdooGetter
-class Group(models.BaseModel):
- _name="group"
- _inherit = "odoo_orm_mode"
+class UserInfo(models.BaseModel):
name: str
+ groups: List[Group] = pydantic.Field(alias="groups_id")
+
+ class Config:
+ orm_mode = True
+ getter_dict = utils.GenericOdooGetter
user = self.env.user
-UserInfoCls = self.env.pydantic_registry["user"]
-user_info = UserInfoCls.from_orm(user)
+user_info = UserInfo.from_orm(user)
See the official Pydantic documentation to discover all the available functionalities.
@@ -449,10 +489,10 @@
-
Current maintainer:
+
This module is maintained by the OCA.
+

+
OCA, or the Odoo Community Association, is a nonprofit organization whose
+mission is to support the collaborative development of Odoo features and
+promote its widespread use.
+
Current maintainer:

-
This module is part of the oca/rest-framework project on GitHub.
-
You are welcome to contribute.
+
This module is part of the OCA/rest-framework project on GitHub.
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
diff --git a/pydantic/tests/common.py b/pydantic/tests/common.py
index 0abf31268..a0ae21d18 100644
--- a/pydantic/tests/common.py
+++ b/pydantic/tests/common.py
@@ -9,6 +9,7 @@
from odoo.tests import common
from .. import registry, utils
+from ..context import pydantic_registry
from ..models import BaseModel
@@ -39,17 +40,17 @@ def setUpPydantic(cls):
builder.build_registry(pydantic_registry, states=("installed",))
# build the pydantic classes of the current tested addon
current_addon = utils._get_addon_name(cls.__module__)
- builder.build_classes(current_addon, pydantic_registry)
+ pydantic_registry.init_registry([current_addon])
# pylint: disable=W8106
def setUp(self):
# should be ready only during tests, never during installation
# of addons
- self._pydantics_registry.ready = True
+ token = pydantic_registry.set(self._pydantics_registry)
@self.addCleanup
def notready():
- self._pydantics_registry.ready = False
+ pydantic_registry.reset(token)
class TransactionPydanticCase(common.TransactionCase, PydanticMixin):
@@ -140,7 +141,7 @@ def setUp(self):
# it will be our temporary pydantic registry for our test session
self.pydantic_registry = registry.PydanticClassesRegistry()
- # it builds the 'final pydantic' for every pydantic of the
+ # it builds the 'final pydantic' class for every pydantic class of the
# 'pydantic' addon and push them in the pydantic registry
self.pydantic_registry.load_pydantic_classes("pydantic")
# build the pydantic classes of every installed addons already installed
@@ -174,6 +175,12 @@ def _close_and_roolback():
# the pydantic classes registry, but we don't mind for the tests.
self.pydantic_registry.ready = True
+ token = pydantic_registry.set(self.pydantic_registry)
+
+ @self.addCleanup
+ def notready():
+ pydantic_registry.reset(token)
+
def tearDown(self):
super(PydanticRegistryCase, self).tearDown()
self.restore_registry()
@@ -182,10 +189,12 @@ def _load_module_pydantics(self, module):
self.pydantic_registry.load_pydantics(module)
def _build_pydantic_classes(self, *classes):
- for cls in classes:
- self.pydantic_registry.load_pydantic_class_def(cls)
- self.pydantic_registry.build_pydantic_classes()
- self.pydantic_registry.update_forward_refs()
+ with self.pydantic_registry.build_mode():
+ for cls in classes:
+ self.pydantic_registry.load_pydantic_class_def(cls)
+ self.pydantic_registry.build_pydantic_classes()
+ self.pydantic_registry.update_forward_refs()
+ self.pydantic_registry.resolve_submodel_fields()
def backup_registry(self):
self._original_classes_by_module = collections.defaultdict(list)
diff --git a/pydantic/tests/test_pydantic.py b/pydantic/tests/test_pydantic.py
index a7eab8fc7..76d2133ef 100644
--- a/pydantic/tests/test_pydantic.py
+++ b/pydantic/tests/test_pydantic.py
@@ -5,22 +5,20 @@
import pydantic
-from .. import models
-from .common import PydanticRegistryCase, TransactionPydanticRegistryCase
+from .. import models, utils
+from .common import PydanticRegistryCase
class TestPydantic(PydanticRegistryCase):
def test_simple_inheritance(self):
class Location(models.BaseModel):
- _name = "location"
lat = 0.1
lng = 10.1
def test(self) -> str:
return "location"
- class ExtendedLocation(models.BaseModel):
- _inherit = "location"
+ class ExtendedLocation(Location, extends=Location):
name: str
def test(self, return_super: bool = False) -> str:
@@ -29,111 +27,130 @@ def test(self, return_super: bool = False) -> str:
return "extended"
self._build_pydantic_classes(Location, ExtendedLocation)
- ClsLocation = self.pydantic_registry["location"]
+ ClsLocation = self.pydantic_registry[Location.__xreg_name__]
self.assertTrue(issubclass(ClsLocation, ExtendedLocation))
self.assertTrue(issubclass(ClsLocation, Location))
- properties = ClsLocation.schema().get("properties", {}).keys()
- self.assertSetEqual({"lat", "lng", "name"}, set(properties))
- location = ClsLocation(name="name", lng=5.0, lat=4.2)
- self.assertDictEqual(location.dict(), {"lat": 4.2, "lng": 5.0, "name": "name"})
- self.assertEqual(location.test(), "extended")
- self.assertEqual(location.test(return_super=True), "location")
+
+ # check that the behaviour is the same for all the definitions
+ # of the same model...
+ classes = Location, ExtendedLocation, ClsLocation
+ for cls in classes:
+ schema = cls.schema()
+ properties = schema.get("properties", {}).keys()
+ self.assertEqual(schema.get("title"), "Location")
+ self.assertSetEqual({"lat", "lng", "name"}, set(properties))
+ location = cls(name="name", lng=5.0, lat=4.2)
+ self.assertDictEqual(
+ location.dict(), {"lat": 4.2, "lng": 5.0, "name": "name"}
+ )
+ self.assertEqual(location.test(), "extended")
+ self.assertEqual(location.test(return_super=True), "location")
def test_composite_inheritance(self):
class Coordinate(models.BaseModel):
- _name = "coordinate"
lat = 0.1
lng = 10.1
class Name(models.BaseModel):
- _name = "name"
name: str
- class Location(models.BaseModel):
- _name = "location"
- _inherit = ["name", "coordinate"]
+ class Location(Coordinate, Name):
+ pass
self._build_pydantic_classes(Coordinate, Name, Location)
- self.assertIn("coordinate", self.pydantic_registry)
- self.assertIn("name", self.pydantic_registry)
- self.assertIn("location", self.pydantic_registry)
- ClsLocation = self.pydantic_registry["location"]
+ ClsLocation = self.pydantic_registry[Location.__xreg_name__]
self.assertTrue(issubclass(ClsLocation, Coordinate))
self.assertTrue(issubclass(ClsLocation, Name))
- properties = ClsLocation.schema().get("properties", {}).keys()
- self.assertSetEqual({"lat", "lng", "name"}, set(properties))
- location = ClsLocation(name="name", lng=5.0, lat=4.2)
- self.assertDictEqual(location.dict(), {"lat": 4.2, "lng": 5.0, "name": "name"})
- def test_model_relation(self):
- class Person(models.BaseModel):
- _name = "person"
- name: str
- coordinate: "coordinate"
-
- class Coordinate(models.BaseModel):
- _name = "coordinate"
- lat = 0.1
- lng = 10.1
-
- self._build_pydantic_classes(Person, Coordinate)
- self.assertIn("coordinate", self.pydantic_registry)
- self.assertIn("person", self.pydantic_registry)
- ClsPerson = self.pydantic_registry["person"]
- ClsCoordinate = self.pydantic_registry["coordinate"]
- person = ClsPerson(name="test", coordinate={"lng": 5.0, "lat": 4.2})
- coordinate = person.coordinate
- self.assertTrue(isinstance(coordinate, Coordinate))
- # sub schema are stored into the definition property
- definitions = ClsPerson.schema().get("definitions", {})
- self.assertIn("coordinate", definitions)
- self.assertDictEqual(definitions["coordinate"], ClsCoordinate.schema())
-
- def test_inherit_bases(self):
- """ Check all BaseModels inherit from base """
+ # check that the behaviour is the same for all the definitions
+ # of the same model...
+ classes = Location, ClsLocation
+ for cls in classes:
+ properties = cls.schema().get("properties", {}).keys()
+ self.assertSetEqual({"lat", "lng", "name"}, set(properties))
+ location = cls(name="name", lng=5.0, lat=4.2)
+ self.assertDictEqual(
+ location.dict(), {"lat": 4.2, "lng": 5.0, "name": "name"}
+ )
+ def test_model_relation(self):
class Coordinate(models.BaseModel):
- _name = "coordinate"
lat = 0.1
lng = 10.1
- class Base(models.BaseModel):
- _inherit = "base"
- title: str = "My title"
-
- self._build_pydantic_classes(Coordinate, Base)
- self.assertIn("coordinate", self.pydantic_registry)
- self.assertIn("base", self.pydantic_registry)
- ClsCoordinate = self.pydantic_registry["coordinate"]
- self.assertTrue(issubclass(ClsCoordinate, models.Base))
- properties = ClsCoordinate.schema().get("properties", {}).keys()
- self.assertSetEqual({"lat", "lng", "title"}, set(properties))
+ class Person(models.BaseModel):
+ name: str
+ coordinate: Coordinate
+
+ class ExtendedCoordinate(Coordinate, extends=Coordinate):
+ country: str = None
+
+ self._build_pydantic_classes(Person, Coordinate, ExtendedCoordinate)
+ ClsPerson = self.pydantic_registry[Person.__xreg_name__]
+
+ # check that the behaviour is the same for all the definitions
+ # of the same model...
+ classes = Person, ClsPerson
+ for cls in classes:
+ person = cls(
+ name="test",
+ coordinate={"lng": 5.0, "lat": 4.2, "country": "belgium"},
+ )
+ coordinate = person.coordinate
+ self.assertTrue(isinstance(coordinate, Coordinate))
+ # sub schema are stored into the definition property
+ definitions = ClsPerson.schema().get("definitions", {})
+ self.assertIn("Coordinate", definitions)
+ coordinate_properties = (
+ definitions["Coordinate"].get("properties", {}).keys()
+ )
+ self.assertSetEqual({"lat", "lng", "country"}, set(coordinate_properties))
def test_from_orm(self):
- class User(models.BaseModel):
- _name = "user"
- _inherit = "odoo_orm_mode"
+ class Group(models.BaseModel):
name: str
- groups: List["group"] = pydantic.Field(alias="groups_id") # noqa: F821
- class Group(models.BaseModel):
- _name = "group"
- _inherit = "odoo_orm_mode"
+ class User(models.BaseModel):
name: str
+ groups: List[Group] = pydantic.Field(alias="groups_id") # noqa: F821
+
+ class OrmMode(models.BaseModel):
+ class Config:
+ orm_mode = True
+ getter_dict = utils.GenericOdooGetter
- self._build_pydantic_classes(User, Group)
- ClsUser = self.pydantic_registry["user"]
+ class GroupOrm(Group, OrmMode, extends=Group):
+ pass
+
+ class UserOrm(User, OrmMode, extends=User):
+ pass
+
+ self._build_pydantic_classes(Group, User, OrmMode, GroupOrm, UserOrm)
+ ClsUser = self.pydantic_registry[User.__xreg_name__]
+
+ # check that the behaviour is the same for all the definitions
+ # of the same model...
+ classes = User, UserOrm, ClsUser
odoo_user = self.env.user
- user = ClsUser.from_orm(odoo_user)
- expected = {
- "name": odoo_user.name,
- "groups": [{"name": g.name} for g in odoo_user.groups_id],
- }
- self.assertDictEqual(user.dict(), expected)
-
-
-class TestRegistryAccess(TransactionPydanticRegistryCase):
- def test_registry_access(self):
- """Check the access to the registry directly on Env"""
- base = self.env.pydantic_registry["base"]
- self.assertIsInstance(base(), models.BaseModel)
+ for cls in classes:
+ user = cls.from_orm(odoo_user)
+ expected = {
+ "name": odoo_user.name,
+ "groups": [{"name": g.name} for g in odoo_user.groups_id],
+ }
+ self.assertDictEqual(user.dict(), expected)
+
+ def test_instance(self):
+ class Location(models.BaseModel):
+ lat = 0.1
+ lng = 10.1
+
+ class ExtendedLocation(Location, extends=Location):
+ name: str
+
+ self._build_pydantic_classes(Location, ExtendedLocation)
+
+ inst1 = Location.construct()
+ inst2 = ExtendedLocation.construct()
+ self.assertEqual(inst1.__class__, inst2.__class__)
+ self.assertEqual(inst1.schema(), inst2.schema())
diff --git a/pydantic/utils.py b/pydantic/utils.py
index bee64c9e7..20ee877ca 100644
--- a/pydantic/utils.py
+++ b/pydantic/utils.py
@@ -23,26 +23,23 @@ class GenericOdooGetter(GetterDict):
import pydantic
from odoo.addons.pydantic import models, utils
- class UserInfo(models.BaseModel):
- _name = "user"
+ class Group(models.BaseModel):
name: str
- groups: List["group"] = pydantic.Field(alias="groups_id")
class Config:
orm_mode = True
getter_dict = utils.GenericOdooGetter
- class Group(models.BaseModel):
- _name="group"
+ class UserInfo(models.BaseModel):
name: str
+ groups: List[Group] = pydantic.Field(alias="groups_id")
class Config:
orm_mode = True
getter_dict = utils.GenericOdooGetter
user = self.env.user
- UserInfoCls = self.env.pydantic_registry["user"]
- user_info = UserInfoCls.from_orm(user)
+ user_info = UserInfo.from_orm(user)
To avoid having to repeat the specific configuration required for the
`from_orm` method into each pydantic model, "odoo_orm_mode" can be used
diff --git a/requirements.txt b/requirements.txt
index 700ae5ca0..20c95515f 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,6 +2,7 @@
apispec
apispec>=4.0.0
cerberus
+contextvars;python_version<"3.7"
jsondiff
marshmallow
marshmallow-objects>=2.0.0
diff --git a/setup/pydantic/setup.py b/setup/pydantic/setup.py
index 28c57bb64..3665ea16f 100644
--- a/setup/pydantic/setup.py
+++ b/setup/pydantic/setup.py
@@ -2,5 +2,9 @@
setuptools.setup(
setup_requires=['setuptools-odoo'],
- odoo_addon=True,
+ odoo_addon={
+ "external_dependencies_override": {
+ "python": {"contextvars": 'contextvars;python_version<"3.7"'}
+ }
+ },
)