Skip to content

Commit

Permalink
[IMP] Pydantic: Improve API to be more pythonic
Browse files Browse the repository at this point in the history
  • Loading branch information
lmignon committed Nov 24, 2021
1 parent 01d3772 commit 1418e06
Show file tree
Hide file tree
Showing 15 changed files with 721 additions and 338 deletions.
144 changes: 107 additions & 37 deletions pydantic/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ Pydantic
.. |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|
.. |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
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/rest-framework-14-0/rest-framework-14-0-pydantic
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/271/14.0
:alt: Try me on Runbot

|badge1| |badge2| |badge3| |badge4| |badge5|

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

Expand All @@ -30,7 +36,7 @@ Usage
=====

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.

.. code-block:: python
Expand All @@ -39,15 +45,11 @@ To define your own pydantic model you just need to create a class that inherits
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
Expand All @@ -56,44 +58,102 @@ To define your own pydantic model you just need to create a class that inherits
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.

.. code-block:: python
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.

.. code-block:: python
class Base(BaseModel):
_inherit = "base"
partner1 = PartnerInfo.construct()
partner2 = PartnerInfoWithCoordintate.construct()
def _my_method(self):
pass
assert partner1.__class__ == partner2.__class__
assert PartnerInfo.schema() == PartnerInfoWithCoordinate.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 `construct()` class
method to create a pydantic instance.

.. caution::

Adding required fields to an existing data structure into an extension
addon violates the `Liskov substitution principle`_ and should generally
be avoided. This is certainly forbidden in requests data structures.
When extending response data structures this could be useful to document
new fields that are guaranteed to be present when extension addons are
installed.

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.

.. code-block:: python
from odoo.addons.my_addons.datamodels import PartnerInfo
from odoo import models
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`.

.. code-block:: python
class UserInfo(models.BaseModel):
_name = "user"
_inherit = "odoo_orm_mode"
name: str
groups: List["group"] = pydantic.Field(alias="groups_id")
import pydantic
from odoo.addons.pydantic import models, utils
class Group(models.BaseModel):
name: str
class Config:
orm_mode = True
getter_dict = utils.GenericOdooGetter
class UserInfo(models.BaseModel):
name: str
groups: List[Group] = pydantic.Field(alias="groups_id")
class Group(models.BaseModel):
_name="group"
_inherit = "odoo_orm_mode"
name: str
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 = self.env.user
user_info = UserInfo.from_orm(user)
See the official Pydantic documentation_ to discover all the available functionalities.
See the official `Pydantic documentation`_ to discover all the available functionalities.

.. _documentation: https://pydantic-docs.helpmanual.io/
.. _`Liskov substitution principle`: https://en.wikipedia.org/wiki/Liskov_substitution_principle
.. _`Pydantic documentation`: https://pydantic-docs.helpmanual.io/

Known issues / Roadmap
======================
Expand All @@ -105,10 +165,10 @@ be found on GitHub.
Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/oca/rest-framework/issues>`_.
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**>`_.
`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.

Expand All @@ -128,14 +188,24 @@ Contributors
Maintainers
~~~~~~~~~~~

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

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.

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

Current maintainer:
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-lmignon|

This module is part of the `oca/rest-framework <https://github.com/oca/rest-framework/tree/14.0/pydantic>`_ project on GitHub.
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.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions pydantic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import builder
from . import models
from . import registry
from . import ir_http
1 change: 1 addition & 0 deletions pydantic/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"external_dependencies": {
"python": [
"pydantic",
"contextvars",
]
},
"installable": True,
Expand Down
31 changes: 5 additions & 26 deletions pydantic/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,20 @@
from typing import List, Optional

import odoo
from odoo import api, models
from odoo import api, models as omodels

from .registry import PydanticClassesRegistry, _pydantic_classes_databases


class PydanticClassesBuilder(models.AbstractModel):
class PydanticClassesBuilder(omodels.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.
a ``__xreg_name__`` and applying pydantic models with an ``__xreg_base_names__``
upon them.
The final pydantic classes are registered in global registry.
Expand All @@ -47,7 +48,6 @@ def _register_hook(self):
# 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):
Expand Down Expand Up @@ -80,25 +80,4 @@ def build_registry(
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()
registry.init_registry([m.name for m in graph])
8 changes: 8 additions & 0 deletions pydantic/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)

# define context vars to hold the pydantic registry

from contextvars import ContextVar

odoo_pydantic_registry = ContextVar("pydantic_registry")
29 changes: 29 additions & 0 deletions pydantic/ir_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2021 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)

from contextlib import contextmanager

from odoo import models
from odoo.http import request

from .context import odoo_pydantic_registry
from .registry import _pydantic_classes_databases


class IrHttp(models.AbstractModel):
_inherit = "ir.http"

@classmethod
def _dispatch(cls):
with cls._pydantic_context_registry():
return super()._dispatch()

@classmethod
@contextmanager
def _pydantic_context_registry(cls):
registry = _pydantic_classes_databases.get(request.env.cr.dbname, {})
token = odoo_pydantic_registry.set(registry)
try:
yield
finally:
odoo_pydantic_registry.reset(token)
Loading

0 comments on commit 1418e06

Please sign in to comment.