Skip to content

Commit 19499a4

Browse files
committed
[ADD] pydantic: allows pydantic usage into Odoo
1 parent 6759bc3 commit 19499a4

File tree

19 files changed

+1563
-0
lines changed

19 files changed

+1563
-0
lines changed

pydantic/README.rst

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
========
2+
Pydantic
3+
========
4+
5+
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
6+
!! This file is generated by oca-gen-addon-readme !!
7+
!! changes will be overwritten. !!
8+
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
9+
10+
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
11+
:target: https://odoo-community.org/page/development-status
12+
:alt: Beta
13+
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
14+
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
15+
:alt: License: LGPL-3
16+
.. |badge3| image:: https://img.shields.io/badge/github-oca%2Frest--framework-lightgray.png?logo=github
17+
:target: https://github.com/oca/rest-framework/tree/14.0/pydantic
18+
:alt: oca/rest-framework
19+
20+
|badge1| |badge2| |badge3|
21+
22+
This addon allows you to define inheritable `Pydantic classes <https://pydantic-docs.helpmanual.io/>`_.
23+
24+
**Table of contents**
25+
26+
.. contents::
27+
:local:
28+
29+
Usage
30+
=====
31+
32+
To define your own pydantic model you just need to create a class that inherits from
33+
``odoo.addons.pydantic.models.BaseModel``
34+
35+
.. code-block:: python
36+
37+
from odoo.addons.pydantic.models import BaseModel
38+
from pydantic import Field
39+
40+
41+
class PartnerShortInfo(BaseModel):
42+
_name = "partner.short.info"
43+
id: str
44+
name: str
45+
46+
47+
class PartnerInfo(BaseModel):
48+
_name = "partner.info"
49+
_inherit = "partner.short.info"
50+
51+
street: str
52+
street2: str = None
53+
zip_code: str = None
54+
city: str
55+
phone: str = None
56+
is_componay : bool = Field(None)
57+
58+
59+
As for odoo models, you can extend the `base` pydantic model by inheriting of `base`.
60+
61+
.. code-block:: python
62+
63+
class Base(BaseModel):
64+
_inherit = "base"
65+
66+
def _my_method(self):
67+
pass
68+
69+
Pydantic model classes are available through the `pydantic_registry` registry provided by the Odoo's environment.
70+
71+
To support pydantic models that map to Odoo models, Pydantic model instances can
72+
be created from arbitrary odoo model instances by mapping fields from odoo
73+
models to fields defined by the pydantic model. To ease the mapping,
74+
your pydantic model should inherit from 'odoo_orm_mode'
75+
76+
.. code-block:: python
77+
78+
class UserInfo(models.BaseModel):
79+
_name = "user"
80+
_inherit = "odoo_orm_mode"
81+
name: str
82+
groups: List["group"] =
83+
groups: List["group"] = pydantic.Field(alias="groups_id")
84+
85+
86+
class Group(models.BaseModel):
87+
_name="group"
88+
_inherit = "odoo_orm_mode"
89+
name: str
90+
91+
user = self.env.user
92+
UserInfoCls = self.env.pydantic_registry["user"]
93+
user_info = UserInfoCls.from_orm(user)
94+
95+
See the official Pydantic documentation_ to discover all the available functionalities.
96+
97+
.. _documentation: https://pydantic-docs.helpmanual.io/
98+
99+
Known issues / Roadmap
100+
======================
101+
102+
The `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Apydantic>`_
103+
and `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Apydantic>`_ can
104+
be found on GitHub.
105+
106+
Bug Tracker
107+
===========
108+
109+
Bugs are tracked on `GitHub Issues <https://github.com/oca/rest-framework/issues>`_.
110+
In case of trouble, please check there if your issue has already been reported.
111+
If you spotted it first, help us smashing it by providing a detailed and welcomed
112+
`feedback <https://github.com/oca/rest-framework/issues/new?body=module:%20pydantic%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
113+
114+
Do not contact contributors directly about support or help with technical issues.
115+
116+
Credits
117+
=======
118+
119+
Authors
120+
~~~~~~~
121+
122+
* ACSONE SA/NV
123+
124+
Contributors
125+
~~~~~~~~~~~~
126+
127+
* Laurent Mignon <[email protected]>
128+
129+
Maintainers
130+
~~~~~~~~~~~
131+
132+
.. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px
133+
:target: https://github.com/lmignon
134+
:alt: lmignon
135+
136+
Current maintainer:
137+
138+
|maintainer-lmignon|
139+
140+
This module is part of the `oca/rest-framework <https://github.com/oca/rest-framework/tree/14.0/pydantic>`_ project on GitHub.
141+
142+
You are welcome to contribute.

