Skip to content

Commit

Permalink
[ADD] pydantic: allows pydantic usage into Odoo
Browse files Browse the repository at this point in the history
  • Loading branch information
lmignon committed Nov 22, 2021
1 parent e9bb952 commit 01d3772
Show file tree
Hide file tree
Showing 19 changed files with 1,558 additions and 0 deletions.
141 changes: 141 additions & 0 deletions pydantic/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
========
Pydantic
========

.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-oca%2Frest--framework-lightgray.png?logo=github
:target: https://github.com/oca/rest-framework/tree/14.0/pydantic
:alt: oca/rest-framework

|badge1| |badge2| |badge3|

This addon allows you to define inheritable `Pydantic classes <https://pydantic-docs.helpmanual.io/>`_.

**Table of contents**

.. contents::
:local:

Usage
=====

To define your own pydantic model you just need to create a class that inherits from
``odoo.addons.pydantic.models.BaseModel``

.. code-block:: python
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
city: str
phone: str = None
is_componay : bool = Field(None)
As for odoo models, you can extend the `base` pydantic model by inheriting of `base`.

.. code-block:: python
class Base(BaseModel):
_inherit = "base"
def _my_method(self):
pass
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'

.. code-block:: python
class UserInfo(models.BaseModel):
_name = "user"
_inherit = "odoo_orm_mode"
name: str
groups: List["group"] = pydantic.Field(alias="groups_id")
class Group(models.BaseModel):
_name="group"
_inherit = "odoo_orm_mode"
name: str
user = self.env.user
UserInfoCls = self.env.pydantic_registry["user"]
user_info = UserInfoCls.from_orm(user)
See the official Pydantic documentation_ to discover all the available functionalities.

.. _documentation: https://pydantic-docs.helpmanual.io/

Known issues / Roadmap
======================

The `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Apydantic>`_
and `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Apydantic>`_ can
be found on GitHub.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/oca/rest-framework/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`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**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
~~~~~~~

* ACSONE SA/NV

Contributors
~~~~~~~~~~~~

* Laurent Mignon <[email protected]>

Maintainers
~~~~~~~~~~~

.. |maintainer-lmignon| image:: https://github.com/lmignon.png?size=40px
:target: https://github.com/lmignon
:alt: lmignon

Current maintainer:

|maintainer-lmignon|

This module is part of the `oca/rest-framework <https://github.com/oca/rest-framework/tree/14.0/pydantic>`_ project on GitHub.

You are welcome to contribute.
3 changes: 3 additions & 0 deletions pydantic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import builder
from . import models
from . import registry
23 changes: 23 additions & 0 deletions pydantic/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)

{
"name": "Pydantic",
"summary": """
Enhance pydantic to allow model extension""",
"version": "14.0.1.0.0",
"development_status": "Beta",
"license": "LGPL-3",
"maintainers": ["lmignon"],
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/rest-framework",
"depends": [],
"data": [],
"demo": [],
"external_dependencies": {
"python": [
"pydantic",
]
},
"installable": True,
}
104 changes: 104 additions & 0 deletions pydantic/builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)

"""
Pydantic Models Builder
=======================
Build the pydantic models at the build of a registry by resolving the
inheritance declaration and ForwardRefs type declaration into the models
"""
from typing import List, Optional

import odoo
from odoo import api, models

from .registry import PydanticClassesRegistry, _pydantic_classes_databases


class PydanticClassesBuilder(models.AbstractModel):
"""Build the component classes
And register them in a global registry.
Every time an Odoo registry is built, the know pydantic models are cleared and
rebuilt as well. The pydantic classes are built by taking every models with
a ``_name`` and applying pydantic models with an ``_inherits`` upon them.
The final pydantic classes are registered in global registry.
This class is an Odoo model, allowing us to hook the build of the
pydantic classes at the end of the Odoo's registry loading, using
``_register_hook``. This method is called after all modules are loaded, so
we are sure that we have all the components Classes and in the correct
order.
"""

_name = "pydantic.classes.builder"
_description = "Pydantic Classes Builder"

def _register_hook(self):
# This method is called by Odoo when the registry is built,
# so in case the registry is rebuilt (cache invalidation, ...),
# we have to to rebuild the components. We use a new
# registry so we have an empty cache and we'll add components in it.
registry = self._init_global_registry()
self.build_registry(registry)
registry.ready = True

@api.model
def _init_global_registry(self):
registry = PydanticClassesRegistry()
_pydantic_classes_databases[self.env.cr.dbname] = registry
return registry

@api.model
def build_registry(
self,
registry: PydanticClassesRegistry,
states: Optional[List[str]] = None,
exclude_addons: Optional[List[str]] = None,
):
if not states:
states = ("installed", "to upgrade")
# lookup all the installed (or about to be) addons and generate
# the graph, so we can load the components following the order
# of the addons' dependencies
graph = odoo.modules.graph.Graph()
graph.add_module(self.env.cr, "base")

