mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-18 21:32:00 +02:00
Initial commit: OCA Technical packages (595 packages)
This commit is contained in:
commit
2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions
938
odoo-bringout-oca-connector-component/component/core.py
Normal file
938
odoo-bringout-oca-connector-component/component/core.py
Normal file
|
|
@ -0,0 +1,938 @@
|
|||
# Copyright 2017 Camptocamp SA
|
||||
# Copyright 2017 Odoo
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html)
|
||||
|
||||
"""
|
||||
|
||||
Core
|
||||
====
|
||||
|
||||
Core classes for the components.
|
||||
The most common classes used publicly are:
|
||||
|
||||
* :class:`Component`
|
||||
* :class:`AbstractComponent`
|
||||
* :class:`WorkContext`
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import operator
|
||||
from collections import OrderedDict, defaultdict
|
||||
|
||||
from odoo import models
|
||||
from odoo.tools import LastOrderedSet, OrderedSet
|
||||
|
||||
from .exception import NoComponentError, RegistryNotReadyError, SeveralComponentError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from cachetools import LRUCache, cachedmethod
|
||||
except ImportError:
|
||||
_logger.debug("Cannot import 'cachetools'.")
|
||||
|
||||
|
||||
# The Cache size represents the number of items, so the number
|
||||
# of components (include abstract components) we will keep in the LRU
|
||||
# cache. We would need stats to know what is the average but this is a bit
|
||||
# early.
|
||||
DEFAULT_CACHE_SIZE = 512
|
||||
|
||||
|
||||
# this is duplicated from odoo.models.MetaModel._get_addon_name() which we
|
||||
# unfortunately can't use because it's an instance method and should have been
|
||||
# a @staticmethod
|
||||
def _get_addon_name(full_name):
|
||||
# The (Odoo) module name can be in the ``odoo.addons`` namespace
|
||||
# or not. For instance, module ``sale`` can be imported as
|
||||
# ``odoo.addons.sale`` (the right way) or ``sale`` (for backward
|
||||
# compatibility).
|
||||
module_parts = full_name.split(".")
|
||||
if len(module_parts) > 2 and module_parts[:2] == ["odoo", "addons"]:
|
||||
addon_name = full_name.split(".")[2]
|
||||
else:
|
||||
addon_name = full_name.split(".")[0]
|
||||
return addon_name
|
||||
|
||||
|
||||
class ComponentDatabases(dict):
|
||||
"""Holds a registry of components for each database"""
|
||||
|
||||
|
||||
class ComponentRegistry:
|
||||
"""Store all the components and allow to find them using criteria
|
||||
|
||||
The key is the ``_name`` of the components.
|
||||
|
||||
This is an OrderedDict, because we want to keep the registration order of
|
||||
the components, addons loaded first have their components found first.
|
||||
|
||||
The :attr:`ready` attribute must be set to ``True`` when all the components
|
||||
are loaded.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, cachesize=DEFAULT_CACHE_SIZE):
|
||||
self._cache = LRUCache(maxsize=cachesize)
|
||||
self._components = OrderedDict()
|
||||
self._loaded_modules = set()
|
||||
self.ready = False
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._components[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self._components[key] = value
|
||||
|
||||
def __contains__(self, key):
|
||||
return key in self._components
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._components.get(key, default)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._components)
|
||||
|
||||
def load_components(self, module):
|
||||
if module in self._loaded_modules:
|
||||
return
|
||||
for component_class in MetaComponent._modules_components[module]:
|
||||
component_class._build_component(self)
|
||||
self._loaded_modules.add(module)
|
||||
|
||||
@cachedmethod(operator.attrgetter("_cache"))
|
||||
def lookup(self, collection_name=None, usage=None, model_name=None):
|
||||
"""Find and return a list of components for a usage
|
||||
|
||||
If a component is not registered in a particular collection (no
|
||||
``_collection``), it will be returned in any case (as far as
|
||||
the ``usage`` and ``model_name`` match). This is useful to share
|
||||
generic components across different collections.
|
||||
|
||||
If no collection name is given, components from any collection
|
||||
will be returned.
|
||||
|
||||
Then, the components of a collection are filtered by usage and/or
|
||||
model. The ``_usage`` is mandatory on the components. When the
|
||||
``_model_name`` is empty, it means it can be used for every models,
|
||||
and it will ignore the ``model_name`` argument.
|
||||
|
||||
The abstract components are never returned.
|
||||
|
||||
This is a rather low-level function, usually you will use the
|
||||
high-level :meth:`AbstractComponent.component`,
|
||||
:meth:`AbstractComponent.many_components` or even
|
||||
:meth:`AbstractComponent.component_by_name`.
|
||||
|
||||
:param collection_name: the name of the collection the component is
|
||||
registered into.
|
||||
:param usage: the usage of component we are looking for
|
||||
:param model_name: filter on components that apply on this model
|
||||
|
||||
"""
|
||||
|
||||
# keep the order so addons loaded first have components used first
|
||||
candidates = (
|
||||
component
|
||||
for component in self._components.values()
|
||||
if not component._abstract
|
||||
)
|
||||
|
||||
if collection_name is not None:
|
||||
candidates = (
|
||||
component
|
||||
for component in candidates
|
||||
if (
|
||||
component._collection == collection_name
|
||||
or component._collection is None
|
||||
)
|
||||
)
|
||||
|
||||
if usage is not None:
|
||||
candidates = (
|
||||
component for component in candidates if component._usage == usage
|
||||
)
|
||||
|
||||
if model_name is not None:
|
||||
candidates = (
|
||||
c
|
||||
for c in candidates
|
||||
if c.apply_on_models is None or model_name in c.apply_on_models
|
||||
)
|
||||
|
||||
return list(candidates)
|
||||
|
||||
|
||||
# We will store a ComponentRegistry per database here,
|
||||
# it will be cleared and updated when the odoo's registry is rebuilt
|
||||
_component_databases = ComponentDatabases()
|
||||
|
||||
|
||||
class WorkContext:
|
||||
"""Transport the context required to work with components
|
||||
|
||||
It is propagated through all the components, so any
|
||||
data or instance (like a random RPC client) that need
|
||||
to be propagated transversally to the components
|
||||
should be kept here.
|
||||
|
||||
Including:
|
||||
|
||||
.. attribute:: model_name
|
||||
|
||||
Name of the model we are working with. It means that any lookup for a
|
||||
component will be done for this model. It also provides a shortcut
|
||||
as a `model` attribute to use directly with the Odoo model from
|
||||
the components
|
||||
|
||||
.. attribute:: collection
|
||||
|
||||
The collection we are working with. The collection is an Odoo
|
||||
Model that inherit from 'collection.base'. The collection attribute
|
||||
can be a record or an "empty" model.
|
||||
|
||||
.. attribute:: model
|
||||
|
||||
Odoo Model for ``model_name`` with the same Odoo
|
||||
:class:`~odoo.api.Environment` than the ``collection`` attribute.
|
||||
|
||||
This is also the entrypoint to work with the components.
|
||||
|
||||
::
|
||||
|
||||
collection = self.env['my.collection'].browse(1)
|
||||
work = WorkContext(model_name='res.partner', collection=collection)
|
||||
component = work.component(usage='record.importer')
|
||||
|
||||
Usually you will use the context manager on the ``collection.base`` Model:
|
||||
|
||||
::
|
||||
|
||||
collection = self.env['my.collection'].browse(1)
|
||||
with collection.work_on('res.partner') as work:
|
||||
component = work.component(usage='record.importer')
|
||||
|
||||
It supports any arbitrary keyword arguments that will become attributes of
|
||||
the instance, and be propagated throughout all the components.
|
||||
|
||||
::
|
||||
|
||||
collection = self.env['my.collection'].browse(1)
|
||||
with collection.work_on('res.partner', hello='world') as work:
|
||||
assert work.hello == 'world'
|
||||
|
||||
When you need to work on a different model, a new work instance will be
|
||||
created for you when you are using the high-level API. This is what
|
||||
happens under the hood:
|
||||
|
||||
::
|
||||
|
||||
collection = self.env['my.collection'].browse(1)
|
||||
with collection.work_on('res.partner', hello='world') as work:
|
||||
assert work.model_name == 'res.partner'
|
||||
assert work.hello == 'world'
|
||||
work2 = work.work_on('res.users')
|
||||
# => spawn a new WorkContext with a copy of the attributes
|
||||
assert work2.model_name == 'res.users'
|
||||
assert work2.hello == 'world'
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, model_name=None, collection=None, components_registry=None, **kwargs
|
||||
):
|
||||
self.collection = collection
|
||||
self.model_name = model_name
|
||||
self.model = self.env[model_name]
|
||||
# lookup components in an alternative registry, used by the tests
|
||||
if components_registry is not None:
|
||||
self.components_registry = components_registry
|
||||
else:
|
||||
dbname = self.env.cr.dbname
|
||||
try:
|
||||
self.components_registry = _component_databases[dbname]
|
||||
except KeyError as exc:
|
||||
msg = (
|
||||
"No component registry for database %s. "
|
||||
"Probably because the Odoo registry has not been built "
|
||||
"yet."
|
||||
)
|
||||
_logger.error(
|
||||
msg,
|
||||
dbname,
|
||||
)
|
||||
raise RegistryNotReadyError(msg) from exc
|
||||
self._propagate_kwargs = ["collection", "model_name", "components_registry"]
|
||||
for attr_name, value in kwargs.items():
|
||||
setattr(self, attr_name, value)
|
||||
self._propagate_kwargs.append(attr_name)
|
||||
|
||||
@property
|
||||
def env(self):
|
||||
"""Return the current Odoo env
|
||||
|
||||
This is the environment of the current collection.
|
||||
"""
|
||||
return self.collection.env
|
||||
|
||||
def work_on(self, model_name=None, collection=None):
|
||||
"""Create a new work context for another model keeping attributes
|
||||
|
||||
Used when one need to lookup components for another model.
|
||||
"""
|
||||
kwargs = {
|
||||
attr_name: getattr(self, attr_name) for attr_name in self._propagate_kwargs
|
||||
}
|
||||
if collection is not None:
|
||||
kwargs["collection"] = collection
|
||||
if model_name is not None:
|
||||
kwargs["model_name"] = model_name
|
||||
return self.__class__(**kwargs)
|
||||
|
||||
def _component_class_by_name(self, name):
|
||||
components_registry = self.components_registry
|
||||
component_class = components_registry.get(name)
|
||||
if not component_class:
|
||||
raise NoComponentError("No component with name '%s' found." % name)
|
||||
return component_class
|
||||
|
||||
def component_by_name(self, name, model_name=None):
|
||||
"""Return a component by its name
|
||||
|
||||
If the component exists, an instance of it will be returned,
|
||||
initialized with the current :class:`WorkContext`.
|
||||
|
||||
A :exc:`odoo.addons.component.exception.NoComponentError` is raised
|
||||
if:
|
||||
|
||||
* no component with this name exists
|
||||
* the ``_apply_on`` of the found component does not match
|
||||
with the current working model
|
||||
|
||||
In the latter case, it can be an indication that you need to switch to
|
||||
a different model, you can do so by providing the ``model_name``
|
||||
argument.
|
||||
|
||||
"""
|
||||
if isinstance(model_name, models.BaseModel):
|
||||
model_name = model_name._name
|
||||
component_class = self._component_class_by_name(name)
|
||||
work_model = model_name or self.model_name
|
||||
if (
|
||||
component_class._collection
|
||||
and self.collection._name != component_class._collection
|
||||
):
|
||||
raise NoComponentError(
|
||||
"Component with name '%s' can't be used for collection '%s'."
|
||||
% (name, self.collection._name)
|
||||
)
|
||||
|
||||
if (
|
||||
component_class.apply_on_models
|
||||
and work_model not in component_class.apply_on_models
|
||||
):
|
||||
if len(component_class.apply_on_models) == 1:
|
||||
hint_models = "'{}'".format(component_class.apply_on_models[0])
|
||||
else:
|
||||
hint_models = "<one of {!r}>".format(component_class.apply_on_models)
|
||||
raise NoComponentError(
|
||||
"Component with name '%s' can't be used for model '%s'.\n"
|
||||
"Hint: you might want to use: "
|
||||
"component_by_name('%s', model_name=%s)"
|
||||
% (name, work_model, name, hint_models)
|
||||
)
|
||||
|
||||
if work_model == self.model_name:
|
||||
work_context = self
|
||||
else:
|
||||
work_context = self.work_on(model_name)
|
||||
return component_class(work_context)
|
||||
|
||||
def _lookup_components(self, usage=None, model_name=None, **kw):
|
||||
component_classes = self.components_registry.lookup(
|
||||
self.collection._name, usage=usage, model_name=model_name
|
||||
)
|
||||
matching_components = []
|
||||
for cls in component_classes:
|
||||
try:
|
||||
matching = cls._component_match(
|
||||
self, usage=usage, model_name=model_name, **kw
|
||||
)
|
||||
except TypeError as err:
|
||||
# Backward compat
|
||||
_logger.info(str(err))
|
||||
_logger.info(
|
||||
"The signature of %s._component_match has changed. "
|
||||
"Please, adapt your code as "
|
||||
"(self, usage=usage, model_name=model_name, **kw)",
|
||||
cls.__name__,
|
||||
)
|
||||
matching = cls._component_match(self)
|
||||
if matching:
|
||||
matching_components.append(cls)
|
||||
return matching_components
|
||||
|
||||
def _filter_components_by_collection(self, component_classes):
|
||||
return [c for c in component_classes if c._collection == self.collection._name]
|
||||
|
||||
def _filter_components_by_model(self, component_classes, model_name):
|
||||
return [
|
||||
c
|
||||
for c in component_classes
|
||||
if c.apply_on_models and model_name in c.apply_on_models
|
||||
]
|
||||
|
||||
def _ensure_model_name(self, model_name):
|
||||
"""Make sure model name is a string or fallback to current ctx value."""
|
||||
if isinstance(model_name, models.BaseModel):
|
||||
model_name = model_name._name
|
||||
return model_name or self.model_name
|
||||
|
||||
def _matching_components(self, usage=None, model_name=None, **kw):
|
||||
"""Retrieve matching components and their work context."""
|
||||
component_classes = self._lookup_components(
|
||||
usage=usage, model_name=model_name, **kw
|
||||
)
|
||||
if model_name == self.model_name:
|
||||
work_context = self
|
||||
else:
|
||||
work_context = self.work_on(model_name)
|
||||
return component_classes, work_context
|
||||
|
||||
def component(self, usage=None, model_name=None, **kw):
|
||||
"""Find a component by usage and model for the current collection
|
||||
|
||||
It searches a component using the rules of
|
||||
:meth:`ComponentRegistry.lookup`. When a component is found,
|
||||
it initialize it with the current :class:`WorkContext` and returned.
|
||||
|
||||
A component with a ``_apply_on`` matching the asked ``model_name``
|
||||
takes precedence over a generic component without ``_apply_on``.
|
||||
A component with a ``_collection`` matching the current collection
|
||||
takes precedence over a generic component without ``_collection``.
|
||||
This behavior allows to define generic components across collections
|
||||
and/or models and override them only for a particular collection and/or
|
||||
model.
|
||||
|
||||
A :exc:`odoo.addons.component.exception.SeveralComponentError` is
|
||||
raised if more than one component match for the provided
|
||||
``usage``/``model_name``.
|
||||
|
||||
A :exc:`odoo.addons.component.exception.NoComponentError` is raised
|
||||
if no component is found for the provided ``usage``/``model_name``.
|
||||
|
||||
"""
|
||||
model_name = self._ensure_model_name(model_name)
|
||||
component_classes, work_context = self._matching_components(
|
||||
usage=usage, model_name=model_name, **kw
|
||||
)
|
||||
if not component_classes:
|
||||
raise NoComponentError(
|
||||
"No component found for collection '%s', "
|
||||
"usage '%s', model_name '%s'."
|
||||
% (self.collection._name, usage, model_name)
|
||||
)
|
||||
elif len(component_classes) > 1:
|
||||
# If we have more than one component, try to find the one
|
||||
# specifically linked to the collection...
|
||||
component_classes = self._filter_components_by_collection(component_classes)
|
||||
if len(component_classes) > 1:
|
||||
# ... or try to find the one specifically linked to the model
|
||||
component_classes = self._filter_components_by_model(
|
||||
component_classes, model_name
|
||||
)
|
||||
if len(component_classes) != 1:
|
||||
raise SeveralComponentError(
|
||||
"Several components found for collection '%s', "
|
||||
"usage '%s', model_name '%s'. Found: %r"
|
||||
% (
|
||||
self.collection._name,
|
||||
usage or "",
|
||||
model_name or "",
|
||||
component_classes,
|
||||
)
|
||||
)
|
||||
return component_classes[0](work_context)
|
||||
|
||||
def many_components(self, usage=None, model_name=None, **kw):
|
||||
"""Find many components by usage and model for the current collection
|
||||
|
||||
It searches a component using the rules of
|
||||
:meth:`ComponentRegistry.lookup`. When components are found, they
|
||||
initialized with the current :class:`WorkContext` and returned as a
|
||||
list.
|
||||
|
||||
If no component is found, an empty list is returned.
|
||||
|
||||
"""
|
||||
model_name = self._ensure_model_name(model_name)
|
||||
component_classes, work_context = self._matching_components(
|
||||
usage=usage, model_name=model_name, **kw
|
||||
)
|
||||
return [comp(work_context) for comp in component_classes]
|
||||
|
||||
def __str__(self):
|
||||
return "WorkContext({}, {})".format(self.model_name, repr(self.collection))
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
class MetaComponent(type):
|
||||
"""Metaclass for Components
|
||||
|
||||
Every new :class:`Component` will be added to ``_modules_components``,
|
||||
that will be used by the component builder.
|
||||
|
||||
"""
|
||||
|
||||
_modules_components = defaultdict(list)
|
||||
|
||||
def __init__(cls, name, bases, attrs):
|
||||
if not cls._register:
|
||||
cls._register = True
|
||||
super().__init__(name, bases, attrs)
|
||||
return
|
||||
|
||||
# If components are declared in tests, exclude them from the
|
||||
# "components of the addon" list. If not, when we use the
|
||||
# "load_components" method, all the test components would be loaded.
|
||||
# This should never be an issue when running the app normally, as the
|
||||
# Python tests should never be executed. But this is an issue when a
|
||||
# test creates a test components for the purpose of the test, then a
|
||||
# second tests uses the "load_components" to load all the addons of the
|
||||
# module: it will load the component of the previous test.
|
||||
if "tests" in cls.__module__.split("."):
|
||||
return
|
||||
|
||||
if not hasattr(cls, "_module"):
|
||||
cls._module = _get_addon_name(cls.__module__)
|
||||
|
||||
cls._modules_components[cls._module].append(cls)
|
||||
|
||||
@property
|
||||
def apply_on_models(cls):
|
||||
# None means all models
|
||||
if cls._apply_on is None:
|
||||
return None
|
||||
# always return a list, used for the lookup
|
||||
elif isinstance(cls._apply_on, str):
|
||||
return [cls._apply_on]
|
||||
return cls._apply_on
|
||||
|
||||
|
||||
class AbstractComponent(metaclass=MetaComponent):
|
||||
"""Main Component Model
|
||||
|
||||
All components have a Python inheritance either on
|
||||
:class:`AbstractComponent` or either on :class:`Component`.
|
||||
|
||||
Abstract Components will not be returned by lookups on components, however
|
||||
they can be used as a base for other Components through inheritance (using
|
||||
``_inherit``).
|
||||
|
||||
Inheritance mechanism
|
||||
The inheritance mechanism is like the Odoo's one for Models. Each
|
||||
component has a ``_name``. This is the absolute minimum in a Component
|
||||
class.
|
||||
|
||||
::
|
||||
|
||||
class MyComponent(Component):
|
||||
_name = 'my.component'
|
||||
|
||||
def speak(self, message):
|
||||
print message
|
||||
|
||||
Every component implicitly inherit from the `'base'` component.
|
||||
|
||||
There are two close but distinct inheritance types, which look
|
||||
familiar if you already know Odoo. The first uses ``_inherit`` with
|
||||
an existing name, the name of the component we want to extend. With
|
||||
the following example, ``my.component`` is now able to speak and to
|
||||
yell.
|
||||
|
||||
::
|
||||
|
||||
class MyComponent(Component): # name of the class does not matter
|
||||
_inherit = 'my.component'
|
||||
|
||||
def yell(self, message):
|
||||
print message.upper()
|
||||
|
||||
The second has a different ``_name``, it creates a new component,
|
||||
including the behavior of the inherited component, but without
|
||||
modifying it. In the following example, ``my.component`` is still able
|
||||
to speak and to yell (brough by the previous inherit), but not to
|
||||
sing. ``another.component`` is able to speak, to yell and to sing.
|
||||
|
||||
::
|
||||
|
||||
class AnotherComponent(Component):
|
||||
_name = 'another.component'
|
||||
_inherit = 'my.component'
|
||||
|
||||
def sing(self, message):
|
||||
print message.upper()
|
||||
|
||||
Registration and lookups
|
||||
It is handled by 3 attributes on the class:
|
||||
|
||||
_collection
|
||||
The name of the collection where we want to register the
|
||||
component. This is not strictly mandatory as a component can be
|
||||
shared across several collections. But usually, you want to set a
|
||||
collection to segregate the components for a domain. A collection
|
||||
can be for instance ``magento.backend``. It is also the name of a
|
||||
model that inherits from ``collection.base``. See also
|
||||
:class:`~WorkContext` and
|
||||
:class:`~odoo.addons.component.models.collection.Collection`.
|
||||
|
||||
_apply_on
|
||||
List of names or name of the Odoo model(s) for which the component
|
||||
can be used. When not set, the component can be used on any model.
|
||||
|
||||
_usage
|
||||
The collection and the model (``_apply_on``) will help to filter
|
||||
the candidate components according to our working context (e.g. I'm
|
||||
working on ``magento.backend`` with the model
|
||||
``magento.res.partner``). The usage will define **what** kind of
|
||||
task the component we are looking for serves to. For instance, it
|
||||
might be ``record.importer``, ``export.mapper```... but you can be
|
||||
as creative as you want.
|
||||
|
||||
Now, to get a component, you'll likely use
|
||||
:meth:`WorkContext.component` when you start to work with components
|
||||
in your flow, but then from within your components, you are more
|
||||
likely to use one of:
|
||||
|
||||
* :meth:`component`
|
||||
* :meth:`many_components`
|
||||
* :meth:`component_by_name` (more rarely though)
|
||||
|
||||
Declaration of some Components can look like::
|
||||
|
||||
class FooBar(models.Model):
|
||||
_name = 'foo.bar.collection'
|
||||
_inherit = 'collection.base' # this inherit is required
|
||||
|
||||
|
||||
class FooBarBase(AbstractComponent):
|
||||
_name = 'foo.bar.base'
|
||||
_collection = 'foo.bar.collection' # name of the model above
|
||||
|
||||
|
||||
class Foo(Component):
|
||||
_name = 'foo'
|
||||
_inherit = 'foo.bar.base' # we will inherit the _collection
|
||||
_apply_on = 'res.users'
|
||||
_usage = 'speak'
|
||||
|
||||
def utter(self, message):
|
||||
print message
|
||||
|
||||
|
||||
class Bar(Component):
|
||||
_name = 'bar'
|
||||
_inherit = 'foo.bar.base' # we will inherit the _collection
|
||||
_apply_on = 'res.users'
|
||||
_usage = 'yell'
|
||||
|
||||
def utter(self, message):
|
||||
print message.upper() + '!!!'
|
||||
|
||||
|
||||
class Vocalizer(Component):
|
||||
_name = 'vocalizer'
|
||||
_inherit = 'foo.bar.base'
|
||||
_usage = 'vocalizer'
|
||||
# can be used for any model
|
||||
|
||||
def vocalize(action, message):
|
||||
self.component(usage=action).utter(message)
|
||||
|
||||
|
||||
And their usage::
|
||||
|
||||
>>> coll = self.env['foo.bar.collection'].browse(1)
|
||||
>>> with coll.work_on('res.users') as work:
|
||||
... vocalizer = work.component(usage='vocalizer')
|
||||
... vocalizer.vocalize('speak', 'hello world')
|
||||
...
|
||||
hello world
|
||||
... vocalizer.vocalize('yell', 'hello world')
|
||||
HELLO WORLD!!!
|
||||
|
||||
Hints:
|
||||
|
||||
* If you want to create components without ``_apply_on``, choose a
|
||||
``_usage`` that will not conflict other existing components.
|
||||
* Unless this is what you want and in that case you use
|
||||
:meth:`many_components` which will return all components for a usage
|
||||
with a matching or a not set ``_apply_on``.
|
||||
* It is advised to namespace the names of the components (e.g.
|
||||
``magento.xxx``) to prevent conflicts between addons.
|
||||
|
||||
"""
|
||||
|
||||
_register = False
|
||||
_abstract = True
|
||||
|
||||
# used for inheritance
|
||||
_name = None #: Name of the component
|
||||
|
||||
#: Name or list of names of the component(s) to inherit from
|
||||
_inherit = None
|
||||
|
||||
#: name of the collection to subscribe in
|
||||
_collection = None
|
||||
|
||||
#: List of models on which the component can be applied.
|
||||
#: None means any Model, can be a list ['res.users', ...]
|
||||
_apply_on = None
|
||||
|
||||
#: Component purpose ('import.mapper', ...).
|
||||
_usage = None
|
||||
|
||||
def __init__(self, work_context):
|
||||
super().__init__()
|
||||
self.work = work_context
|
||||
|
||||
@classmethod
|
||||
def _component_match(cls, work, usage=None, model_name=None, **kw):
|
||||
"""Evaluated on candidate components
|
||||
|
||||
When a component lookup is done and candidate(s) have
|
||||
been found for a usage, a final call is done on this method.
|
||||
If the method return False, the candidate component is ignored.
|
||||
|
||||
It can be used for instance to dynamically choose a component
|
||||
according to a value in the :class:`WorkContext`.
|
||||
|
||||
Beware, if the lookups from usage, model and collection are
|
||||
cached, the calls to :meth:`_component_match` are executed
|
||||
each time we get components. Heavy computation should be
|
||||
avoided.
|
||||
|
||||
:param work: the :class:`WorkContext` we are working with
|
||||
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def collection(self):
|
||||
"""Collection we are working with"""
|
||||
return self.work.collection
|
||||
|
||||
@property
|
||||
def env(self):
|
||||
"""Current Odoo environment, the one of the collection record"""
|
||||
return self.work.env
|
||||
|
||||
@property
|
||||
def model(self):
|
||||
"""The model instance we are working with"""
|
||||
return self.work.model
|
||||
|
||||
def component_by_name(self, name, model_name=None):
|
||||
"""Return a component by its name
|
||||
|
||||
Shortcut to meth:`~WorkContext.component_by_name`
|
||||
"""
|
||||
return self.work.component_by_name(name, model_name=model_name)
|
||||
|
||||
def component(self, usage=None, model_name=None, **kw):
|
||||
"""Return a component
|
||||
|
||||
Shortcut to meth:`~WorkContext.component`
|
||||
"""
|
||||
return self.work.component(usage=usage, model_name=model_name, **kw)
|
||||
|
||||
def many_components(self, usage=None, model_name=None, **kw):
|
||||
"""Return several components
|
||||
|
||||
Shortcut to meth:`~WorkContext.many_components`
|
||||
"""
|
||||
return self.work.many_components(usage=usage, model_name=model_name, **kw)
|
||||
|
||||
def __str__(self):
|
||||
return "Component(%s)" % self._name
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
@classmethod
|
||||
def _build_component(cls, registry):
|
||||
"""Instantiate a given Component in the components registry.
|
||||
|
||||
This method is called at the end of the Odoo's registry build. The
|
||||
caller is :meth:`component.builder.ComponentBuilder.load_components`.
|
||||
|
||||
It generates new classes, which will be the Component classes we will
|
||||
be using. The new classes are generated following the inheritance
|
||||
of ``_inherit``. It ensures that the ``__bases__`` of the generated
|
||||
Component classes follow the ``_inherit`` chain.
|
||||
|
||||
Once a Component class is created, it adds it in the Component Registry
|
||||
(:class:`ComponentRegistry`), so it will be available for
|
||||
lookups.
|
||||
|
||||
At the end of new class creation, a hook method
|
||||
:meth:`_complete_component_build` is called, so you can customize
|
||||
further the created components. An example can be found in
|
||||
:meth:`odoo.addons.connector.components.mapper.Mapper._complete_component_build`
|
||||
|
||||
The following code is roughly the same than the Odoo's one for
|
||||
building Models.
|
||||
|
||||
"""
|
||||
|
||||
# In the simplest case, the component's registry class inherits from
|
||||
# cls and the other classes that define the component in a flat
|
||||
# hierarchy. The registry contains the instance ``component`` (on the
|
||||
# left). Its class, ``ComponentClass``, carries inferred metadata that
|
||||
# is shared between all the component's instances for this registry
|
||||
# only.
|
||||
#
|
||||
# class A1(Component): Component
|
||||
# _name = 'a' / | \
|
||||
# A3 A2 A1
|
||||
# class A2(Component): \ | /
|
||||
# _inherit = 'a' ComponentClass
|
||||
#
|
||||
# class A3(Component):
|
||||
# _inherit = 'a'
|
||||
#
|
||||
# When a component is extended by '_inherit', its base classes are
|
||||
# modified to include the current class and the other inherited
|
||||
# component classes.
|
||||
# Note that we actually inherit from other ``ComponentClass``, so that
|
||||
# extensions to an inherited component are immediately visible in the
|
||||
# current component class, like in the following example:
|
||||
#
|
||||
# class A1(Component):
|
||||
# _name = 'a' Component
|
||||
# / / \ \
|
||||
# class B1(Component): / A2 A1 \
|
||||
# _name = 'b' / \ / \
|
||||
# B2 ComponentA B1
|
||||
# class B2(Component): \ | /
|
||||
# _name = 'b' \ | /
|
||||
# _inherit = ['b', 'a'] \ | /
|
||||
# ComponentB
|
||||
# class A2(Component):
|
||||
# _inherit = 'a'
|
||||
|
||||
# determine inherited components
|
||||
parents = cls._inherit
|
||||
if isinstance(parents, str):
|
||||
parents = [parents]
|
||||
elif parents is None:
|
||||
parents = []
|
||||
|
||||
if cls._name in registry and not parents:
|
||||
raise TypeError(
|
||||
"Component %r (in class %r) already exists. "
|
||||
"Consider using _inherit instead of _name "
|
||||
"or using a different _name." % (cls._name, cls)
|
||||
)
|
||||
|
||||
# determine the component's name
|
||||
name = cls._name or (len(parents) == 1 and parents[0])
|
||||
|
||||
if not name:
|
||||
raise TypeError("Component %r must have a _name" % cls)
|
||||
|
||||
# all components except 'base' implicitly inherit from 'base'
|
||||
if name != "base":
|
||||
parents = list(parents) + ["base"]
|
||||
|
||||
# create or retrieve the component's class
|
||||
if name in parents:
|
||||
if name not in registry:
|
||||
raise TypeError("Component %r does not exist in registry." % name)
|
||||
ComponentClass = registry[name]
|
||||
ComponentClass._build_component_check_base(cls)
|
||||
check_parent = ComponentClass._build_component_check_parent
|
||||
else:
|
||||
ComponentClass = type(
|
||||
name,
|
||||
(AbstractComponent,),
|
||||
{
|
||||
"_name": name,
|
||||
"_register": False,
|
||||
# names of children component
|
||||
"_inherit_children": OrderedSet(),
|
||||
},
|
||||
)
|
||||
check_parent = cls._build_component_check_parent
|
||||
|
||||
# determine all the classes the component should inherit from
|
||||
bases = LastOrderedSet([cls])
|
||||
for parent in parents:
|
||||
if parent not in registry:
|
||||
raise TypeError(
|
||||
"Component %r inherits from non-existing component %r."
|
||||
% (name, parent)
|
||||
)
|
||||
parent_class = registry[parent]
|
||||
if parent == name:
|
||||
for base in parent_class.__bases__:
|
||||
bases.add(base)
|
||||
else:
|
||||
check_parent(cls, parent_class)
|
||||
bases.add(parent_class)
|
||||
parent_class._inherit_children.add(name)
|
||||
ComponentClass.__bases__ = tuple(bases)
|
||||
|
||||
ComponentClass._complete_component_build()
|
||||
|
||||
registry[name] = ComponentClass
|
||||
|
||||
return ComponentClass
|
||||
|
||||
@classmethod
|
||||
def _build_component_check_base(cls, extend_cls):
|
||||
"""Check whether ``cls`` can be extended with ``extend_cls``."""
|
||||
if cls._abstract and not extend_cls._abstract:
|
||||
msg = (
|
||||
"%s transforms the abstract component %r into a "
|
||||
"non-abstract component. "
|
||||
"That class should either inherit from AbstractComponent, "
|
||||
"or set a different '_name'."
|
||||
)
|
||||
raise TypeError(msg % (extend_cls, cls._name))
|
||||
|
||||
@classmethod
|
||||
def _build_component_check_parent(component_class, cls, parent_class): # noqa: B902
|
||||
"""Check whether ``model_class`` can inherit from ``parent_class``."""
|
||||
if component_class._abstract and not parent_class._abstract:
|
||||
msg = (
|
||||
"In %s, the abstract Component %r cannot inherit "
|
||||
"from the non-abstract Component %r."
|
||||
)
|
||||
raise TypeError(msg % (cls, component_class._name, parent_class._name))
|
||||
|
||||
@classmethod
|
||||
def _complete_component_build(cls):
|
||||
"""Complete build of the new component class
|
||||
|
||||
After the component has been built from its bases, this method is
|
||||
called, and can be used to customize the class before it can be used.
|
||||
|
||||
Nothing is done in the base Component, but a Component can inherit
|
||||
the method to add its own behavior.
|
||||
"""
|
||||
|
||||
|
||||
class Component(AbstractComponent):
|
||||
"""Concrete Component class
|
||||
|
||||
This is the class you inherit from when you want your component to
|
||||
be registered in the component collections.
|
||||
|
||||
Look in :class:`AbstractComponent` for more details.
|
||||
|
||||
"""
|
||||
|
||||
_register = False
|
||||
_abstract = False
|
||||
Loading…
Add table
Add a link
Reference in a new issue