pydantic/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from . import builder
2+
from . import models
3+
from . import registry

pydantic/__manifest__.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Copyright 2021 ACSONE SA/NV
2+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
3+
4+
{
5+
"name": "Pydantic",
6+
"summary": """
7+
Enhance pydantic to allow model extension""",
8+
"version": "14.0.1.0.0",
9+
"development_status": "Beta",
10+
"license": "LGPL-3",
11+
"maintainers": ["lmignon"],
12+
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
13+
"website": "https://github.com/OCA/rest-framework",
14+
"depends": [],
15+
"data": [],
16+
"demo": [],
17+
"external_dependencies": {
18+
"python": [
19+
"pydantic",
20+
]
21+
},
22+
"installable": True,
23+
}

pydantic/builder.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# Copyright 2021 ACSONE SA/NV
2+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
3+
4+
"""
5+
6+
Pydantic Models Builder
7+
=======================
8+
9+
Build the pydantic models at the build of a registry by resolving the
10+
inheritance declaration and ForwardRefs type declaration into the models
11+
12+
"""
13+
from typing import List, Optional
14+
15+
import odoo
16+
from odoo import api, models
17+
18+
from .registry import PydanticClassesRegistry, _pydantic_classes_databases
19+
20+
21+
class PydanticClassesBuilder(models.AbstractModel):
22+
"""Build the component classes
23+
24+
And register them in a global registry.
25+
26+
Every time an Odoo registry is built, the know pydantic models are cleared and
27+
rebuilt as well. The pydantic classes are built by taking every models with
28+
a ``_name`` and applying pydantic models with an ``_inherits`` upon them.
29+
30+
The final pydantic classes are registered in global registry.
31+
32+
This class is an Odoo model, allowing us to hook the build of the
33+
pydantic classes at the end of the Odoo's registry loading, using
34+
``_register_hook``. This method is called after all modules are loaded, so
35+
we are sure that we have all the components Classes and in the correct
36+
order.
37+
38+
"""
39+
40+
_name = "pydantic.classes.builder"
41+
_description = "Pydantic Classes Builder"
42+
43+
def _register_hook(self):
44+
# This method is called by Odoo when the registry is built,
45+
# so in case the registry is rebuilt (cache invalidation, ...),
46+
# we have to to rebuild the components. We use a new
47+
# registry so we have an empty cache and we'll add components in it.
48+
registry = self._init_global_registry()
49+
self.build_registry(registry)
50+
registry.ready = True
51+
52+
@api.model
53+
def _init_global_registry(self):
54+
registry = PydanticClassesRegistry()
55+
_pydantic_classes_databases[self.env.cr.dbname] = registry
56+
return registry
57+
58+
@api.model
59+
def build_registry(
60+
self,
61+
registry: PydanticClassesRegistry,
62+
states: Optional[List[str]] = None,
63+
exclude_addons: Optional[List[str]] = None,
64+
):
65+
if not states:
66+
states = ("installed", "to upgrade")
67+
# lookup all the installed (or about to be) addons and generate
68+
# the graph, so we can load the components following the order
69+
# of the addons' dependencies
70+
graph = odoo.modules.graph.Graph()
71+
graph.add_module(self.env.cr, "base")
72+
73+
query = "SELECT name " "FROM ir_module_module " "WHERE state IN %s "
74+
params = [tuple(states)]
75+
if exclude_addons:
76+
query += " AND name NOT IN %s "
77+
params.append(tuple(exclude_addons))
78+
self.env.cr.execute(query, params)
79+
80+
module_list = [name for (name,) in self.env.cr.fetchall() if name not in graph]
81+
graph.add_modules(self.env.cr, module_list)
82+
83+
# Here we have a graph of installed modules. By iterating on the graph,
84+
# we get the modules from the most generic one to the most specialized
85+
# one. We walk through the graph to build the definition of the classes
86+
# to assemble. The goal is to have for each class name the final
87+
# picture of all the pieces required to build the right hierarchy.
88+
# It's required to avoid to change the bases of an already build class
89+
# each time a module extend the initial implementation as Odoo is
90+
# doing with `Model`. The final definition of a class could depend on
91+
# the potential metaclass associated to the class (a metaclass is a
92+
# class factory). It's therefore not safe to modify on the fly
93+
# the __bases__ attribute of a class once it's constructed since
94+
# the factory method of the metaclass depends these 'bases'
95+
# __new__(mcs, name, bases, new_namespace, **kwargs).
96+
# 'bases' could therefore be processed by the factory in a way or an
97+
# other to build the final class. If you modify the bases after the
98+
# class creation, the logic implemented by the factory will not be
99+
# applied to the new bases and your class could be in an incoherent
100+
# state.
101+
for module in graph:
102+
registry.load_pydantic_classes(module)
103+
registry.build_pydantic_classes()
104+
registry.update_forward_refs()