query = "SELECT name " "FROM ir_module_module " "WHERE state IN %s "
params = [tuple(states)]
if exclude_addons:
query += " AND name NOT IN %s "
params.append(tuple(exclude_addons))
self.env.cr.execute(query, params)

module_list = [name for (name,) in self.env.cr.fetchall() if name not in graph]
graph.add_modules(self.env.cr, module_list)

# Here we have a graph of installed modules. By iterating on the graph,
# we get the modules from the most generic one to the most specialized
# one. We walk through the graph to build the definition of the classes
# to assemble. The goal is to have for each class name the final
# picture of all the pieces required to build the right hierarchy.
# It's required to avoid to change the bases of an already build class
# each time a module extend the initial implementation as Odoo is
# doing with `Model`. The final definition of a class could depend on
# the potential metaclass associated to the class (a metaclass is a
# class factory). It's therefore not safe to modify on the fly
# the __bases__ attribute of a class once it's constructed since
# the factory method of the metaclass depends these 'bases'
# __new__(mcs, name, bases, new_namespace, **kwargs).
# 'bases' could therefore be processed by the factory in a way or an
# other to build the final class. If you modify the bases after the
# class creation, the logic implemented by the factory will not be
# applied to the new bases and your class could be in an incoherent
# state.
for module in graph:
registry.load_pydantic_classes(module)
registry.build_pydantic_classes()
registry.update_forward_refs()
102 changes: 102 additions & 0 deletions pydantic/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)

import collections
from typing import DefaultDict, List, TypeVar, Union

import pydantic
from pydantic.utils import ClassAttribute

from . import utils

ModelType = TypeVar("Model", bound="BaseModel")


class BaseModel(pydantic.BaseModel):
_name: str = None
_inherit: Union[List[str], str] = None

_pydantic_classes_by_module: DefaultDict[
str, List[ModelType]
] = collections.defaultdict(list)

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if cls != BaseModel:
cls.__normalize__definition__()
cls.__register__()

@classmethod
def __normalize__definition__(cls):
"""Normalize class definition
Compute the module name
Compute and validate the model name if class is a subclass
of another BaseModel;
Ensure that _inherit is a list
"""
parents = cls._inherit
if isinstance(parents, str):
parents = [parents]
elif parents is None:
parents = []
name = cls._name or (parents[0] if len(parents) == 1 else None)
if not name:
raise TypeError(f"Extended pydantic class {cls} must have a name")
cls._name = ClassAttribute("_module", name)
cls._module = ClassAttribute("_module", utils._get_addon_name(cls.__module__))
cls.__config__.title = name
# all BaseModels except 'base' implicitly inherit from 'base'
if name != "base":
parents = list(parents) + ["base"]
cls._inherit = ClassAttribute("_inherit", parents)

@classmethod
def __register__(cls):
"""Register the class into the list of classes defined by the module"""
if "tests" not in cls.__module__.split(":"):
cls._pydantic_classes_by_module[cls._module].append(cls)


class Base(BaseModel):
"""This is the base pydantic BaseModel for every BaseModels
It is implicitely inherited by all BaseModels.
All your base are belong to us
"""

_name = "base"


class OdooOrmMode(BaseModel):
"""Generic model that can be used to instantiate pydantis model from
odoo models
Usage:
.. code-block:: python
class UserInfo(models.BaseModel):
_name = "user"
_inherit = "odoo_orm_mode"
name: str
groups: List["group"] = pydantic.Field(alias="groups_id")
class Group(models.BaseModel):
_name="group"
_inherit = "odoo_orm_mode"
name: str
user = self.env.user
UserInfoCls = self.env.pydantic_registry["user"]
user_info = UserInfoCls.from_orm(user)
"""

_name = "odoo_orm_mode"

class Config:
orm_mode = True
getter_dict = utils.GenericOdooGetter
1 change: 1 addition & 0 deletions pydantic/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Laurent Mignon <[email protected]>
1 change: 1 addition & 0 deletions pydantic/readme/DESCRIPTION.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This addon allows you to define inheritable `Pydantic classes <https://pydantic-docs.helpmanual.io/>`_.
3 changes: 3 additions & 0 deletions pydantic/readme/ROADMAP.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
The `roadmap <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Aenhancement+label%3Apydantic>`_
and `known issues <https://github.com/OCA/rest-framework/issues?q=is%3Aopen+is%3Aissue+label%3Abug+label%3Apydantic>`_ can
be found on GitHub.
Loading

0 comments on commit 01d3772

Please sign in to comment.