|
| 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() |
0 commit comments