pydantic/models.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Copyright 2021 ACSONE SA/NV
2+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
3+
4+
import collections
5+
from typing import DefaultDict, List, TypeVar, Union
6+
7+
import pydantic
8+
from pydantic.utils import ClassAttribute
9+
10+
from . import utils
11+
12+
ModelType = TypeVar("Model", bound="BaseModel")
13+
14+
15+
class BaseModel(pydantic.BaseModel):
16+
_name: str = None
17+
_inherit: Union[List[str], str] = None
18+
19+
_pydantic_classes_by_module: DefaultDict[
20+
str, List[ModelType]
21+
] = collections.defaultdict(list)
22+
23+
def __init_subclass__(cls, **kwargs):
24+
super().__init_subclass__(**kwargs)
25+
if cls != BaseModel:
26+
cls.__normalize__definition__()
27+
cls.__register__()
28+
29+
@classmethod
30+
def __normalize__definition__(cls):
31+
"""Normalize class definition
32+
33+
Compute the module name
34+
Compute and validate the model name if class is a subclass
35+
of another BaseModel;
36+
Ensure that _inherit is a list
37+
"""
38+
parents = cls._inherit
39+
if isinstance(parents, str):
40+
parents = [parents]
41+
elif parents is None:
42+
parents = []
43+
name = cls._name or (parents[0] if len(parents) == 1 else None)
44+
if not name:
45+
raise TypeError(f"Extended pydantic class {cls} must have a name")
46+
cls._name = ClassAttribute("_module", name)
47+
cls._module = ClassAttribute("_module", utils._get_addon_name(cls.__module__))
48+
cls.__config__.title = name
49+
# all BaseModels except 'base' implicitly inherit from 'base'
50+
if name != "base":
51+
parents = list(parents) + ["base"]
52+
cls._inherit = ClassAttribute("_inherit", parents)
53+
54+
@classmethod
55+
def __register__(cls):
56+
"""Register the class into the list of classes defined by the module"""
57+
if "tests" not in cls.__module__.split(":"):
58+
cls._pydantic_classes_by_module[cls._module].append(cls)
59+
60+
61+
class Base(BaseModel):
62+
"""This is the base pydantic BaseModel for every BaseModels
63+
64+
It is implicitely inherited by all BaseModels.
65+
66+
All your base are belong to us
67+
"""
68+
69+
_name = "base"
70+
71+
72+
class OdooOrmMode(BaseModel):
73+
"""Generic model that can be used to instantiate pydantis model from
74+
odoo models
75+
76+
Usage:
77+
78+
.. code-block:: python
79+
80+
class UserInfo(models.BaseModel):
81+
_name = "user"
82+
_inherit = "odoo_orm_mode"
83+
name: str
84+
groups: List["group"] =
85+
groups: List["group"] = pydantic.Field(alias="groups_id")
86+
87+
88+
class Group(models.BaseModel):
89+
_name="group"
90+
_inherit = "odoo_orm_mode"
91+
name: str
92+
93+
user = self.env.user
94+
UserInfoCls = self.env.pydantic_registry["user"]
95+
user_info = UserInfoCls.from_orm(user)
96+
97+
"""
98+
99+
_name = "odoo_orm_mode"
100+
101+
class Config:
102+
orm_mode = True
103+
getter_dict = utils.GenericOdooGetter

pydantic/readme/CONTRIBUTORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* Laurent Mignon <[email protected]>

pydantic/readme/DESCRIPTION.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This addon allows you to define inheritable `Pydantic classes <https://pydantic-docs.helpmanual.io/>`_.

pydantic/readme/ROADMAP.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Apydantic>`_
2+
and `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Apydantic>`_ can
3+
be found on GitHub.

0 commit comments

Comments
 (0)