From 4f3b897839d691a2cb71f3c7e1d15727ecf6ee48 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Sun, 15 Jan 2017 22:24:44 +0100 Subject: [PATCH 001/122] Extract 'components' into an individual addon --- component/__init__.py | 7 + component/__manifest__.py | 16 ++ component/base_components.py | 9 + component/builder.py | 49 ++++++ component/core.py | 300 +++++++++++++++++++++++++++++++++ component/models/__init__.py | 3 + component/models/collection.py | 49 ++++++ 7 files changed, 433 insertions(+) create mode 100644 component/__init__.py create mode 100644 component/__manifest__.py create mode 100644 component/base_components.py create mode 100644 component/builder.py create mode 100644 component/core.py create mode 100644 component/models/__init__.py create mode 100644 component/models/collection.py diff --git a/component/__init__.py b/component/__init__.py new file mode 100644 index 0000000000..fa68443ab7 --- /dev/null +++ b/component/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- +from . import core + +from . import base_components +from . import builder +from . import collection +from . import models diff --git a/component/__manifest__.py b/component/__manifest__.py new file mode 100644 index 0000000000..a5faac7fca --- /dev/null +++ b/component/__manifest__.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Copyright 2013-2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +{'name': 'Components', + 'version': '10.0.1.0.0', + 'author': 'Camptocamp,' + 'Odoo Community Association (OCA)', + 'website': 'https://www.camptocamp.com', + 'license': 'AGPL-3', + 'category': 'Generic Modules', + 'depends': ['base', + ], + 'data': [], + 'installable': True, + } diff --git a/component/base_components.py b/component/base_components.py new file mode 100644 index 0000000000..d08327a706 --- /dev/null +++ b/component/base_components.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from .core import Component + + +class BaseComponent(Component): + _name = 'base' diff --git a/component/builder.py b/component/builder.py new file mode 100644 index 0000000000..9436769799 --- /dev/null +++ b/component/builder.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import odoo +from odoo import api, models +from .core import MetaComponent, all_components + + +class ComponentBuilder(models.AbstractModel): + """ Build the component classes + + And register them in a global registry. + The classes are built using the same mechanism + than the Odoo one, meaning that a final class + that inherits from all the ``_inherit`` is created. + This class is kept in the ``all_components`` global + registry with the Components ``_name`` as keys. + + This is an Odoo model so we can hook the build of the components at the end + of the registry loading with ``_register_hook``, after all modules are + loaded. + + """ + _name = 'component.builder' + _description = 'Component Builder' + + @api.model_cr + def _register_hook(self): + all_components.clear() + + graph = odoo.modules.graph.Graph() + graph.add_module(self.env.cr, 'base') + + self.env.cr.execute( + "SELECT name " + "FROM ir_module_module " + "WHERE state IN ('installed', 'to upgrade', 'to update')" + ) + module_list = [name for (name,) in self.env.cr.fetchall() + if name not in graph] + graph.add_modules(self.env.cr, module_list) + + for module in graph: + self.load_components(module.name, all_components) + + def load_components(self, module, registry): + for component_class in MetaComponent._modules_components[module]: + component_class._build_component(registry) diff --git a/component/core.py b/component/core.py new file mode 100644 index 0000000000..c18f4350dd --- /dev/null +++ b/component/core.py @@ -0,0 +1,300 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# Copyright 2017 Odoo +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from collections import defaultdict, OrderedDict + +from odoo.tools import OrderedSet, LastOrderedSet + + +# 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 (OpenERP) 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 ComponentGlobalRegistry(OrderedDict): + """ Store all the components by name + + Allow to _inherit components. + + Another registry allow to register components on a + particular collection and to find them back. + + This is an OrderedDict, because we want to keep the + registration order of the components, addons loaded first + have their components found first (when we look for a list + components using `multi`). + + """ + +all_components = ComponentGlobalRegistry() + + +class WorkContext(object): + + def __init__(self, collection, model_name, **kwargs): + self.collection = collection + self.model_name = model_name + self.model = self.env[model_name] + self._propagate_kwargs = [] + for attr_name, value in kwargs.iteritems: + setattr(self, attr_name, value) + self._propagate_kwargs.append(attr_name) + + @property + def env(self): + return self.collection.env + + def work_on(self, model_name): + kwargs = {attr_name: getattr(self, attr_name) + for attr_name in self._propagate_kwargs} + return self.__class__(self.collection, model_name, **kwargs) + + def components(self, name=None, usage=None, model_name=None, multi=False): + all_components['base'](self).components( + name=name, + usage=usage, + model_name=model_name, + multi=multi, + ) + + def __str__(self): + return "WorkContext(%s,%s)" % (repr(self.collection), self.model_name) + + def __unicode__(self): + return unicode(str(self)) + + __repr__ = __str__ + + +class MetaComponent(type): + + _modules_components = defaultdict(list) + + def __init__(self, name, bases, attrs): + if not self._register: + self._register = True + super(MetaComponent, self).__init__(name, bases, attrs) + return + + if not hasattr(self, '_module'): + self._module = _get_addon_name(self.__module__) + + self._modules_components[self._module].append(self) + + +class Component(object): + __metaclass__ = MetaComponent + + _register = False + + _name = None + _inherit = None + + # name of the collection to subscribe in, abstract when None + _collection = None + + _apply_on = None # None means any Model, can be a list ['res.users', ...] + _usage = None # component purpose, might be a list? ['import.mapper', ...] + + def __init__(self, work_context): + super(Component, self).__init__() + self.work = work_context + + @property + def apply_on_models(self): + # None means all models + if self._apply_on is None: + return None + # always return a list, used for the lookup + elif isinstance(self._apply_on, basestring): + return [self._apply_on] + return self._apply_on + + @property + def collection(self): + return self.work.collection + + @property + def env(self): + return self.collection.env + + @property + def model(self): + return self.collection.model + + # TODO use a LRU cache (repoze.lru, beware we must include the collection + # name in the cache but not 'self') + @staticmethod # staticmethod in order to use a LRU cache on all args + def lookup(collection_name, name=None, usage=None, model_name=None, + multi=False): + # keep the order so addons loaded first have components used first + # in case of multi=True + candidates = OrderedSet() + if name is not None: + component = all_components.get(name) + if not component: + # TODO: which error type? + raise ValueError("No component with name '%s' found." % name) + candidates.add(component) + + if usage is not None: + components = [c for c in all_components.itervalues() + if c._usage == usage] + if components: + candidates.update(components) + + if name is None and usage is None: + candidates.update(all_components.values()) + + # filter out by model name + candidates = OrderedSet(c for c in candidates + if c.apply_on_models is None + or model_name in c.apply_on_models) + + if not multi and len(candidates) > 1: + # TODO which error type? + raise ValueError( + "Several components found for collection '%s', name '%s', " + "usage '%s', model_name '%s'. Found: %s" % + (collection_name, name, usage, model_name, candidates) + ) + + return candidates + + def components(self, name=None, usage=None, model_name=None, multi=False): + return self.lookup( + self.collection._name, + name=name, + usage=usage, + model_name=model_name, + multi=multi, + )(self.work) + + def __str__(self): + return "Component(%s)" % self._name + + def __unicode__(self): + return unicode(str(self)) + + __repr__ = __str__ + + # + # Goal: try to apply inheritance at the instantiation level and + # put objects in the registry var + # + @classmethod + def _build_component(cls, registry): + """ Instantiate a given Component in the registry. + + This method creates or extends a "registry" class for the given + component. + This "registry" class carries inferred component metadata, and inherits + (in the Python sense) from all classes that define the component, and + possibly other registry classes. + + """ + + # 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 = ['a', 'b'] \ | / + # ComponentB + # class A2(Component): + # _inherit = 'a' + + # determine inherited components + parents = cls._inherit + if isinstance(parents, basestring): + parents = [parents] + elif parents is None: + parents = [] + + # determine the component's name + name = cls._name or (len(parents) == 1 and parents[0]) or cls.__name__ + + # 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] + else: + ComponentClass = type(name, (Component,), { + '_name': name, + '_register': False, + # names of children component + '_inherit_children': OrderedSet(), + }) + + # 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: + bases.add(parent_class) + parent_class._inherit_children.add(name) + ComponentClass.__bases__ = tuple(bases) + + # determine the attributes of the component's class + ComponentClass._build_component_attributes(registry) + + registry[name] = ComponentClass + + return ComponentClass + + @classmethod + def _build_component_attributes(cls, registry): + """ Initialize base component attributes. """ + # TODO: see if we concatenate models, usage, ... diff --git a/component/models/__init__.py b/component/models/__init__.py new file mode 100644 index 0000000000..4343b4aeca --- /dev/null +++ b/component/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import collection diff --git a/component/models/collection.py b/component/models/collection.py new file mode 100644 index 0000000000..3343563ca6 --- /dev/null +++ b/component/models/collection.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + + +from odoo import models, api +from .core import WorkContext + + +class Collection(models.AbstractModel): + """ The model on which components are subscribed + + It would be for instance the ``backend`` for the connectors. + + Example:: + + class MagentoBackend(models.Model): + _name = 'magento.backend' # name of the collection + _inherit = 'collection.base' + + + class MagentoSaleImporter(Component): + _name = 'magento.sale.importer' + _apply_on = 'magento.sale.order' + _collection = 'magento.backend' # name of the collection + + def run(self, magento_id): + mapper = self.components(name='magento.sale.importer.mapper') + extra_mappers = self.components( + usage='magento.sale.importer.mapper', + multi=True, + ) + # ... + + # use it: + + backend = self.env['magento.backend'].browse(1) + work = backend.work_on('magento.sale.order') + importer = work.components(name='magento.sale.importer') + importer.run(1) + + + """ + _name = 'collection.base' + _description = 'Base Abstract Collection' + + @api.multi + def work_on(self, model_name, **kwargs): + return WorkContext(self, model_name, **kwargs) From 1f701b619bac99117798482581a3b05682fffdd1 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Sun, 15 Jan 2017 22:50:24 +0100 Subject: [PATCH 002/122] Add test_component --- component/builder.py | 1 + component/core.py | 53 +++++++++++++++++++--------------- component/models/collection.py | 2 +- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/component/builder.py b/component/builder.py index 9436769799..5dec656f7a 100644 --- a/component/builder.py +++ b/component/builder.py @@ -28,6 +28,7 @@ class ComponentBuilder(models.AbstractModel): @api.model_cr def _register_hook(self): all_components.clear() + # TODO: reset the LRU cache of the component lookups graph = odoo.modules.graph.Graph() graph.add_module(self.env.cr, 'base') diff --git a/component/core.py b/component/core.py index c18f4350dd..360e4bf9fc 100644 --- a/component/core.py +++ b/component/core.py @@ -49,7 +49,7 @@ def __init__(self, collection, model_name, **kwargs): self.model_name = model_name self.model = self.env[model_name] self._propagate_kwargs = [] - for attr_name, value in kwargs.iteritems: + for attr_name, value in kwargs.iteritems(): setattr(self, attr_name, value) self._propagate_kwargs.append(attr_name) @@ -63,7 +63,7 @@ def work_on(self, model_name): return self.__class__(self.collection, model_name, **kwargs) def components(self, name=None, usage=None, model_name=None, multi=False): - all_components['base'](self).components( + return all_components['base'](self).components( name=name, usage=usage, model_name=model_name, @@ -94,6 +94,16 @@ def __init__(self, name, bases, attrs): self._modules_components[self._module].append(self) + @property + def apply_on_models(self): + # None means all models + if self._apply_on is None: + return None + # always return a list, used for the lookup + elif isinstance(self._apply_on, basestring): + return [self._apply_on] + return self._apply_on + class Component(object): __metaclass__ = MetaComponent @@ -113,16 +123,6 @@ def __init__(self, work_context): super(Component, self).__init__() self.work = work_context - @property - def apply_on_models(self): - # None means all models - if self._apply_on is None: - return None - # always return a list, used for the lookup - elif isinstance(self._apply_on, basestring): - return [self._apply_on] - return self._apply_on - @property def collection(self): return self.work.collection @@ -160,17 +160,24 @@ def lookup(collection_name, name=None, usage=None, model_name=None, candidates.update(all_components.values()) # filter out by model name - candidates = OrderedSet(c for c in candidates - if c.apply_on_models is None - or model_name in c.apply_on_models) - - if not multi and len(candidates) > 1: - # TODO which error type? - raise ValueError( - "Several components found for collection '%s', name '%s', " - "usage '%s', model_name '%s'. Found: %s" % - (collection_name, name, usage, model_name, candidates) - ) + if model_name is not None: + candidates = OrderedSet(c for c in candidates + if c.apply_on_models is None + or model_name in c.apply_on_models) + + if not candidates: + # TODO: do we want to raise? + raise ValueError("No component found.") + + if not multi: + if len(candidates) > 1: + # TODO which error type? + raise ValueError( + "Several components found for collection '%s', name '%s', " + "usage '%s', model_name '%s'. Found: %s" % + (collection_name, name, usage, model_name, candidates) + ) + return candidates.pop() return candidates diff --git a/component/models/collection.py b/component/models/collection.py index 3343563ca6..34cc5345f1 100644 --- a/component/models/collection.py +++ b/component/models/collection.py @@ -4,7 +4,7 @@ from odoo import models, api -from .core import WorkContext +from ..core import WorkContext class Collection(models.AbstractModel): From 10c5bf70092d0b93c08fcf393bf823e8b1be486f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 19 Jan 2017 21:40:38 +0100 Subject: [PATCH 003/122] Separate lookup by name and by usage The favorite lookup should be by 'usage' (kinda interface) and by model name ('components()' method). The lookup by component name should normally not be used as it reduces the flexibility. Using a different method for this lookup discourage its usage. Also, the lookup by component name completely ignores the current collection, which is bad or not bad depending of what you want to achieve. --- component/core.py | 74 ++++++++++++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 30 deletions(-) diff --git a/component/core.py b/component/core.py index 360e4bf9fc..7deda7ab80 100644 --- a/component/core.py +++ b/component/core.py @@ -62,9 +62,11 @@ def work_on(self, model_name): for attr_name in self._propagate_kwargs} return self.__class__(self.collection, model_name, **kwargs) - def components(self, name=None, usage=None, model_name=None, multi=False): + def component_by_name(self, name): + return all_components['base'](self).component_by_name(name) + + def components(self, usage=None, model_name=None, multi=False): return all_components['base'](self).components( - name=name, usage=usage, model_name=model_name, multi=multi, @@ -138,57 +140,69 @@ def model(self): # TODO use a LRU cache (repoze.lru, beware we must include the collection # name in the cache but not 'self') @staticmethod # staticmethod in order to use a LRU cache on all args - def lookup(collection_name, name=None, usage=None, model_name=None, - multi=False): + def lookup(collection_name, usage=None, model_name=None, multi=False): + # TODO: verify that ordering is kept + # keep the order so addons loaded first have components used first # in case of multi=True - candidates = OrderedSet() - if name is not None: - component = all_components.get(name) - if not component: - # TODO: which error type? - raise ValueError("No component with name '%s' found." % name) - candidates.add(component) + collection_components = [ + component for component in all_components.itervalues() + if component._collection == collection_name + ] + candidates = [] if usage is not None: - components = [c for c in all_components.itervalues() - if c._usage == usage] + components = [component for component in collection_components + if component._usage == usage] if components: - candidates.update(components) - - if name is None and usage is None: - candidates.update(all_components.values()) + candidates = components + else: + candidates = collection_components.values() # filter out by model name - if model_name is not None: - candidates = OrderedSet(c for c in candidates - if c.apply_on_models is None - or model_name in c.apply_on_models) + candidates = [c for c in candidates + if c.apply_on_models is None + or model_name in c.apply_on_models] if not candidates: # TODO: do we want to raise? - raise ValueError("No component found.") + raise ValueError( + "No component found for collection '%s', " + "usage '%s', model_name '%s'." % + (collection_name, usage, model_name) + ) if not multi: if len(candidates) > 1: # TODO which error type? raise ValueError( - "Several components found for collection '%s', name '%s', " - "usage '%s', model_name '%s'. Found: %s" % - (collection_name, name, usage, model_name, candidates) + "Several components found for collection '%s', " + "usage '%s', model_name '%s'. Found: %r" % + (collection_name, usage, model_name, candidates) ) return candidates.pop() return candidates - def components(self, name=None, usage=None, model_name=None, multi=False): - return self.lookup( + def component_by_name(self, name): + component = all_components.get(name) + if not component: + # TODO: which error type? + raise ValueError("No component with name '%s' found." % name) + return component + + def components(self, usage=None, model_name=None, multi=False): + component_class = self.lookup( self.collection._name, - name=name, usage=usage, - model_name=model_name, + model_name=model_name or self.work.model_name, multi=multi, - )(self.work) + ) + if model_name is None or model_name == self.work.model_name: + work_context = self.work + else: + work_context = self.work.work_on(model_name) + return component_class(work_context) def __str__(self): return "Component(%s)" % self._name From caf7f93e2d1616a787876794a2ad1317d6729b80 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 12 Jun 2017 15:13:26 +0200 Subject: [PATCH 004/122] Various fixes in component --- component/__init__.py | 1 - component/core.py | 11 ++++++++--- component/exception.py | 15 +++++++++++++++ component/models/collection.py | 1 + 4 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 component/exception.py diff --git a/component/__init__.py b/component/__init__.py index fa68443ab7..7d8580dec8 100644 --- a/component/__init__.py +++ b/component/__init__.py @@ -3,5 +3,4 @@ from . import base_components from . import builder -from . import collection from . import models diff --git a/component/core.py b/component/core.py index 7deda7ab80..07c08a292f 100644 --- a/component/core.py +++ b/component/core.py @@ -5,7 +5,9 @@ from collections import defaultdict, OrderedDict +from odoo import models from odoo.tools import OrderedSet, LastOrderedSet +from .exception import NoComponentError, SeveralComponentError # this is duplicated from odoo.models.MetaModel._get_addon_name() which we @@ -39,6 +41,7 @@ class ComponentGlobalRegistry(OrderedDict): """ + all_components = ComponentGlobalRegistry() @@ -135,7 +138,7 @@ def env(self): @property def model(self): - return self.collection.model + return self.work.model # TODO use a LRU cache (repoze.lru, beware we must include the collection # name in the cache but not 'self') @@ -166,7 +169,7 @@ def lookup(collection_name, usage=None, model_name=None, multi=False): if not candidates: # TODO: do we want to raise? - raise ValueError( + raise NoComponentError( "No component found for collection '%s', " "usage '%s', model_name '%s'." % (collection_name, usage, model_name) @@ -175,7 +178,7 @@ def lookup(collection_name, usage=None, model_name=None, multi=False): if not multi: if len(candidates) > 1: # TODO which error type? - raise ValueError( + raise SeveralComponentError( "Several components found for collection '%s', " "usage '%s', model_name '%s'. Found: %r" % (collection_name, usage, model_name, candidates) @@ -192,6 +195,8 @@ def component_by_name(self, name): return component def components(self, usage=None, model_name=None, multi=False): + if isinstance(model_name, models.BaseModel): + model_name = model_name._name component_class = self.lookup( self.collection._name, usage=usage, diff --git a/component/exception.py b/component/exception.py new file mode 100644 index 0000000000..71fcd38b38 --- /dev/null +++ b/component/exception.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + + +class ComponentException(Exception): + """ Base Exception for the components """ + + +class NoComponentError(ComponentException): + """ No component has been found """ + + +class SeveralComponentError(ComponentException): + """ More than one component have been found """ diff --git a/component/models/collection.py b/component/models/collection.py index 34cc5345f1..12e0561805 100644 --- a/component/models/collection.py +++ b/component/models/collection.py @@ -46,4 +46,5 @@ def run(self, magento_id): @api.multi def work_on(self, model_name, **kwargs): + self.ensure_one() return WorkContext(self, model_name, **kwargs) From 3a89d3ea739ec4daa552bd6c6850515f5e8ea1b9 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 13 Jun 2017 13:10:14 +0200 Subject: [PATCH 005/122] Add AbstractComponent --- component/base_components.py | 4 ++-- component/builder.py | 2 +- component/core.py | 18 +++++++++++++----- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/component/base_components.py b/component/base_components.py index d08327a706..0c4cfefdb7 100644 --- a/component/base_components.py +++ b/component/base_components.py @@ -2,8 +2,8 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -from .core import Component +from .core import AbstractComponent -class BaseComponent(Component): +class BaseComponent(AbstractComponent): _name = 'base' diff --git a/component/builder.py b/component/builder.py index 5dec656f7a..f8805707cd 100644 --- a/component/builder.py +++ b/component/builder.py @@ -28,7 +28,7 @@ class ComponentBuilder(models.AbstractModel): @api.model_cr def _register_hook(self): all_components.clear() - # TODO: reset the LRU cache of the component lookups + # TODO: reset the LRU cache of the component lookups when implemented graph = odoo.modules.graph.Graph() graph.add_module(self.env.cr, 'base') diff --git a/component/core.py b/component/core.py index 07c08a292f..414a19fab7 100644 --- a/component/core.py +++ b/component/core.py @@ -110,22 +110,23 @@ def apply_on_models(self): return self._apply_on -class Component(object): +class AbstractComponent(object): __metaclass__ = MetaComponent _register = False + _abstract = True _name = None _inherit = None - # name of the collection to subscribe in, abstract when None + # name of the collection to subscribe in _collection = None _apply_on = None # None means any Model, can be a list ['res.users', ...] - _usage = None # component purpose, might be a list? ['import.mapper', ...] + _usage = None # component purpose ('import.mapper', ...) def __init__(self, work_context): - super(Component, self).__init__() + super(AbstractComponent, self).__init__() self.work = work_context @property @@ -151,6 +152,7 @@ def lookup(collection_name, usage=None, model_name=None, multi=False): collection_components = [ component for component in all_components.itervalues() if component._collection == collection_name + and not component._abstract ] candidates = [] @@ -183,6 +185,7 @@ def lookup(collection_name, usage=None, model_name=None, multi=False): "usage '%s', model_name '%s'. Found: %r" % (collection_name, usage, model_name, candidates) ) + # TODO: always return a list here, use a 2 methods for multi/normal return candidates.pop() return candidates @@ -289,7 +292,7 @@ def _build_component(cls, registry): name) ComponentClass = registry[name] else: - ComponentClass = type(name, (Component,), { + ComponentClass = type(name, (AbstractComponent,), { '_name': name, '_register': False, # names of children component @@ -324,3 +327,8 @@ def _build_component(cls, registry): def _build_component_attributes(cls, registry): """ Initialize base component attributes. """ # TODO: see if we concatenate models, usage, ... + + +class Component(AbstractComponent): + _register = False + _abstract = False From 9979690ea193d4cdaa3856fef2d1b9937b3df1a4 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 13 Jun 2017 13:40:46 +0200 Subject: [PATCH 006/122] Remove overrides of attributes when inheriting from multiple components, we might inadvertly override a value with None --- component/core.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/component/core.py b/component/core.py index 414a19fab7..08f6dda592 100644 --- a/component/core.py +++ b/component/core.py @@ -170,7 +170,6 @@ def lookup(collection_name, usage=None, model_name=None, multi=False): or model_name in c.apply_on_models] if not candidates: - # TODO: do we want to raise? raise NoComponentError( "No component found for collection '%s', " "usage '%s', model_name '%s'." % @@ -179,7 +178,6 @@ def lookup(collection_name, usage=None, model_name=None, multi=False): if not multi: if len(candidates) > 1: - # TODO which error type? raise SeveralComponentError( "Several components found for collection '%s', " "usage '%s', model_name '%s'. Found: %r" % From 2d922b2799695509d03f39fea3ad2417634883c9 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 13 Jun 2017 16:23:22 +0200 Subject: [PATCH 007/122] Allow to share some components across collections Used for instance by the 'ecommerce' components --- component/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/component/core.py b/component/core.py index 08f6dda592..7d1a9ece84 100644 --- a/component/core.py +++ b/component/core.py @@ -151,7 +151,8 @@ def lookup(collection_name, usage=None, model_name=None, multi=False): # in case of multi=True collection_components = [ component for component in all_components.itervalues() - if component._collection == collection_name + if (component._collection == collection_name + or component._collection is None) and not component._abstract ] candidates = [] From 2021106b475ea221b80150228d28676c78a25728 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 13 Jun 2017 17:09:01 +0200 Subject: [PATCH 008/122] Instanciate the component returned by name --- component/core.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/component/core.py b/component/core.py index 7d1a9ece84..b66c004f57 100644 --- a/component/core.py +++ b/component/core.py @@ -189,12 +189,21 @@ def lookup(collection_name, usage=None, model_name=None, multi=False): return candidates - def component_by_name(self, name): - component = all_components.get(name) - if not component: + def _component_class_by_name(self, name): + component_class = all_components.get(name) + if not component_class: # TODO: which error type? raise ValueError("No component with name '%s' found." % name) - return component + return component_class + + def component_by_name(self, name, model_name=None): + if model_name is None or model_name == self.work.model_name: + work_context = self.work + else: + work_context = self.work.work_on(model_name) + + component_class = self._component_class_by_name(name) + return component_class(work_context) def components(self, usage=None, model_name=None, multi=False): if isinstance(model_name, models.BaseModel): From 36a3b7137617584897662ab5acf2358d8ce05431 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 14 Jun 2017 10:02:08 +0200 Subject: [PATCH 009/122] Get rid of MetaMapper The mapping methods are now built by the component initialization when the "aggregated" class is built. --- component/core.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/component/core.py b/component/core.py index b66c004f57..89d5318337 100644 --- a/component/core.py +++ b/component/core.py @@ -300,12 +300,13 @@ def _build_component(cls, registry): name) ComponentClass = registry[name] else: - ComponentClass = type(name, (AbstractComponent,), { - '_name': name, - '_register': False, - # names of children component - '_inherit_children': OrderedSet(), - }) + ComponentClass = type( + name, (AbstractComponent,), + {'_name': name, + '_register': False, + # names of children component + '_inherit_children': OrderedSet()}, + ) # determine all the classes the component should inherit from bases = LastOrderedSet([cls]) @@ -324,17 +325,22 @@ def _build_component(cls, registry): parent_class._inherit_children.add(name) ComponentClass.__bases__ = tuple(bases) - # determine the attributes of the component's class - ComponentClass._build_component_attributes(registry) + ComponentClass._complete_component_build() registry[name] = ComponentClass return ComponentClass @classmethod - def _build_component_attributes(cls, registry): - """ Initialize base component attributes. """ - # TODO: see if we concatenate models, usage, ... + 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): From dc4a20d2290ebaa15fccf9679fc1d796cc68dfda Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 14 Jun 2017 12:30:00 +0200 Subject: [PATCH 010/122] Add check to help find duplicate components And remove automatic naming from class name, not explicit enough, especially when we have to inherit from one. We could take the total opposite and only use class names components: class MyComponent(Component): ... class MyComponentExtended(Component): _inherit = 'MyComponent' But it would be less close to the odoo's API. --- component/core.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/component/core.py b/component/core.py index 89d5318337..146d39f4fb 100644 --- a/component/core.py +++ b/component/core.py @@ -286,8 +286,13 @@ def _build_component(cls, registry): elif parents is None: parents = [] + if cls._name in registry: + 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]) or cls.__name__ + name = cls._name or (len(parents) == 1 and parents[0]) # all components except 'base' implicitly inherit from 'base' if name != 'base': From 03c5709dd65eff902320fd6b0debe407dc04af27 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 14 Jun 2017 22:36:15 +0200 Subject: [PATCH 011/122] Fix pep8 --- component/core.py | 15 ++++++++------- component/tests/__init__.py | 3 +++ component/tests/test_build_component.py | 8 ++++++++ 3 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 component/tests/__init__.py create mode 100644 component/tests/test_build_component.py diff --git a/component/core.py b/component/core.py index 146d39f4fb..587f2c1ea3 100644 --- a/component/core.py +++ b/component/core.py @@ -151,9 +151,9 @@ def lookup(collection_name, usage=None, model_name=None, multi=False): # in case of multi=True collection_components = [ component for component in all_components.itervalues() - if (component._collection == collection_name - or component._collection is None) - and not component._abstract + if (component._collection == collection_name or + component._collection is None) and + not component._abstract ] candidates = [] @@ -167,8 +167,8 @@ def lookup(collection_name, usage=None, model_name=None, multi=False): # filter out by model name candidates = [c for c in candidates - if c.apply_on_models is None - or model_name in c.apply_on_models] + if c.apply_on_models is None or + model_name in c.apply_on_models] if not candidates: raise NoComponentError( @@ -260,8 +260,9 @@ def _build_component(cls, registry): # 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. + # 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: diff --git a/component/tests/__init__.py b/component/tests/__init__.py new file mode 100644 index 0000000000..664dd33717 --- /dev/null +++ b/component/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import test_build_component diff --git a/component/tests/test_build_component.py b/component/tests/test_build_component.py new file mode 100644 index 0000000000..57ff15b8ce --- /dev/null +++ b/component/tests/test_build_component.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +# from unittest2 import TestCase + + +# class TestBuildComponent(TestCase): From 7c6d09801b02f8e242590219fb4e8e62da39da9d Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 15 Jun 2017 11:23:24 +0200 Subject: [PATCH 012/122] Add tests to component ============================================================================================================= test session starts ============================================================================================================= platform linux2 -- Python 2.7.9, pytest-3.0.7, py-1.4.33, pluggy-0.4.0 run-last-failure: run all (no recorded failures) rootdir: /opt/odoo, inifile: plugins: cov-2.5.1, odoo-0.2.2 collected 25 items odoo/external-src/connector/component/tests/test_build_component.py ......... odoo/external-src/connector/component/tests/test_component.py ........ odoo/external-src/connector/component/tests/test_lookup.py ...... odoo/external-src/connector/component/tests/test_work_on.py .. ---------- coverage: platform linux2, python 2.7.9-final-0 ----------- Name Stmts Miss Cover ----------------------------------------------------------------------------------------- odoo/external-src/connector/component/__init__.py 4 0 100% odoo/external-src/connector/component/__manifest__.py 1 1 0% odoo/external-src/connector/component/base_components.py 3 0 100% odoo/external-src/connector/component/builder.py 18 0 100% odoo/external-src/connector/component/core.py 155 6 96% odoo/external-src/connector/component/exception.py 3 0 100% odoo/external-src/connector/component/models/__init__.py 1 0 100% odoo/external-src/connector/component/models/collection.py 8 0 100% odoo/external-src/connector/component/tests/__init__.py 4 0 100% odoo/external-src/connector/component/tests/common.py 26 2 92% odoo/external-src/connector/component/tests/test_build_component.py 102 0 100% odoo/external-src/connector/component/tests/test_component.py 56 0 100% odoo/external-src/connector/component/tests/test_lookup.py 84 0 100% odoo/external-src/connector/component/tests/test_work_on.py 26 0 100% ----------------------------------------------------------------------------------------- TOTAL 491 9 98% ========================================================================================================== 25 passed in 0.76 seconds ========================================================================================================== --- component/core.py | 150 ++++++++++++--------- component/models/collection.py | 2 +- component/tests/__init__.py | 3 + component/tests/common.py | 49 +++++++ component/tests/test_build_component.py | 168 +++++++++++++++++++++++- component/tests/test_component.py | 86 ++++++++++++ component/tests/test_lookup.py | 146 ++++++++++++++++++++ component/tests/test_work_on.py | 43 ++++++ 8 files changed, 580 insertions(+), 67 deletions(-) create mode 100644 component/tests/common.py create mode 100644 component/tests/test_component.py create mode 100644 component/tests/test_lookup.py create mode 100644 component/tests/test_work_on.py diff --git a/component/core.py b/component/core.py index 587f2c1ea3..cb107933c8 100644 --- a/component/core.py +++ b/component/core.py @@ -37,21 +37,72 @@ class ComponentGlobalRegistry(OrderedDict): This is an OrderedDict, because we want to keep the registration order of the components, addons loaded first have their components found first (when we look for a list - components using `multi`). + components using `many`). """ + # TODO use a LRU cache (repoze.lru?) + def lookup(self, collection_name, usage=None, + model_name=None, many=False): + + # keep the order so addons loaded first have components used first + # in case of many=True + collection_components = [ + component for component in self.itervalues() + if (component._collection == collection_name or + component._collection is None) and + not component._abstract + ] + candidates = [] + + if usage is not None: + components = [component for component in collection_components + if component._usage == usage] + if components: + candidates = components + else: + candidates = collection_components + + # filter out by model name + candidates = [c for c in candidates + if c.apply_on_models is None or + model_name in c.apply_on_models] + + if not candidates: + raise NoComponentError( + "No component found for collection '%s', " + "usage '%s', model_name '%s'." % + (collection_name, usage, model_name) + ) + + if not many: + if len(candidates) > 1: + raise SeveralComponentError( + "Several components found for collection '%s', " + "usage '%s', model_name '%s'. Found: %r" % + (collection_name, usage, model_name, candidates) + ) + # TODO: always return a list here, use a 2 methods for multi/normal + return candidates.pop() + + return candidates + all_components = ComponentGlobalRegistry() class WorkContext(object): - def __init__(self, collection, model_name, **kwargs): + def __init__(self, collection, model_name, + components_registry=None, **kwargs): self.collection = collection self.model_name = model_name self.model = self.env[model_name] - self._propagate_kwargs = [] + if components_registry: + self.components_registry = components_registry + else: + self.components_registry = all_components + self._propagate_kwargs = ['components_registry'] for attr_name, value in kwargs.iteritems(): setattr(self, attr_name, value) self._propagate_kwargs.append(attr_name) @@ -66,13 +117,13 @@ def work_on(self, model_name): return self.__class__(self.collection, model_name, **kwargs) def component_by_name(self, name): - return all_components['base'](self).component_by_name(name) + return self.components_registry['base'](self).component_by_name(name) - def components(self, usage=None, model_name=None, multi=False): - return all_components['base'](self).components( + def components(self, usage=None, model_name=None, many=False): + return self.components_registry['base'](self).components( usage=usage, model_name=model_name, - multi=multi, + many=many, ) def __str__(self): @@ -141,78 +192,43 @@ def env(self): def model(self): return self.work.model - # TODO use a LRU cache (repoze.lru, beware we must include the collection - # name in the cache but not 'self') - @staticmethod # staticmethod in order to use a LRU cache on all args - def lookup(collection_name, usage=None, model_name=None, multi=False): - # TODO: verify that ordering is kept - - # keep the order so addons loaded first have components used first - # in case of multi=True - collection_components = [ - component for component in all_components.itervalues() - if (component._collection == collection_name or - component._collection is None) and - not component._abstract - ] - candidates = [] - - if usage is not None: - components = [component for component in collection_components - if component._usage == usage] - if components: - candidates = components - else: - candidates = collection_components.values() - - # filter out by model name - candidates = [c for c in candidates - if c.apply_on_models is None or - model_name in c.apply_on_models] - - if not candidates: - raise NoComponentError( - "No component found for collection '%s', " - "usage '%s', model_name '%s'." % - (collection_name, usage, model_name) - ) - - if not multi: - if len(candidates) > 1: - raise SeveralComponentError( - "Several components found for collection '%s', " - "usage '%s', model_name '%s'. Found: %r" % - (collection_name, usage, model_name, candidates) - ) - # TODO: always return a list here, use a 2 methods for multi/normal - return candidates.pop() - - return candidates - def _component_class_by_name(self, name): - component_class = all_components.get(name) + components_registry = self.work.components_registry + component_class = components_registry.get(name) if not component_class: - # TODO: which error type? - raise ValueError("No component with name '%s' found." % name) + raise NoComponentError("No component with name '%s' found." % name) return component_class def component_by_name(self, name, model_name=None): - if model_name is None or model_name == self.work.model_name: + component_class = self._component_class_by_name(name) + work_model = model_name or self.work.model_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 = "'%s'" % (component_class.apply_on_models[0],) + else: + hint_models = "" % (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.work.model_name: work_context = self.work else: work_context = self.work.work_on(model_name) - - component_class = self._component_class_by_name(name) return component_class(work_context) - def components(self, usage=None, model_name=None, multi=False): + def components(self, usage=None, model_name=None, many=False): if isinstance(model_name, models.BaseModel): model_name = model_name._name - component_class = self.lookup( + component_class = self.work.components_registry.lookup( self.collection._name, usage=usage, model_name=model_name or self.work.model_name, - multi=multi, + many=many, ) if model_name is None or model_name == self.work.model_name: work_context = self.work @@ -242,6 +258,9 @@ def _build_component(cls, registry): (in the Python sense) from all classes that define the component, and possibly other registry classes. + 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 @@ -295,6 +314,9 @@ def _build_component(cls, registry): # 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'] diff --git a/component/models/collection.py b/component/models/collection.py index 12e0561805..9ce6e91f0f 100644 --- a/component/models/collection.py +++ b/component/models/collection.py @@ -28,7 +28,7 @@ def run(self, magento_id): mapper = self.components(name='magento.sale.importer.mapper') extra_mappers = self.components( usage='magento.sale.importer.mapper', - multi=True, + many=True, ) # ... diff --git a/component/tests/__init__.py b/component/tests/__init__.py index 664dd33717..ef6a6108b5 100644 --- a/component/tests/__init__.py +++ b/component/tests/__init__.py @@ -1,3 +1,6 @@ # -*- coding: utf-8 -*- from . import test_build_component +from . import test_component +from . import test_lookup +from . import test_work_on diff --git a/component/tests/common.py b/component/tests/common.py new file mode 100644 index 0000000000..d25a3faf95 --- /dev/null +++ b/component/tests/common.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import unittest2 +from odoo.tests import common +from odoo.addons.component.core import ( + AbstractComponent, + ComponentGlobalRegistry, + MetaComponent, +) + + +class ComponentRegistryCase(unittest2.TestCase): + + def setUp(self): + super(ComponentRegistryCase, self).setUp() + self._original_components = MetaComponent._modules_components.copy() + MetaComponent._modules_components.clear() + + self.comp_registry = ComponentGlobalRegistry() + + # there's always an implicit dependency on a 'base' component + # so we must register one + class Base(AbstractComponent): + _name = 'base' + + Base._build_component(self.comp_registry) + + def tearDown(self): + super(ComponentRegistryCase, self).tearDown() + MetaComponent._modules_components = self._original_components + + def _build_components(self, *classes): + for cls in classes: + cls._build_component(self.comp_registry) + + +class TransactionComponentRegistryCase(common.TransactionCase, + ComponentRegistryCase): + + def setUp(self): + common.TransactionCase.setUp(self) + ComponentRegistryCase.setUp(self) + self.collection = self.env['collection.base'] + + def teardown(self): + common.TransactionCase.tearDown(self) + ComponentRegistryCase.tearDown(self) diff --git a/component/tests/test_build_component.py b/component/tests/test_build_component.py index 57ff15b8ce..e41254bae3 100644 --- a/component/tests/test_build_component.py +++ b/component/tests/test_build_component.py @@ -2,7 +2,171 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -# from unittest2 import TestCase +import mock +from odoo.addons.component.core import Component +from .common import ComponentRegistryCase -# class TestBuildComponent(TestCase): +class TestBuildComponent(ComponentRegistryCase): + + def test_no_name(self): + class Component1(Component): + pass + + msg = '.*must have a _name.*' + with self.assertRaisesRegexp(TypeError, msg): + Component1._build_component(self.comp_registry) + + def test_register(self): + class Component1(Component): + _name = 'component1' + + class Component2(Component): + _name = 'component2' + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + self.assertEquals( + ['base', 'component1', 'component2'], + list(self.comp_registry) + ) + + def test_inherit_bases(self): + class Component1(Component): + _name = 'component1' + + class Component2(Component): + _inherit = 'component1' + + class Component3(Component): + _inherit = 'component1' + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + Component3._build_component(self.comp_registry) + self.assertEquals( + (Component3, + Component2, + Component1, + self.comp_registry['base']), + self.comp_registry['component1'].__bases__ + ) + + def test_prototype_inherit_bases(self): + class Component1(Component): + _name = 'component1' + + class Component2(Component): + _name = 'component2' + _inherit = 'component1' + + class Component3(Component): + _name = 'component3' + _inherit = 'component1' + + class Component4(Component): + _name = 'component4' + _inherit = ['component2', 'component3'] + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + Component3._build_component(self.comp_registry) + Component4._build_component(self.comp_registry) + self.assertEquals( + (Component1, + self.comp_registry['base']), + self.comp_registry['component1'].__bases__ + ) + self.assertEquals( + (Component2, + self.comp_registry['component1'], + self.comp_registry['base']), + self.comp_registry['component2'].__bases__ + ) + self.assertEquals( + (Component3, + self.comp_registry['component1'], + self.comp_registry['base']), + self.comp_registry['component3'].__bases__ + ) + self.assertEquals( + (Component4, + self.comp_registry['component2'], + self.comp_registry['component3'], + self.comp_registry['base']), + self.comp_registry['component4'].__bases__ + ) + + def test_custom_build(self): + class Component1(Component): + _name = 'component1' + + @classmethod + def _complete_component_build(cls): + cls._build_done = True + + Component1._build_component(self.comp_registry) + self.assertTrue( + self.comp_registry['component1']._build_done + ) + + def test_inherit_attrs(self): + class Component1(Component): + _name = 'component1' + + msg = 'ping' + + def say(self): + return 'foo' + + class Component2(Component): + _name = 'component2' + _inherit = 'component1' + + msg = 'pong' + + def say(self): + return super(Component2, self).say() + ' bar' + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + component1 = self.comp_registry['component1'](mock.Mock()) + component2 = self.comp_registry['component2'](mock.Mock()) + self.assertEquals('ping', component1.msg) + self.assertEquals('pong', component2.msg) + self.assertEquals('foo', component1.say()) + self.assertEquals('foo bar', component2.say()) + + def test_duplicate_component(self): + class Component1(Component): + _name = 'component1' + + class Component2(Component): + _name = 'component1' + + Component1._build_component(self.comp_registry) + msg = 'Component.*already exists.*' + with self.assertRaisesRegexp(TypeError, msg): + Component2._build_component(self.comp_registry) + + def test_no_parent(self): + class Component1(Component): + _name = 'component1' + _inherit = 'component1' + + msg = 'Component.*does not exist in registry.*' + with self.assertRaisesRegexp(TypeError, msg): + Component1._build_component(self.comp_registry) + + def test_no_parent2(self): + class Component1(Component): + _name = 'component1' + + class Component2(Component): + _name = 'component2' + _inherit = ['component1', 'component3'] + + Component1._build_component(self.comp_registry) + msg = 'Component.*inherits from non-existing component.*' + with self.assertRaisesRegexp(TypeError, msg): + Component2._build_component(self.comp_registry) diff --git a/component/tests/test_component.py b/component/tests/test_component.py new file mode 100644 index 0000000000..1ae2b417bd --- /dev/null +++ b/component/tests/test_component.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo.addons.component.core import ( + Component, +) +from .common import TransactionComponentRegistryCase +from odoo.addons.component.exception import NoComponentError + + +class TestComponent(TransactionComponentRegistryCase): + + def setUp(self): + super(TestComponent, self).setUp() + self.collection = self.env['collection.base'] + + class Component1(Component): + _name = 'component1' + _collection = 'collection.base' + _usage = 'for.test' + _apply_on = ['res.partner'] + + class Component2(Component): + _name = 'component2' + _collection = 'collection.base' + _usage = 'for.test' + _apply_on = ['res.users'] + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + + self.collection_record = self.collection.new() + self.work = self.collection_record.work_on( + 'res.partner', + # we use a custom registry only + # for the sake of the tests + components_registry=self.comp_registry + ) + self.base = self.comp_registry['base'](self.work) + + def test_component_attrs(self): + comp = self.work.components(usage='for.test') + self.assertEquals(self.collection_record, comp.collection) + self.assertEquals(self.work, comp.work) + self.assertEquals(self.env, comp.env) + self.assertEquals(self.env['res.partner'], comp.model) + + def test_component_get_by_name_same_model(self): + comp = self.base.component_by_name('component1') + self.assertEquals('component1', comp._name) + self.assertEquals(self.env['res.partner'], comp.model) + + def test_component_get_by_name_other_model(self): + comp = self.base.component_by_name( + 'component2', model_name='res.users' + ) + self.assertEquals('component2', comp._name) + self.assertEquals(self.env['res.users'], comp.model) + + def test_component_get_by_name_wrong_model(self): + msg = ("Component with name 'component2' can't be used " + "for model 'res.partner'.*") + with self.assertRaisesRegexp(NoComponentError, msg): + self.base.component_by_name('component2') + + def test_component_get_by_name_not_exist(self): + msg = "No component with name 'foo' found." + with self.assertRaisesRegexp(NoComponentError, msg): + self.base.component_by_name('foo') + + def test_component_by_usage_same_model(self): + comp = self.base.components(usage='for.test') + self.assertEquals('component1', comp._name) + self.assertEquals(self.env['res.partner'], comp.model) + + def test_component_by_usage_other_model(self): + comp = self.base.components(usage='for.test', model_name='res.users') + self.assertEquals('component2', comp._name) + self.assertEquals(self.env['res.users'], comp.model) + + def test_component_by_usage_other_model_env(self): + comp = self.base.components(usage='for.test', + model_name=self.env['res.users']) + self.assertEquals('component2', comp._name) + self.assertEquals(self.env['res.users'], comp.model) diff --git a/component/tests/test_lookup.py b/component/tests/test_lookup.py new file mode 100644 index 0000000000..e46a49859e --- /dev/null +++ b/component/tests/test_lookup.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +from odoo.addons.component.core import ( + AbstractComponent, + Component, +) +from odoo.addons.component.exception import ( + NoComponentError, + SeveralComponentError, +) +from .common import ComponentRegistryCase + + +class TestLookup(ComponentRegistryCase): + + def test_lookup_collection(self): + class Foo(Component): + _name = 'foo' + _collection = 'foobar' + + class Bar(Component): + _name = 'bar' + _collection = 'foobar' + + class Homer(Component): + _name = 'homer' + _collection = 'other' + + self._build_components(Foo, Bar, Homer) + + with self.assertRaises(SeveralComponentError): + self.comp_registry.lookup('foobar') + + components = self.comp_registry.lookup('foobar', many=True) + self.assertEqual( + ['foo', 'bar'], + [c._name for c in components] + ) + + def test_lookup_usage(self): + class Foo(Component): + _name = 'foo' + _collection = 'foobar' + _usage = 'speaker' + + class Bar(Component): + _name = 'bar' + _collection = 'foobar' + _usage = 'speaker' + + class Baz(Component): + _name = 'baz' + _collection = 'foobar' + _usage = 'listener' + + self._build_components(Foo, Bar, Baz) + + component = self.comp_registry.lookup('foobar', usage='listener') + self.assertEqual('baz', component._name) + + with self.assertRaises(SeveralComponentError): + components = self.comp_registry.lookup('foobar', usage='speaker') + + components = self.comp_registry.lookup( + 'foobar', usage='speaker', many=True + ) + self.assertEqual( + ['foo', 'bar'], + [c._name for c in components] + ) + + def test_lookup_no_component(self): + with self.assertRaises(NoComponentError): + self.comp_registry.lookup('something', usage='something') + + def test_get_by_name(self): + class Foo(AbstractComponent): + _name = 'foo' + _collection = 'foobar' + + self._build_components(Foo) + self.assertEquals('foo', self.comp_registry['foo']._name) + + def test_lookup_abstract(self): + class Foo(AbstractComponent): + _name = 'foo' + _collection = 'foobar' + _usage = 'speaker' + + class Bar(Component): + _name = 'bar' + _inherit = 'foo' + + self._build_components(Foo, Bar) + + comp_registry = self.comp_registry + + component = comp_registry.lookup('foobar', usage='speaker') + self.assertEqual('bar', component._name) + + components = comp_registry.lookup('foobar', usage='speaker', many=True) + self.assertEqual( + ['bar'], + [c._name for c in components] + ) + + def test_lookup_model_name(self): + class Foo(Component): + _name = 'foo' + _collection = 'foobar' + _usage = 'speaker' + # support list + _apply_on = ['res.partner'] + + class Bar(Component): + _name = 'bar' + _collection = 'foobar' + _usage = 'speaker' + # support string + _apply_on = 'res.users' + + class Any(Component): + # can be used with any model as far as we look it up + # with its usage + _name = 'any' + _collection = 'foobar' + _usage = 'listener' + + self._build_components(Foo, Bar, Any) + + component = self.comp_registry.lookup('foobar', + usage='speaker', + model_name='res.partner') + self.assertEqual('foo', component._name) + + component = self.comp_registry.lookup('foobar', + usage='speaker', + model_name='res.users') + self.assertEqual('bar', component._name) + + component = self.comp_registry.lookup('foobar', + usage='listener', + model_name='res.users') + self.assertEqual('any', component._name) diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py new file mode 100644 index 0000000000..3203d8fe42 --- /dev/null +++ b/component/tests/test_work_on.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) + +import mock +from odoo.tests import common +from odoo.addons.component.core import ( + WorkContext, +) + + +class TestWorkOn(common.TransactionCase): + + def setUp(self): + super(TestWorkOn, self).setUp() + self.collection = self.env['collection.base'] + + def test_collection_work_on(self): + collection_record = self.collection.new() + work = collection_record.work_on('res.partner') + self.assertEquals(collection_record, work.collection) + self.assertEquals('collection.base', work.collection._name) + self.assertEquals('res.partner', work.model_name) + self.assertEquals(self.env['res.partner'], work.model) + self.assertEquals(self.env, work.env) + + def test_propagate_work_on(self): + registry = mock.Mock(name='components_registry') + work = WorkContext( + self.collection, + 'res.partner', + components_registry=registry, + test_keyword='value', + ) + self.assertEquals(registry, work.components_registry) + self.assertEquals('value', work.test_keyword) + + work2 = work.work_on('res.users') + self.assertEquals(self.env, work2.env) + self.assertEquals(self.collection, work2.collection) + self.assertEquals('res.users', work2.model_name) + self.assertEquals(registry, work2.components_registry) + self.assertEquals('value', work2.test_keyword) From f2415d444a0898a95c65d2cfc5a677c1265ec902 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 15 Jun 2017 16:06:17 +0200 Subject: [PATCH 013/122] Use 2 different methods for single/many lookup the 'components' method had 2 different return types depending of the 'multi' argument. Now we have 'component' or 'many_components' that return a Component instance or a list of Component instances. --- component/core.py | 82 +++++++++++++++++------------ component/models/collection.py | 6 +-- component/tests/test_component.py | 85 ++++++++++++++++++++++++++++--- component/tests/test_lookup.py | 54 ++++++++------------ 4 files changed, 154 insertions(+), 73 deletions(-) diff --git a/component/core.py b/component/core.py index cb107933c8..220f88462a 100644 --- a/component/core.py +++ b/component/core.py @@ -36,17 +36,14 @@ class ComponentGlobalRegistry(OrderedDict): This is an OrderedDict, because we want to keep the registration order of the components, addons loaded first - have their components found first (when we look for a list - components using `many`). + have their components found first. """ # TODO use a LRU cache (repoze.lru?) - def lookup(self, collection_name, usage=None, - model_name=None, many=False): + def lookup(self, collection_name, usage=None, model_name=None): # keep the order so addons loaded first have components used first - # in case of many=True collection_components = [ component for component in self.itervalues() if (component._collection == collection_name or @@ -68,23 +65,6 @@ def lookup(self, collection_name, usage=None, if c.apply_on_models is None or model_name in c.apply_on_models] - if not candidates: - raise NoComponentError( - "No component found for collection '%s', " - "usage '%s', model_name '%s'." % - (collection_name, usage, model_name) - ) - - if not many: - if len(candidates) > 1: - raise SeveralComponentError( - "Several components found for collection '%s', " - "usage '%s', model_name '%s'. Found: %r" % - (collection_name, usage, model_name, candidates) - ) - # TODO: always return a list here, use a 2 methods for multi/normal - return candidates.pop() - return candidates @@ -119,11 +99,16 @@ def work_on(self, model_name): def component_by_name(self, name): return self.components_registry['base'](self).component_by_name(name) - def components(self, usage=None, model_name=None, many=False): - return self.components_registry['base'](self).components( + def component(self, usage=None, model_name=None): + return self.components_registry['base'](self).component( + usage=usage, + model_name=model_name, + ) + + def many_components(self, usage=None, model_name=None): + return self.components_registry['base'](self).many_components( usage=usage, model_name=model_name, - many=many, ) def __str__(self): @@ -207,7 +192,9 @@ def component_by_name(self, name, model_name=None): if len(component_class.apply_on_models) == 1: hint_models = "'%s'" % (component_class.apply_on_models[0],) else: - hint_models = "" % (component_class.apply_on_models,) + hint_models = "" % ( + 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: " @@ -221,20 +208,51 @@ def component_by_name(self, name, model_name=None): work_context = self.work.work_on(model_name) return component_class(work_context) - def components(self, usage=None, model_name=None, many=False): - if isinstance(model_name, models.BaseModel): - model_name = model_name._name - component_class = self.work.components_registry.lookup( + def _lookup_components(self, usage=None, model_name=None): + component_classes = self.work.components_registry.lookup( self.collection._name, usage=usage, model_name=model_name or self.work.model_name, - many=many, ) + if not component_classes: + raise NoComponentError( + "No component found for collection '%s', " + "usage '%s', model_name '%s'." % + (self.collection._name, usage, model_name) + ) + + return component_classes + + def component(self, usage=None, model_name=None): + if isinstance(model_name, models.BaseModel): + model_name = model_name._name + component_classes = self._lookup_components( + usage=usage, model_name=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) + ) if model_name is None or model_name == self.work.model_name: work_context = self.work else: work_context = self.work.work_on(model_name) - return component_class(work_context) + return component_classes[0](work_context) + + def many_components(self, usage=None, model_name=None): + if isinstance(model_name, models.BaseModel): + model_name = model_name._name + component_classes = self._lookup_components( + usage=usage, model_name=model_name + ) + if model_name is None or model_name == self.work.model_name: + work_context = self.work + else: + work_context = self.work.work_on(model_name) + return [comp(work_context) for comp in component_classes] def __str__(self): return "Component(%s)" % self._name diff --git a/component/models/collection.py b/component/models/collection.py index 9ce6e91f0f..fa5a22efe8 100644 --- a/component/models/collection.py +++ b/component/models/collection.py @@ -25,8 +25,8 @@ class MagentoSaleImporter(Component): _collection = 'magento.backend' # name of the collection def run(self, magento_id): - mapper = self.components(name='magento.sale.importer.mapper') - extra_mappers = self.components( + mapper = self.component(name='magento.sale.importer.mapper') + extra_mappers = self.component( usage='magento.sale.importer.mapper', many=True, ) @@ -36,7 +36,7 @@ def run(self, magento_id): backend = self.env['magento.backend'].browse(1) work = backend.work_on('magento.sale.order') - importer = work.components(name='magento.sale.importer') + importer = work.component(name='magento.sale.importer') importer.run(1) diff --git a/component/tests/test_component.py b/component/tests/test_component.py index 1ae2b417bd..9ca1022776 100644 --- a/component/tests/test_component.py +++ b/component/tests/test_component.py @@ -6,7 +6,10 @@ Component, ) from .common import TransactionComponentRegistryCase -from odoo.addons.component.exception import NoComponentError +from odoo.addons.component.exception import ( + NoComponentError, + SeveralComponentError, +) class TestComponent(TransactionComponentRegistryCase): @@ -40,7 +43,7 @@ class Component2(Component): self.base = self.comp_registry['base'](self.work) def test_component_attrs(self): - comp = self.work.components(usage='for.test') + comp = self.work.component(usage='for.test') self.assertEquals(self.collection_record, comp.collection) self.assertEquals(self.work, comp.work) self.assertEquals(self.env, comp.env) @@ -70,17 +73,87 @@ def test_component_get_by_name_not_exist(self): self.base.component_by_name('foo') def test_component_by_usage_same_model(self): - comp = self.base.components(usage='for.test') + comp = self.base.component(usage='for.test') self.assertEquals('component1', comp._name) self.assertEquals(self.env['res.partner'], comp.model) def test_component_by_usage_other_model(self): - comp = self.base.components(usage='for.test', model_name='res.users') + comp = self.base.component(usage='for.test', model_name='res.users') self.assertEquals('component2', comp._name) self.assertEquals(self.env['res.users'], comp.model) def test_component_by_usage_other_model_env(self): - comp = self.base.components(usage='for.test', - model_name=self.env['res.users']) + comp = self.base.component(usage='for.test', + model_name=self.env['res.users']) self.assertEquals('component2', comp._name) self.assertEquals(self.env['res.users'], comp.model) + + def test_component_error_several(self): + class Component3(Component): + _name = 'component3' + _collection = 'collection.base' + _usage = 'for.test' + + Component3._build_component(self.comp_registry) + + with self.assertRaises(SeveralComponentError): + self.base.component(usage='for.test') + + def test_many_components(self): + class Component3(Component): + _name = 'component3' + _collection = 'collection.base' + _usage = 'for.test' + + Component3._build_component(self.comp_registry) + comps = self.base.many_components(usage='for.test') + self.assertEqual( + ['component1', 'component3'], + [c._name for c in comps] + ) + + def test_many_components_other_model(self): + class Component3(Component): + _name = 'component3' + _collection = 'collection.base' + _apply_on = 'res.users' + _usage = 'for.test' + + Component3._build_component(self.comp_registry) + comps = self.base.many_components(usage='for.test', + model_name='res.users') + self.assertEqual( + ['component2', 'component3'], + [c._name for c in comps] + ) + + def test_many_components_other_model_env(self): + class Component3(Component): + _name = 'component3' + _collection = 'collection.base' + _apply_on = 'res.users' + _usage = 'for.test' + + Component3._build_component(self.comp_registry) + comps = self.base.many_components(usage='for.test', + model_name=self.env['res.users']) + self.assertEqual( + ['component2', 'component3'], + [c._name for c in comps] + ) + + def test_no_component(self): + with self.assertRaises(NoComponentError): + self.base.component(usage='foo') + + def test_no_many_component(self): + with self.assertRaises(NoComponentError): + self.base.many_components(usage='foo') + + def test_work_on_component(self): + comp = self.work.component(usage='for.test') + self.assertEquals('component1', comp._name) + + def test_work_on_many_components(self): + comps = self.work.many_components(usage='for.test') + self.assertEquals('component1', comps[0]._name) diff --git a/component/tests/test_lookup.py b/component/tests/test_lookup.py index e46a49859e..0b95a1f57e 100644 --- a/component/tests/test_lookup.py +++ b/component/tests/test_lookup.py @@ -6,10 +6,6 @@ AbstractComponent, Component, ) -from odoo.addons.component.exception import ( - NoComponentError, - SeveralComponentError, -) from .common import ComponentRegistryCase @@ -30,10 +26,7 @@ class Homer(Component): self._build_components(Foo, Bar, Homer) - with self.assertRaises(SeveralComponentError): - self.comp_registry.lookup('foobar') - - components = self.comp_registry.lookup('foobar', many=True) + components = self.comp_registry.lookup('foobar') self.assertEqual( ['foo', 'bar'], [c._name for c in components] @@ -57,23 +50,20 @@ class Baz(Component): self._build_components(Foo, Bar, Baz) - component = self.comp_registry.lookup('foobar', usage='listener') - self.assertEqual('baz', component._name) - - with self.assertRaises(SeveralComponentError): - components = self.comp_registry.lookup('foobar', usage='speaker') + components = self.comp_registry.lookup('foobar', usage='listener') + self.assertEqual('baz', components[0]._name) - components = self.comp_registry.lookup( - 'foobar', usage='speaker', many=True - ) + components = self.comp_registry.lookup('foobar', usage='speaker') self.assertEqual( ['foo', 'bar'], [c._name for c in components] ) def test_lookup_no_component(self): - with self.assertRaises(NoComponentError): + self.assertEquals( + [], self.comp_registry.lookup('something', usage='something') + ) def test_get_by_name(self): class Foo(AbstractComponent): @@ -97,10 +87,10 @@ class Bar(Component): comp_registry = self.comp_registry - component = comp_registry.lookup('foobar', usage='speaker') - self.assertEqual('bar', component._name) + components = comp_registry.lookup('foobar', usage='speaker') + self.assertEqual('bar', components[0]._name) - components = comp_registry.lookup('foobar', usage='speaker', many=True) + components = comp_registry.lookup('foobar', usage='speaker') self.assertEqual( ['bar'], [c._name for c in components] @@ -130,17 +120,17 @@ class Any(Component): self._build_components(Foo, Bar, Any) - component = self.comp_registry.lookup('foobar', - usage='speaker', - model_name='res.partner') - self.assertEqual('foo', component._name) + components = self.comp_registry.lookup('foobar', + usage='speaker', + model_name='res.partner') + self.assertEqual('foo', components[0]._name) - component = self.comp_registry.lookup('foobar', - usage='speaker', - model_name='res.users') - self.assertEqual('bar', component._name) + components = self.comp_registry.lookup('foobar', + usage='speaker', + model_name='res.users') + self.assertEqual('bar', components[0]._name) - component = self.comp_registry.lookup('foobar', - usage='listener', - model_name='res.users') - self.assertEqual('any', component._name) + components = self.comp_registry.lookup('foobar', + usage='listener', + model_name='res.users') + self.assertEqual('any', components[0]._name) From 0b62ce04d28e4119a6bcf6c5992e29a0db86a973 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 15 Jun 2017 21:18:53 +0200 Subject: [PATCH 014/122] Document component --- component/base_components.py | 6 + component/builder.py | 37 ++- component/core.py | 408 +++++++++++++++++++++--- component/models/collection.py | 20 +- component/tests/common.py | 9 + component/tests/test_build_component.py | 31 ++ component/tests/test_component.py | 79 ++++- component/tests/test_lookup.py | 27 ++ component/tests/test_work_on.py | 23 +- 9 files changed, 582 insertions(+), 58 deletions(-) diff --git a/component/base_components.py b/component/base_components.py index 0c4cfefdb7..5b1c7ad1ba 100644 --- a/component/base_components.py +++ b/component/base_components.py @@ -6,4 +6,10 @@ class BaseComponent(AbstractComponent): + """ This is the base component for every component + + It is implicitely inherited by all components. + + All your base are belong to us + """ _name = 'base' diff --git a/component/builder.py b/component/builder.py index f8805707cd..3ed2d00c7e 100644 --- a/component/builder.py +++ b/component/builder.py @@ -11,15 +11,19 @@ class ComponentBuilder(models.AbstractModel): """ Build the component classes And register them in a global registry. - The classes are built using the same mechanism - than the Odoo one, meaning that a final class - that inherits from all the ``_inherit`` is created. - This class is kept in the ``all_components`` global - registry with the Components ``_name`` as keys. - This is an Odoo model so we can hook the build of the components at the end - of the registry loading with ``_register_hook``, after all modules are - loaded. + Every time an Odoo registry is built, the know components are cleared and + rebuilt as well. The Component classes are built using the same mechanism + than Odoo's Models: a final class is created, taking every Components with + a ``_name`` and applying Components with an ``_inherits`` upon them. + + The final Component classes are registered in global registry. + + This class is an Odoo model, allowing us to hook the build of the + components 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 = 'component.builder' @@ -27,9 +31,15 @@ class ComponentBuilder(models.AbstractModel): @api.model_cr 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 clear the components then rebuild them all_components.clear() # TODO: reset the LRU cache of the component lookups when implemented + # 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') @@ -46,5 +56,16 @@ def _register_hook(self): self.load_components(module.name, all_components) def load_components(self, module, registry): + """ Build every component known by MetaComponent for an odoo module + + The final component (composed by all the Component classes in this + module) will be pushed into the registry. + + :param module: the name of the addon for which we want to load + the components + :type module: str | unicode + :param registry: the registry in which we want to put the Component + :type registry: :py:class:`~component.core.ComponentGlobalRegistry` + """ for component_class in MetaComponent._modules_components[module]: component_class._build_component(registry) diff --git a/component/core.py b/component/core.py index 220f88462a..89de045b81 100644 --- a/component/core.py +++ b/component/core.py @@ -27,21 +27,38 @@ def _get_addon_name(full_name): class ComponentGlobalRegistry(OrderedDict): - """ Store all the components by name + """ Store all the components and allow to find them using criteria - Allow to _inherit components. + The key is the ``_name`` of the components. - Another registry allow to register components on a - particular collection and to find them back. - - This is an OrderedDict, because we want to keep the - registration order of the components, addons loaded first - have their components found first. + This is an OrderedDict, because we want to keep the registration order of + the components, addons loaded first have their components found first. """ # TODO use a LRU cache (repoze.lru?) def lookup(self, collection_name, usage=None, model_name=None): + """ Find and return a list of components for a usage + + The collection name is required, however, if a component is not + registered in a particular collection (no ``_collection``), it might + will be returned (as far as the ``usage`` and ``model_name`` match). + This is useful to share generic components across different + collections. + + 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. + + :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 collection_components = [ @@ -68,45 +85,143 @@ def lookup(self, collection_name, usage=None, model_name=None): return candidates +# This is where we will keep all the generated classes of the Components +# it will be cleared and updated when the odoo's registry is rebuilt all_components = ComponentGlobalRegistry() class WorkContext(object): + """ 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:: collection + + The collection we are working with. The collection is an Odoo + Model that inherit from 'collection.base'. The collection attribute + can be an record or an "empty" model. + + .. 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:: 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(collection, 'res.partner') + component = work.component(usage='record.importer') + + Usually you will use the shortcut available thanks to the + `collection.base` Model: + + :: + + collection = self.env['my.collection'].browse(1) + work = collection.work_on('res.partner') + 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) + work = collection.work_on('res.partner', hello='world') + assert work.hello == 'world' + + When you need to work on a different model, a new work instance will be + created for you when you work with the higher lever API. This is what + happens under the hood: + + :: + + collection = self.env['my.collection'].browse(1) + work = collection.work_on('res.partner', hello='world') + 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 work.model_name == 'res.users' + assert work.hello == 'world' + + """ def __init__(self, collection, model_name, components_registry=None, **kwargs): self.collection = collection self.model_name = model_name self.model = self.env[model_name] - if components_registry: - self.components_registry = components_registry + # lookup components in an alternative registry, used by the tests + if components_registry is not None: + self._components_registry = components_registry else: - self.components_registry = all_components - self._propagate_kwargs = ['components_registry'] + self._components_registry = all_components + self._propagate_kwargs = ['_components_registry'] for attr_name, value in kwargs.iteritems(): 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): + """ 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} return self.__class__(self.collection, model_name, **kwargs) - def component_by_name(self, name): - return self.components_registry['base'](self).component_by_name(name) + def component_by_name(self, name, model_name=None): + """ Return a component by its name + + Entrypoint to get a component, then you will probably use + meth:`~AbstractComponent.component_by_name` + """ + base = self._components_registry['base'](self) + return base.component_by_name(name, model_name=model_name) def component(self, usage=None, model_name=None): - return self.components_registry['base'](self).component( + """ Return a component + + Entrypoint to get a component, then you will probably use + meth:`~AbstractComponent.component` or + meth:`~AbstractComponent.many_components` + """ + return self._components_registry['base'](self).component( usage=usage, model_name=model_name, ) def many_components(self, usage=None, model_name=None): - return self.components_registry['base'](self).many_components( + """ Return several components + + Entrypoint to get a component, then you will probably use + meth:`~AbstractComponent.component` or + meth:`~AbstractComponent.many_components` + """ + return self._components_registry['base'](self).many_components( usage=usage, model_name=model_name, ) @@ -121,6 +236,12 @@ def __unicode__(self): 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) @@ -147,19 +268,172 @@ def apply_on_models(self): class AbstractComponent(object): + """ Main Component Model + + All components have a Python inheritance on this class or its companion + :class:`Component`. + + ``AbstractComponent`` will not appear in the lookups for components, + however they can be used as a base for other Components through inheritance + (using ``_inherit``). + + The inheritance mechanism is like the Odoo's one for Models. Every + component starts with a ``_name``. + + :: + + class MyComponent(Component): + _name = 'my.component' + + def speak(self, message): + print message + + Every component implicitly inherit from the `base` component. + + Then 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() + + It is all for the inheritance. The next topic is the registration / lookup + of components. + + It is handled by 3 attributes on the class: + + .. attribute: _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 reduce the odds of conflicts for the same usage/model. + A collection can be for instance ``magento.backend``. It is the name + of a model that inherits from ``collection.base``. + See also :class:`WorkContext`. + + .. attribute: _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. + + .. attribute: _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) + >>> work = coll.work_on('res.users') + >>> 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. + + """ __metaclass__ = MetaComponent _register = False _abstract = True + # used for inheritance _name = None _inherit = None # name of the collection to subscribe in _collection = None - _apply_on = None # None means any Model, can be a list ['res.users', ...] - _usage = None # component purpose ('import.mapper', ...) + # 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(AbstractComponent, self).__init__() @@ -167,24 +441,45 @@ def __init__(self, work_context): @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.collection.env @property def model(self): + """ The model instance we are working with """ return self.work.model def _component_class_by_name(self, name): - components_registry = self.work.components_registry + components_registry = self.work._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 ``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.work.model_name if (component_class.apply_on_models and @@ -209,27 +504,41 @@ def component_by_name(self, name, model_name=None): return component_class(work_context) def _lookup_components(self, usage=None, model_name=None): - component_classes = self.work.components_registry.lookup( + component_classes = self.work._components_registry.lookup( self.collection._name, usage=usage, model_name=model_name or self.work.model_name, ) - if not component_classes: - raise NoComponentError( - "No component found for collection '%s', " - "usage '%s', model_name '%s'." % - (self.collection._name, usage, model_name) - ) return component_classes def component(self, usage=None, model_name=None): + """ Find a component by usage and model for the current collection + + It searches a component using the rules of + :meth:`ComponentGlobalRegistry.lookup`. When a component is found, + it initialize it with the current :class:`WorkContext` and returned. + + A :class:`component.exception.SeveralComponentError` is raised if + more than one component match for the provided + ``usage``/``model_name``. + + A :class:`component.exception.NoComponentError` is raised if + no component is found for the provided ``usage``/``model_name``. + + """ if isinstance(model_name, models.BaseModel): model_name = model_name._name component_classes = self._lookup_components( usage=usage, model_name=model_name ) - if len(component_classes) > 1: + 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: raise SeveralComponentError( "Several components found for collection '%s', " "usage '%s', model_name '%s'. Found: %r" % @@ -243,6 +552,16 @@ def component(self, usage=None, model_name=None): return component_classes[0](work_context) def many_components(self, usage=None, model_name=None): + """ Find many components by usage and model for the current collection + + It searches a component using the rules of + :meth:`ComponentGlobalRegistry.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. + + """ if isinstance(model_name, models.BaseModel): model_name = model_name._name component_classes = self._lookup_components( @@ -262,19 +581,26 @@ def __unicode__(self): __repr__ = __str__ - # - # Goal: try to apply inheritance at the instantiation level and - # put objects in the registry var - # @classmethod def _build_component(cls, registry): - """ Instantiate a given Component in the registry. + """ Instantiate a given Component in the components registry. - This method creates or extends a "registry" class for the given - component. - This "registry" class carries inferred component metadata, and inherits - (in the Python sense) from all classes that define the component, and - possibly other registry classes. + 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:`ComponentGlobalRegistry`), 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. @@ -390,5 +716,13 @@ def _complete_component_build(cls): 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 diff --git a/component/models/collection.py b/component/models/collection.py index fa5a22efe8..ce1cb6755b 100644 --- a/component/models/collection.py +++ b/component/models/collection.py @@ -25,20 +25,21 @@ class MagentoSaleImporter(Component): _collection = 'magento.backend' # name of the collection def run(self, magento_id): - mapper = self.component(name='magento.sale.importer.mapper') - extra_mappers = self.component( - usage='magento.sale.importer.mapper', - many=True, + mapper = self.component(usage='import.mapper') + extra_mappers = self.many_components( + usage='import.mapper.extra', ) # ... - # use it: + Use it:: backend = self.env['magento.backend'].browse(1) work = backend.work_on('magento.sale.order') - importer = work.component(name='magento.sale.importer') + importer = work.component(usage='magento.sale.importer') importer.run(1) + See also: :class:`~component.core.WorkContext` + """ _name = 'collection.base' @@ -46,5 +47,12 @@ def run(self, magento_id): @api.multi def work_on(self, model_name, **kwargs): + """ Entry-point for the components + + Start a work using the components on the model. + Any keyword argument will be assigned to the work context. + See documentation of :class:`component.core.WorkContext`. + + """ self.ensure_one() return WorkContext(self, model_name, **kwargs) diff --git a/component/tests/common.py b/component/tests/common.py index d25a3faf95..97f50b9c53 100644 --- a/component/tests/common.py +++ b/component/tests/common.py @@ -15,9 +15,13 @@ class ComponentRegistryCase(unittest2.TestCase): def setUp(self): super(ComponentRegistryCase, self).setUp() + + # keep the original classes registered by the metaclass + # so we'll restore them at the end of the tests self._original_components = MetaComponent._modules_components.copy() MetaComponent._modules_components.clear() + # it will be our temporary component registry for our test session self.comp_registry = ComponentGlobalRegistry() # there's always an implicit dependency on a 'base' component @@ -25,10 +29,13 @@ def setUp(self): class Base(AbstractComponent): _name = 'base' + # it builds the 'final component' and push it in the component + # registry Base._build_component(self.comp_registry) def tearDown(self): super(ComponentRegistryCase, self).tearDown() + # restore the original metaclass' classes MetaComponent._modules_components = self._original_components def _build_components(self, *classes): @@ -40,6 +47,8 @@ class TransactionComponentRegistryCase(common.TransactionCase, ComponentRegistryCase): def setUp(self): + # resolve an inheritance issue (common.TransactionCase does not use + # super) common.TransactionCase.setUp(self) ComponentRegistryCase.setUp(self) self.collection = self.env['collection.base'] diff --git a/component/tests/test_build_component.py b/component/tests/test_build_component.py index e41254bae3..b551ab02f7 100644 --- a/component/tests/test_build_component.py +++ b/component/tests/test_build_component.py @@ -8,8 +8,23 @@ class TestBuildComponent(ComponentRegistryCase): + """ Test build of components + + All the tests in this suite are based on the same principle with + variations: + + * Create new Components (classes inheriting from + :class:`component.core.Component` or + :class:`component.core.AbstractComponent` + * Call :meth:`component.core.Component._build_component` on them + in order to build the 'final class' composed from all the ``_inherit`` + and push it in the components registry (``self.comp_registry`` here) + * Assert that classes are built, registered, have correct ``__bases__``... + + """ def test_no_name(self): + """ Ensure that a component has a _name """ class Component1(Component): pass @@ -18,12 +33,15 @@ class Component1(Component): Component1._build_component(self.comp_registry) def test_register(self): + """ Able to register components in components registry """ class Component1(Component): _name = 'component1' class Component2(Component): _name = 'component2' + # build the 'final classes' for the components and check that we find + # them in the components registry Component1._build_component(self.comp_registry) Component2._build_component(self.comp_registry) self.assertEquals( @@ -32,6 +50,7 @@ class Component2(Component): ) def test_inherit_bases(self): + """ Check __bases__ of Component with _inherit """ class Component1(Component): _name = 'component1' @@ -53,6 +72,7 @@ class Component3(Component): ) def test_prototype_inherit_bases(self): + """ Check __bases__ of Component with _inherit and different _name """ class Component1(Component): _name = 'component1' @@ -98,19 +118,24 @@ class Component4(Component): ) def test_custom_build(self): + """ Check that we can hook at the end of a Component build """ class Component1(Component): _name = 'component1' @classmethod def _complete_component_build(cls): + # This method should be called after the Component + # is built, and before it is pushed in the registry cls._build_done = True Component1._build_component(self.comp_registry) + # we inspect that our custom build has been executed self.assertTrue( self.comp_registry['component1']._build_done ) def test_inherit_attrs(self): + """ Check attributes inheritance of Components with _inherit """ class Component1(Component): _name = 'component1' @@ -130,6 +155,9 @@ def say(self): Component1._build_component(self.comp_registry) Component2._build_component(self.comp_registry) + # we initialize the components, normally we should pass + # an instance of WorkContext, but we don't need a real one + # for this test component1 = self.comp_registry['component1'](mock.Mock()) component2 = self.comp_registry['component2'](mock.Mock()) self.assertEquals('ping', component1.msg) @@ -138,6 +166,7 @@ def say(self): self.assertEquals('foo bar', component2.say()) def test_duplicate_component(self): + """ Check that we can't have 2 components with the same name """ class Component1(Component): _name = 'component1' @@ -150,6 +179,7 @@ class Component2(Component): Component2._build_component(self.comp_registry) def test_no_parent(self): + """ Ensure we can't _inherit a non-existent component """ class Component1(Component): _name = 'component1' _inherit = 'component1' @@ -159,6 +189,7 @@ class Component1(Component): Component1._build_component(self.comp_registry) def test_no_parent2(self): + """ Ensure we can't _inherit by prototype a non-existent component """ class Component1(Component): _name = 'component1' diff --git a/component/tests/test_component.py b/component/tests/test_component.py index 9ca1022776..6f6dda67cd 100644 --- a/component/tests/test_component.py +++ b/component/tests/test_component.py @@ -13,11 +13,21 @@ class TestComponent(TransactionComponentRegistryCase): + """ Test usage of components + + These tests are a bit more broad that mere unit tests. + We test the chain odoo Model -> generate a WorkContext instance -> Work + with Component. + + Tests are inside Odoo transactions, so we can work + with Odoo's env / models. + """ def setUp(self): super(TestComponent, self).setUp() self.collection = self.env['collection.base'] + # create some Component to play with class Component1(Component): _name = 'component1' _collection = 'collection.base' @@ -30,65 +40,114 @@ class Component2(Component): _usage = 'for.test' _apply_on = ['res.users'] + # build the components and register them in our + # test component registry Component1._build_component(self.comp_registry) Component2._build_component(self.comp_registry) + # our collection, in a less abstract use case, it + # could be a record of 'magento.backend' for instance self.collection_record = self.collection.new() + # Our WorkContext, it will be passed along in every + # components so we can share data transversally. + # We are working with res.partner in the following tests, + # unless we change it in the test. self.work = self.collection_record.work_on( 'res.partner', # we use a custom registry only # for the sake of the tests components_registry=self.comp_registry ) - self.base = self.comp_registry['base'](self.work) + # We get the 'base' component, handy to test the base + # methods component, many_components, ... + self.base = self.work.component_by_name('base') def test_component_attrs(self): + """ Basic access to a Component's attribute """ + # as we are working on res.partner, we should get 'component1' comp = self.work.component(usage='for.test') + # but this is not what we test here, we test the attributes: self.assertEquals(self.collection_record, comp.collection) self.assertEquals(self.work, comp.work) self.assertEquals(self.env, comp.env) self.assertEquals(self.env['res.partner'], comp.model) def test_component_get_by_name_same_model(self): + """ Use component_by_name with current working model """ + # we ask a component directly by it's name, considering + # we work with res.partner, we should get 'component1' + # this is ok because it's _apply_on contains res.partner comp = self.base.component_by_name('component1') self.assertEquals('component1', comp._name) self.assertEquals(self.env['res.partner'], comp.model) def test_component_get_by_name_other_model(self): + """ Use component_by_name with another model """ + # we ask a component directly by it's name, but we + # want to work with 'res.users', this is ok since + # component2's _apply_on contains res.users comp = self.base.component_by_name( 'component2', model_name='res.users' ) self.assertEquals('component2', comp._name) self.assertEquals(self.env['res.users'], comp.model) + # what happens under the hood, is that a new WorkContext + # has been created for this model, with all the other values + # identical to the previous WorkContext (the one for res.partner) + # We can check that with: + self.assertNotEquals(self.work, comp.work) + self.assertEquals('res.partner', self.work.model_name) + self.assertEquals('res.users', comp.work.model_name) def test_component_get_by_name_wrong_model(self): + """ Use component_by_name with a model not in _apply_on """ msg = ("Component with name 'component2' can't be used " "for model 'res.partner'.*") with self.assertRaisesRegexp(NoComponentError, msg): + # we ask for the model 'component2' but we are working + # with res.partner, and it only accepts res.users self.base.component_by_name('component2') def test_component_get_by_name_not_exist(self): + """ Use component_by_name on a component that do not exist """ msg = "No component with name 'foo' found." with self.assertRaisesRegexp(NoComponentError, msg): self.base.component_by_name('foo') def test_component_by_usage_same_model(self): + """ Use component(usage=...) on the same model """ + # we ask for a component having _usage == 'for.test', and + # model being res.partner (the model in the current WorkContext) comp = self.base.component(usage='for.test') self.assertEquals('component1', comp._name) self.assertEquals(self.env['res.partner'], comp.model) def test_component_by_usage_other_model(self): + """ Use component(usage=...) on a different model (name) """ + # we ask for a component having _usage == 'for.test', and + # a different model (res.users) comp = self.base.component(usage='for.test', model_name='res.users') self.assertEquals('component2', comp._name) self.assertEquals(self.env['res.users'], comp.model) + # what happens under the hood, is that a new WorkContext + # has been created for this model, with all the other values + # identical to the previous WorkContext (the one for res.partner) + # We can check that with: + self.assertNotEquals(self.work, comp.work) + self.assertEquals('res.partner', self.work.model_name) + self.assertEquals('res.users', comp.work.model_name) def test_component_by_usage_other_model_env(self): + """ Use component(usage=...) on a different model (instance) """ comp = self.base.component(usage='for.test', model_name=self.env['res.users']) self.assertEquals('component2', comp._name) self.assertEquals(self.env['res.users'], comp.model) def test_component_error_several(self): + """ Use component(usage=...) when more than one component match """ + # we create a new Component with _usage 'for.test', in the same + # collection and no _apply_on class Component3(Component): _name = 'component3' _collection = 'collection.base' @@ -97,9 +156,14 @@ class Component3(Component): Component3._build_component(self.comp_registry) with self.assertRaises(SeveralComponentError): + # When a component has no _apply_on, it means it can be applied + # on *any* model. Here, the candidates components would be: + # component1 (because we are working with res.partner), + # component3 (because it has no _apply_on so apply in any case) self.base.component(usage='for.test') def test_many_components(self): + """ Use many_components(usage=...) on the same model """ class Component3(Component): _name = 'component3' _collection = 'collection.base' @@ -107,12 +171,15 @@ class Component3(Component): Component3._build_component(self.comp_registry) comps = self.base.many_components(usage='for.test') + # When a component has no _apply_on, it means it can be applied + # on *any* model. So here, both component1 and component3 match self.assertEqual( ['component1', 'component3'], [c._name for c in comps] ) def test_many_components_other_model(self): + """ Use many_components(usage=...) on a different model (name) """ class Component3(Component): _name = 'component3' _collection = 'collection.base' @@ -128,6 +195,7 @@ class Component3(Component): ) def test_many_components_other_model_env(self): + """ Use many_components(usage=...) on a different model (instance) """ class Component3(Component): _name = 'component3' _collection = 'collection.base' @@ -143,17 +211,22 @@ class Component3(Component): ) def test_no_component(self): + """ No component found for asked usage """ with self.assertRaises(NoComponentError): self.base.component(usage='foo') def test_no_many_component(self): - with self.assertRaises(NoComponentError): - self.base.many_components(usage='foo') + """ No component found for asked usage for many_components() """ + self.assertEquals([], self.base.many_components(usage='foo')) def test_work_on_component(self): + """ Check WorkContext.component() (shortcut to Component.component) """ comp = self.work.component(usage='for.test') self.assertEquals('component1', comp._name) def test_work_on_many_components(self): + """ Check WorkContext.many_components() + + (shortcut to Component.many_components) """ comps = self.work.many_components(usage='for.test') self.assertEquals('component1', comps[0]._name) diff --git a/component/tests/test_lookup.py b/component/tests/test_lookup.py index 0b95a1f57e..24834ac666 100644 --- a/component/tests/test_lookup.py +++ b/component/tests/test_lookup.py @@ -10,8 +10,24 @@ class TestLookup(ComponentRegistryCase): + """ Test the ComponentGlobalRegistry + + Tests in this testsuite mainly do: + + * Create new Components (classes inheriting from + :class:`component.core.Component` or + :class:`component.core.AbstractComponent` + * Call :meth:`component.core.Component._build_component` on them + in order to build the 'final class' composed from all the ``_inherit`` + and push it in the components registry (``self.comp_registry`` here) + * Use the lookup method of the components registry and check + that we get the correct result + + """ def test_lookup_collection(self): + """ Lookup components of a collection """ + # we register 2 components in foobar and one in other class Foo(Component): _name = 'foo' _collection = 'foobar' @@ -26,6 +42,7 @@ class Homer(Component): self._build_components(Foo, Bar, Homer) + # we should no see the component in 'other' components = self.comp_registry.lookup('foobar') self.assertEqual( ['foo', 'bar'], @@ -33,6 +50,7 @@ class Homer(Component): ) def test_lookup_usage(self): + """ Lookup components by usage """ class Foo(Component): _name = 'foo' _collection = 'foobar' @@ -60,20 +78,26 @@ class Baz(Component): ) def test_lookup_no_component(self): + """ No component """ + # we just expect an empty list when no component match, the error + # handling is handled at an higher level self.assertEquals( [], self.comp_registry.lookup('something', usage='something') ) def test_get_by_name(self): + """ Get component by name """ class Foo(AbstractComponent): _name = 'foo' _collection = 'foobar' self._build_components(Foo) + # this is just a dict access self.assertEquals('foo', self.comp_registry['foo']._name) def test_lookup_abstract(self): + """ Do not include abstract components in lookup """ class Foo(AbstractComponent): _name = 'foo' _collection = 'foobar' @@ -87,6 +111,8 @@ class Bar(Component): comp_registry = self.comp_registry + # we should never have 'foo' in the returned components + # as it is abstract components = comp_registry.lookup('foobar', usage='speaker') self.assertEqual('bar', components[0]._name) @@ -97,6 +123,7 @@ class Bar(Component): ) def test_lookup_model_name(self): + """ Lookup with model names """ class Foo(Component): _name = 'foo' _collection = 'foobar' diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py index 3203d8fe42..d78ae36b15 100644 --- a/component/tests/test_work_on.py +++ b/component/tests/test_work_on.py @@ -2,20 +2,27 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -import mock from odoo.tests import common from odoo.addons.component.core import ( WorkContext, + ComponentGlobalRegistry, ) class TestWorkOn(common.TransactionCase): + """ Test on WorkContext + + This model is mostly a container, so we check the access + to the attributes and properties. + + """ def setUp(self): super(TestWorkOn, self).setUp() self.collection = self.env['collection.base'] def test_collection_work_on(self): + """ Create a new instance and test attributes access """ collection_record = self.collection.new() work = collection_record.work_on('res.partner') self.assertEquals(collection_record, work.collection) @@ -25,19 +32,27 @@ def test_collection_work_on(self): self.assertEquals(self.env, work.env) def test_propagate_work_on(self): - registry = mock.Mock(name='components_registry') + """ Check custom attributes and their propagation """ + registry = ComponentGlobalRegistry() work = WorkContext( self.collection, 'res.partner', + # we can customize the lookup registry, but used mostly for tests components_registry=registry, + # we can pass our own keyword args that will set as attributes test_keyword='value', ) - self.assertEquals(registry, work.components_registry) + self.assertIs(registry, work._components_registry) + # check that our custom keyword is set as attribute self.assertEquals('value', work.test_keyword) + # when we want to work on another model, work_on() create + # another instance and propagate the attributes to it work2 = work.work_on('res.users') + self.assertNotEquals(work, work2) self.assertEquals(self.env, work2.env) self.assertEquals(self.collection, work2.collection) self.assertEquals('res.users', work2.model_name) - self.assertEquals(registry, work2.components_registry) + self.assertIs(registry, work2._components_registry) + # test_keyword has been propagated to the new WorkContext instance self.assertEquals('value', work2.test_keyword) From 18b4327442427ec781fe5b8ca05f67acad5ab73a Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 16 Jun 2017 12:29:17 +0200 Subject: [PATCH 015/122] Fix error messages --- component/core.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/component/core.py b/component/core.py index 89de045b81..66656f50bd 100644 --- a/component/core.py +++ b/component/core.py @@ -507,7 +507,7 @@ def _lookup_components(self, usage=None, model_name=None): component_classes = self.work._components_registry.lookup( self.collection._name, usage=usage, - model_name=model_name or self.work.model_name, + model_name=model_name, ) return component_classes @@ -529,6 +529,7 @@ def component(self, usage=None, model_name=None): """ if isinstance(model_name, models.BaseModel): model_name = model_name._name + model_name = model_name or self.work.model_name component_classes = self._lookup_components( usage=usage, model_name=model_name ) @@ -545,7 +546,7 @@ def component(self, usage=None, model_name=None): (self.collection._name, usage or '', model_name or '', component_classes) ) - if model_name is None or model_name == self.work.model_name: + if model_name == self.work.model_name: work_context = self.work else: work_context = self.work.work_on(model_name) @@ -564,10 +565,11 @@ def many_components(self, usage=None, model_name=None): """ if isinstance(model_name, models.BaseModel): model_name = model_name._name + model_name = model_name or self.work.model_name component_classes = self._lookup_components( usage=usage, model_name=model_name ) - if model_name is None or model_name == self.work.model_name: + if model_name == self.work.model_name: work_context = self.work else: work_context = self.work.work_on(model_name) From d3cfdf13796fc36c06c3716fd19e1ce9be4680fa Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Sat, 17 Jun 2017 11:35:28 +0200 Subject: [PATCH 016/122] Update documentation --- component/builder.py | 11 +- component/core.py | 239 ++++++++++++++++++--------------- component/models/collection.py | 24 +++- 3 files changed, 158 insertions(+), 116 deletions(-) diff --git a/component/builder.py b/component/builder.py index 3ed2d00c7e..fd377758f9 100644 --- a/component/builder.py +++ b/component/builder.py @@ -2,6 +2,15 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +""" + +Components Builder +================== + +Build the components at the build of a registry. + +""" + import odoo from odoo import api, models from .core import MetaComponent, all_components @@ -65,7 +74,7 @@ def load_components(self, module, registry): the components :type module: str | unicode :param registry: the registry in which we want to put the Component - :type registry: :py:class:`~component.core.ComponentGlobalRegistry` + :type registry: :py:class:`~.core.ComponentGlobalRegistry` """ for component_class in MetaComponent._modules_components[module]: component_class._build_component(registry) diff --git a/component/core.py b/component/core.py index 66656f50bd..2e9009c497 100644 --- a/component/core.py +++ b/component/core.py @@ -3,6 +3,20 @@ # Copyright 2017 Odoo # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +""" + +Core +==== + +Core classes for the components. +The most common classes used publicly are: + +* :class:`Component` +* :class:`AbstractComponent` +* :class:`WorkContext` + +""" + from collections import defaultdict, OrderedDict from odoo import models @@ -53,6 +67,11 @@ def lookup(self, collection_name, usage=None, model_name=None): 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 @@ -104,7 +123,7 @@ class WorkContext(object): The collection we are working with. The collection is an Odoo Model that inherit from 'collection.base'. The collection attribute - can be an record or an "empty" model. + can be a record or an "empty" model. .. attribute:: model_name @@ -145,7 +164,7 @@ class WorkContext(object): assert work.hello == 'world' When you need to work on a different model, a new work instance will be - created for you when you work with the higher lever API. This is what + created for you when you are using the high-level API. This is what happens under the hood: :: @@ -156,8 +175,8 @@ class WorkContext(object): assert work.hello == 'world' work2 = work.work_on('res.users') # => spawn a new WorkContext with a copy of the attributes - assert work.model_name == 'res.users' - assert work.hello == 'world' + assert work2.model_name == 'res.users' + assert work2.hello == 'world' """ @@ -270,142 +289,143 @@ def apply_on_models(self): class AbstractComponent(object): """ Main Component Model - All components have a Python inheritance on this class or its companion - :class:`Component`. + All components have a Python inheritance either on + :class:`AbstractComponent` or either on :class:`Component`. - ``AbstractComponent`` will not appear in the lookups for components, - however they can be used as a base for other Components through inheritance - (using ``_inherit``). + 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``). - The inheritance mechanism is like the Odoo's one for Models. Every - component starts with a ``_name``. + 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 + class MyComponent(Component): + _name = 'my.component' - Every component implicitly inherit from the `base` component. + def speak(self, message): + print message - Then 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. - - :: + Every component implicitly inherit from the `'base'` component. - class MyComponent(Component): # name of the class does not matter - _inherit = 'my.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. - 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' + class MyComponent(Component): # name of the class does not matter + _inherit = 'my.component' - def sing(self, message): - print message.upper() + def yell(self, message): + print message.upper() - It is all for the inheritance. The next topic is the registration / lookup - of components. + 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. - It is handled by 3 attributes on the class: + :: - .. attribute: _collection + class AnotherComponent(Component): + _name = 'another.component' + _inherit = 'my.component' - 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 reduce the odds of conflicts for the same usage/model. - A collection can be for instance ``magento.backend``. It is the name - of a model that inherits from ``collection.base``. - See also :class:`WorkContext`. + def sing(self, message): + print message.upper() - .. attribute: _apply_on + Registration and lookups + It is handled by 3 attributes on the class: - 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. + _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`. - .. attribute: _usage + _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. - 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. + _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: + 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) + * :meth:`component` + * :meth:`many_components` + * :meth:`component_by_name` (more rarely though) - Declaration of some Components can look like:: + Declaration of some Components can look like:: - class FooBar(models.Model): - _name = 'foo.bar.collection' - _inherit = 'collection.base' # this inherit is required + 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 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' + 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 + 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' + 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() + '!!!' + def utter(self, message): + print message.upper() + '!!!' - class Vocalizer(Component): - _name = 'vocalizer' - _inherit = 'foo.bar.base' - _usage = 'vocalizer' - # can be used for any model + 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) + def vocalize(action, message): + self.component(usage=action).utter(message) - And their usage:: + And their usage:: - >>> coll = self.env['foo.bar.collection'].browse(1) - >>> work = coll.work_on('res.users') - >>> vocalizer = work.component(usage='vocalizer') - >>> vocalizer.vocalize('speak', 'hello world') - hello world - >>> vocalizer.vocalize('yell', 'hello world') - HELLO WORLD!!! + >>> coll = self.env['foo.bar.collection'].browse(1) + >>> work = coll.work_on('res.users') + >>> vocalizer = work.component(usage='vocalizer') + >>> vocalizer.vocalize('speak', 'hello world') + hello world + >>> vocalizer.vocalize('yell', 'hello world') + HELLO WORLD!!! Hints: @@ -467,7 +487,8 @@ def component_by_name(self, name, model_name=None): If the component exists, an instance of it will be returned, initialized with the current :class:`WorkContext`. - A ``NoComponentError`` is raised if: + A :class:`odoo.addons.component.exception.NoComponentError` is raised + if: * no component with this name exists * the ``_apply_on`` of the found component does not match @@ -519,12 +540,12 @@ def component(self, usage=None, model_name=None): :meth:`ComponentGlobalRegistry.lookup`. When a component is found, it initialize it with the current :class:`WorkContext` and returned. - A :class:`component.exception.SeveralComponentError` is raised if - more than one component match for the provided + A :class:`odoo.addons.component.exception.SeveralComponentError` is + raised if more than one component match for the provided ``usage``/``model_name``. - A :class:`component.exception.NoComponentError` is raised if - no component is found for the provided ``usage``/``model_name``. + A :class:`odoo.addons.component.exception.NoComponentError` is raised + if no component is found for the provided ``usage``/``model_name``. """ if isinstance(model_name, models.BaseModel): diff --git a/component/models/collection.py b/component/models/collection.py index ce1cb6755b..e6db82800c 100644 --- a/component/models/collection.py +++ b/component/models/collection.py @@ -2,6 +2,18 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +""" + +Collection Model +================ + +This is the base Model shared by all the Collections. +In the context of the Connector, a collection is the Backend. +The `_name` given to the Collection Model will be the name +to use in the `_collection` of the Components usable for the Backend. + +""" + from odoo import models, api from ..core import WorkContext @@ -33,12 +45,12 @@ def run(self, magento_id): Use it:: - backend = self.env['magento.backend'].browse(1) - work = backend.work_on('magento.sale.order') - importer = work.component(usage='magento.sale.importer') - importer.run(1) + >>> backend = self.env['magento.backend'].browse(1) + >>> work = backend.work_on('magento.sale.order') + >>> importer = work.component(usage='magento.sale.importer') + >>> importer.run(1) - See also: :class:`~component.core.WorkContext` + See also: :class:`odoo.addons.component.core.WorkContext` """ @@ -51,7 +63,7 @@ def work_on(self, model_name, **kwargs): Start a work using the components on the model. Any keyword argument will be assigned to the work context. - See documentation of :class:`component.core.WorkContext`. + See documentation of :class:`odoo.addons.component.core.WorkContext`. """ self.ensure_one() From e31d832592f54436a43020a869fa42fe2dd9ab87 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Sat, 17 Jun 2017 22:20:49 +0200 Subject: [PATCH 017/122] Draft new component_event addon Proposing a new API based on components for the events --- component/__manifest__.py | 2 +- component/core.py | 77 ++++++++++++++++++--------------- component/models/collection.py | 2 +- component/tests/test_work_on.py | 4 +- 4 files changed, 46 insertions(+), 39 deletions(-) diff --git a/component/__manifest__.py b/component/__manifest__.py index a5faac7fca..656e49c86c 100644 --- a/component/__manifest__.py +++ b/component/__manifest__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2013-2017 Camptocamp SA +# Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) {'name': 'Components', diff --git a/component/core.py b/component/core.py index 2e9009c497..f3470397c0 100644 --- a/component/core.py +++ b/component/core.py @@ -28,7 +28,7 @@ # unfortunately can't use because it's an instance method and should have been # a @staticmethod def _get_addon_name(full_name): - # The (OpenERP) module name can be in the ``odoo.addons`` namespace + # 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). @@ -40,6 +40,7 @@ def _get_addon_name(full_name): return addon_name +# FIXME: have a different registry per database class ComponentGlobalRegistry(OrderedDict): """ Store all the components and allow to find them using criteria @@ -51,14 +52,16 @@ class ComponentGlobalRegistry(OrderedDict): """ # TODO use a LRU cache (repoze.lru?) - def lookup(self, collection_name, usage=None, model_name=None): + def lookup(self, collection_name=None, usage=None, model_name=None): """ Find and return a list of components for a usage - The collection name is required, however, if a component is not - registered in a particular collection (no ``_collection``), it might - will be returned (as far as the ``usage`` and ``model_name`` match). - This is useful to share generic components across different - collections. + If a component is not registered in a particular collection (no + ``_collection``), it might 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 @@ -80,28 +83,28 @@ def lookup(self, collection_name, usage=None, model_name=None): """ # keep the order so addons loaded first have components used first - collection_components = [ + candidates = ( component for component in self.itervalues() - if (component._collection == collection_name or - component._collection is None) and - not component._abstract - ] - candidates = [] + 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: - components = [component for component in collection_components - if component._usage == usage] - if components: - candidates = components - else: - candidates = collection_components + candidates = (component for component in candidates + if component._usage == usage) - # filter out by model name - candidates = [c for c in candidates - if c.apply_on_models is None or - model_name in c.apply_on_models] + 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 candidates + return list(candidates) # This is where we will keep all the generated classes of the Components @@ -119,12 +122,6 @@ class WorkContext(object): Including: - .. 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_name Name of the model we are working with. It means that any lookup for a @@ -132,6 +129,12 @@ class WorkContext(object): 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 @@ -142,7 +145,7 @@ class WorkContext(object): :: collection = self.env['my.collection'].browse(1) - work = WorkContext(collection, 'res.partner') + work = WorkContext(model_name='res.partner', collection=collection) component = work.component(usage='record.importer') Usually you will use the shortcut available thanks to the @@ -180,7 +183,7 @@ class WorkContext(object): """ - def __init__(self, collection, model_name, + def __init__(self, model_name=None, collection=None, components_registry=None, **kwargs): self.collection = collection self.model_name = model_name @@ -190,7 +193,10 @@ def __init__(self, collection, model_name, self._components_registry = components_registry else: self._components_registry = all_components - self._propagate_kwargs = ['_components_registry'] + self._propagate_kwargs = [ + 'collection', + '_components_registry', + ] for attr_name, value in kwargs.iteritems(): setattr(self, attr_name, value) self._propagate_kwargs.append(attr_name) @@ -210,7 +216,8 @@ def work_on(self, model_name): """ kwargs = {attr_name: getattr(self, attr_name) for attr_name in self._propagate_kwargs} - return self.__class__(self.collection, model_name, **kwargs) + return self.__class__(model_name=model_name, + **kwargs) def component_by_name(self, name, model_name=None): """ Return a component by its name @@ -246,7 +253,7 @@ def many_components(self, usage=None, model_name=None): ) def __str__(self): - return "WorkContext(%s,%s)" % (repr(self.collection), self.model_name) + return "WorkContext(%s,%s)" % (self.model_name, repr(self.collection)) def __unicode__(self): return unicode(str(self)) diff --git a/component/models/collection.py b/component/models/collection.py index e6db82800c..30699954a8 100644 --- a/component/models/collection.py +++ b/component/models/collection.py @@ -67,4 +67,4 @@ def work_on(self, model_name, **kwargs): """ self.ensure_one() - return WorkContext(self, model_name, **kwargs) + return WorkContext(model_name=model_name, collection=self, **kwargs) diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py index d78ae36b15..dff7b2ce88 100644 --- a/component/tests/test_work_on.py +++ b/component/tests/test_work_on.py @@ -35,8 +35,8 @@ def test_propagate_work_on(self): """ Check custom attributes and their propagation """ registry = ComponentGlobalRegistry() work = WorkContext( - self.collection, - 'res.partner', + model_name='res.partner', + collection=self.collection, # we can customize the lookup registry, but used mostly for tests components_registry=registry, # we can pass our own keyword args that will set as attributes From cc00a329e5e8ca973b8591fe1eb2b8a535e41433 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Sat, 17 Jun 2017 23:10:13 +0200 Subject: [PATCH 018/122] Hold a component registry per database Because they can have different addons --- component/builder.py | 13 +++++----- component/core.py | 46 ++++++++++++++++++++++----------- component/tests/common.py | 4 +-- component/tests/test_lookup.py | 2 +- component/tests/test_work_on.py | 11 +++----- 5 files changed, 45 insertions(+), 31 deletions(-) diff --git a/component/builder.py b/component/builder.py index fd377758f9..25e7a33485 100644 --- a/component/builder.py +++ b/component/builder.py @@ -13,7 +13,7 @@ import odoo from odoo import api, models -from .core import MetaComponent, all_components +from .core import MetaComponent, _component_databases class ComponentBuilder(models.AbstractModel): @@ -43,7 +43,7 @@ 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 clear the components then rebuild them - all_components.clear() + _component_databases.clear(self.env.cr.dbname) # TODO: reset the LRU cache of the component lookups when implemented # lookup all the installed (or about to be) addons and generate @@ -61,10 +61,11 @@ def _register_hook(self): if name not in graph] graph.add_modules(self.env.cr, module_list) + components_registry = _component_databases[self.env.cr.dbname] for module in graph: - self.load_components(module.name, all_components) + self.load_components(module.name, components_registry) - def load_components(self, module, registry): + def load_components(self, module, components_registry): """ Build every component known by MetaComponent for an odoo module The final component (composed by all the Component classes in this @@ -74,7 +75,7 @@ def load_components(self, module, registry): the components :type module: str | unicode :param registry: the registry in which we want to put the Component - :type registry: :py:class:`~.core.ComponentGlobalRegistry` + :type registry: :py:class:`~.core.ComponentRegistry` """ for component_class in MetaComponent._modules_components[module]: - component_class._build_component(registry) + component_class._build_component(components_registry) diff --git a/component/core.py b/component/core.py index f3470397c0..6fa9da3908 100644 --- a/component/core.py +++ b/component/core.py @@ -40,8 +40,23 @@ def _get_addon_name(full_name): return addon_name -# FIXME: have a different registry per database -class ComponentGlobalRegistry(OrderedDict): +class ComponentDatabases(object): + """ Holds a registry of components for each database """ + + def __init__(self): + self._databases = {} + + def get(self, key, default=None): + return self._databases.get(key, default=default) + + def clear(self, dbname): + self._databases[dbname] = ComponentRegistry() + + def __getitem__(self, key): + return self._databases[key] + + +class ComponentRegistry(OrderedDict): """ Store all the components and allow to find them using criteria The key is the ``_name`` of the components. @@ -107,9 +122,9 @@ def lookup(self, collection_name=None, usage=None, model_name=None): return list(candidates) -# This is where we will keep all the generated classes of the Components +# We will store a ComponentRegistry per database here, # it will be cleared and updated when the odoo's registry is rebuilt -all_components = ComponentGlobalRegistry() +_component_databases = ComponentDatabases() class WorkContext(object): @@ -190,12 +205,13 @@ def __init__(self, model_name=None, collection=None, 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 + self.components_registry = components_registry else: - self._components_registry = all_components + dbname = self.env.cr.dbname + self.components_registry = _component_databases[dbname] self._propagate_kwargs = [ 'collection', - '_components_registry', + 'components_registry', ] for attr_name, value in kwargs.iteritems(): setattr(self, attr_name, value) @@ -225,7 +241,7 @@ def component_by_name(self, name, model_name=None): Entrypoint to get a component, then you will probably use meth:`~AbstractComponent.component_by_name` """ - base = self._components_registry['base'](self) + base = self.components_registry['base'](self) return base.component_by_name(name, model_name=model_name) def component(self, usage=None, model_name=None): @@ -235,7 +251,7 @@ def component(self, usage=None, model_name=None): meth:`~AbstractComponent.component` or meth:`~AbstractComponent.many_components` """ - return self._components_registry['base'](self).component( + return self.components_registry['base'](self).component( usage=usage, model_name=model_name, ) @@ -247,7 +263,7 @@ def many_components(self, usage=None, model_name=None): meth:`~AbstractComponent.component` or meth:`~AbstractComponent.many_components` """ - return self._components_registry['base'](self).many_components( + return self.components_registry['base'](self).many_components( usage=usage, model_name=model_name, ) @@ -482,7 +498,7 @@ def model(self): return self.work.model def _component_class_by_name(self, name): - components_registry = self.work._components_registry + components_registry = self.work.components_registry component_class = components_registry.get(name) if not component_class: raise NoComponentError("No component with name '%s' found." % name) @@ -532,7 +548,7 @@ def component_by_name(self, name, model_name=None): return component_class(work_context) def _lookup_components(self, usage=None, model_name=None): - component_classes = self.work._components_registry.lookup( + component_classes = self.work.components_registry.lookup( self.collection._name, usage=usage, model_name=model_name, @@ -544,7 +560,7 @@ def component(self, usage=None, model_name=None): """ Find a component by usage and model for the current collection It searches a component using the rules of - :meth:`ComponentGlobalRegistry.lookup`. When a component is found, + :meth:`ComponentRegistry.lookup`. When a component is found, it initialize it with the current :class:`WorkContext` and returned. A :class:`odoo.addons.component.exception.SeveralComponentError` is @@ -584,7 +600,7 @@ def many_components(self, usage=None, model_name=None): """ Find many components by usage and model for the current collection It searches a component using the rules of - :meth:`ComponentGlobalRegistry.lookup`. When components are found, they + :meth:`ComponentRegistry.lookup`. When components are found, they initialized with the current :class:`WorkContext` and returned as a list. @@ -624,7 +640,7 @@ def _build_component(cls, registry): Component classes follow the ``_inherit`` chain. Once a Component class is created, it adds it in the Component Registry - (:class:`ComponentGlobalRegistry`), so it will be available for + (:class:`ComponentRegistry`), so it will be available for lookups. At the end of new class creation, a hook method diff --git a/component/tests/common.py b/component/tests/common.py index 97f50b9c53..93fdf446aa 100644 --- a/component/tests/common.py +++ b/component/tests/common.py @@ -6,7 +6,7 @@ from odoo.tests import common from odoo.addons.component.core import ( AbstractComponent, - ComponentGlobalRegistry, + ComponentRegistry, MetaComponent, ) @@ -22,7 +22,7 @@ def setUp(self): MetaComponent._modules_components.clear() # it will be our temporary component registry for our test session - self.comp_registry = ComponentGlobalRegistry() + self.comp_registry = ComponentRegistry() # there's always an implicit dependency on a 'base' component # so we must register one diff --git a/component/tests/test_lookup.py b/component/tests/test_lookup.py index 24834ac666..1de1aaabae 100644 --- a/component/tests/test_lookup.py +++ b/component/tests/test_lookup.py @@ -10,7 +10,7 @@ class TestLookup(ComponentRegistryCase): - """ Test the ComponentGlobalRegistry + """ Test the ComponentRegistry Tests in this testsuite mainly do: diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py index dff7b2ce88..37a92cfd65 100644 --- a/component/tests/test_work_on.py +++ b/component/tests/test_work_on.py @@ -3,10 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) from odoo.tests import common -from odoo.addons.component.core import ( - WorkContext, - ComponentGlobalRegistry, -) +from odoo.addons.component.core import WorkContext, ComponentRegistry class TestWorkOn(common.TransactionCase): @@ -33,7 +30,7 @@ def test_collection_work_on(self): def test_propagate_work_on(self): """ Check custom attributes and their propagation """ - registry = ComponentGlobalRegistry() + registry = ComponentRegistry() work = WorkContext( model_name='res.partner', collection=self.collection, @@ -42,7 +39,7 @@ def test_propagate_work_on(self): # we can pass our own keyword args that will set as attributes test_keyword='value', ) - self.assertIs(registry, work._components_registry) + self.assertIs(registry, work.components_registry) # check that our custom keyword is set as attribute self.assertEquals('value', work.test_keyword) @@ -53,6 +50,6 @@ def test_propagate_work_on(self): self.assertEquals(self.env, work2.env) self.assertEquals(self.collection, work2.collection) self.assertEquals('res.users', work2.model_name) - self.assertIs(registry, work2._components_registry) + self.assertIs(registry, work2.components_registry) # test_keyword has been propagated to the new WorkContext instance self.assertEquals('value', work2.test_keyword) From d71d9c65a788dc297ed1d4c673f425579cc9e43f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Sat, 17 Jun 2017 23:48:33 +0200 Subject: [PATCH 019/122] Add a cache on the components lookups --- component/__manifest__.py | 3 ++ component/builder.py | 19 +++++++++--- component/core.py | 57 ++++++++++++++++++++++++---------- component/tests/test_lookup.py | 31 ++++++++++++++++++ 4 files changed, 89 insertions(+), 21 deletions(-) diff --git a/component/__manifest__.py b/component/__manifest__.py index 656e49c86c..a4445d9839 100644 --- a/component/__manifest__.py +++ b/component/__manifest__.py @@ -11,6 +11,9 @@ 'category': 'Generic Modules', 'depends': ['base', ], + 'external_dependencies': { + 'python': ['cachetools'], + }, 'data': [], 'installable': True, } diff --git a/component/builder.py b/component/builder.py index 25e7a33485..0f73d2df08 100644 --- a/component/builder.py +++ b/component/builder.py @@ -13,7 +13,12 @@ import odoo from odoo import api, models -from .core import MetaComponent, _component_databases +from .core import ( + MetaComponent, + _component_databases, + ComponentRegistry, + DEFAULT_CACHE_SIZE, +) class ComponentBuilder(models.AbstractModel): @@ -38,13 +43,18 @@ class ComponentBuilder(models.AbstractModel): _name = 'component.builder' _description = 'Component Builder' + _components_registry_cache_size = DEFAULT_CACHE_SIZE + @api.model_cr 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 clear the components then rebuild them - _component_databases.clear(self.env.cr.dbname) - # TODO: reset the LRU cache of the component lookups when implemented + # 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. + components_registry = ComponentRegistry( + cachesize=self._components_registry_cache_size + ) + _component_databases[self.env.cr.dbname] = components_registry # lookup all the installed (or about to be) addons and generate # the graph, so we can load the components following the order @@ -61,7 +71,6 @@ def _register_hook(self): if name not in graph] graph.add_modules(self.env.cr, module_list) - components_registry = _component_databases[self.env.cr.dbname] for module in graph: self.load_components(module.name, components_registry) diff --git a/component/core.py b/component/core.py index 6fa9da3908..6fbb17f68c 100644 --- a/component/core.py +++ b/component/core.py @@ -17,6 +17,9 @@ """ +import logging +import operator + from collections import defaultdict, OrderedDict from odoo import models @@ -24,6 +27,21 @@ from .exception import NoComponentError, 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 @@ -40,23 +58,11 @@ def _get_addon_name(full_name): return addon_name -class ComponentDatabases(object): +class ComponentDatabases(dict): """ Holds a registry of components for each database """ - def __init__(self): - self._databases = {} - - def get(self, key, default=None): - return self._databases.get(key, default=default) - - def clear(self, dbname): - self._databases[dbname] = ComponentRegistry() - - def __getitem__(self, key): - return self._databases[key] - -class ComponentRegistry(OrderedDict): +class ComponentRegistry(object): """ Store all the components and allow to find them using criteria The key is the ``_name`` of the components. @@ -66,7 +72,26 @@ class ComponentRegistry(OrderedDict): """ - # TODO use a LRU cache (repoze.lru?) + def __init__(self, cachesize=DEFAULT_CACHE_SIZE): + self._cache = LRUCache(maxsize=cachesize) + self._components = OrderedDict() + + 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) + + @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 @@ -99,7 +124,7 @@ def lookup(self, collection_name=None, usage=None, model_name=None): # keep the order so addons loaded first have components used first candidates = ( - component for component in self.itervalues() + component for component in self._components.itervalues() if not component._abstract ) diff --git a/component/tests/test_lookup.py b/component/tests/test_lookup.py index 1de1aaabae..a1d0ba6068 100644 --- a/component/tests/test_lookup.py +++ b/component/tests/test_lookup.py @@ -161,3 +161,34 @@ class Any(Component): usage='listener', model_name='res.users') self.assertEqual('any', components[0]._name) + + def test_lookup_cache(self): + """ Lookup uses a cache """ + class Foo(Component): + _name = 'foo' + _collection = 'foobar' + + self._build_components(Foo) + + components = self.comp_registry.lookup('foobar') + self.assertEqual(['foo'], [c._name for c in components]) + + # we add a new component + class Bar(Component): + _name = 'bar' + _collection = 'foobar' + + self._build_components(Bar) + + # As the lookups are cached, we should still see only foo, + # even if we added a new component. + # We do this for testing, but in a real use case, we can't + # add new Component classes on the fly, and when we install + # new addons, the registry is rebuilt and cache cleared. + components = self.comp_registry.lookup('foobar') + self.assertEqual(['foo'], [c._name for c in components]) + + self.comp_registry._cache.clear() + # now we should find them both as the cache has been cleared + components = self.comp_registry.lookup('foobar') + self.assertEqual(['foo', 'bar'], [c._name for c in components]) From 8c3d466badd42b34b7498f25db80f37c6e1d7c12 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 19 Jun 2017 11:44:56 +0200 Subject: [PATCH 020/122] Improve components classes * Move the methods to get components from the components to the WorkContext, it should really be its responsibilities to get them. There are shorcuts on the components though. My first implementation was like that, then I moved the lookup methods on the components thinking that it would allow to customize them. But if we did that, we would have a different behavior from component to component which is bad, so if one really wanted to do that, better have to work with a different subclass of WorkContext, which would have its own behavior, consistently throughout all the work session. * Allow to switch the collection using work_on(), not only the model_name --- component/core.py | 259 +++++++++++++++++++++++----------------------- 1 file changed, 129 insertions(+), 130 deletions(-) diff --git a/component/core.py b/component/core.py index 6fbb17f68c..978667ba03 100644 --- a/component/core.py +++ b/component/core.py @@ -236,6 +236,7 @@ def __init__(self, model_name=None, collection=None, self.components_registry = _component_databases[dbname] self._propagate_kwargs = [ 'collection', + 'model_name', 'components_registry', ] for attr_name, value in kwargs.iteritems(): @@ -250,48 +251,147 @@ def env(self): """ return self.collection.env - def work_on(self, model_name): + 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} - return self.__class__(model_name=model_name, - **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 - Entrypoint to get a component, then you will probably use - meth:`~AbstractComponent.component_by_name` - """ - base = self.components_registry['base'](self) - return base.component_by_name(name, model_name=model_name) + If the component exists, an instance of it will be returned, + initialized with the current :class:`WorkContext`. - def component(self, usage=None, model_name=None): - """ Return a component + A :class:`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. - Entrypoint to get a component, then you will probably use - meth:`~AbstractComponent.component` or - meth:`~AbstractComponent.many_components` """ - return self.components_registry['base'](self).component( + 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 = "'%s'" % (component_class.apply_on_models[0],) + else: + hint_models = "" % ( + 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): + component_classes = self.components_registry.lookup( + self.collection._name, usage=usage, model_name=model_name, ) + return component_classes + + def component(self, usage=None, model_name=None): + """ 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 :class:`odoo.addons.component.exception.SeveralComponentError` is + raised if more than one component match for the provided + ``usage``/``model_name``. + + A :class:`odoo.addons.component.exception.NoComponentError` is raised + if no component is found for the provided ``usage``/``model_name``. + + """ + if isinstance(model_name, models.BaseModel): + model_name = model_name._name + model_name = model_name or self.model_name + component_classes = self._lookup_components( + usage=usage, model_name=model_name + ) + 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: + 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) + ) + if model_name == self.model_name: + work_context = self + else: + work_context = self.work_on(model_name) + return component_classes[0](work_context) + def many_components(self, usage=None, model_name=None): - """ Return several components + """ 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. - Entrypoint to get a component, then you will probably use - meth:`~AbstractComponent.component` or - meth:`~AbstractComponent.many_components` """ - return self.components_registry['base'](self).many_components( - usage=usage, - model_name=model_name, + if isinstance(model_name, models.BaseModel): + model_name = model_name._name + model_name = model_name or self.model_name + component_classes = self._lookup_components( + usage=usage, model_name=model_name ) + if model_name == self.model_name: + work_context = self + else: + work_context = self.work_on(model_name) + return [comp(work_context) for comp in component_classes] def __str__(self): return "WorkContext(%s,%s)" % (self.model_name, repr(self.collection)) @@ -522,127 +622,26 @@ def model(self): """ The model instance we are working with """ return self.work.model - def _component_class_by_name(self, name): - components_registry = self.work.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 :class:`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. - + Shortcut to meth:`~WorkContext.component_by_name` """ - 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.work.model_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 = "'%s'" % (component_class.apply_on_models[0],) - else: - hint_models = "" % ( - 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.work.model_name: - work_context = self.work - else: - work_context = self.work.work_on(model_name) - return component_class(work_context) - - def _lookup_components(self, usage=None, model_name=None): - component_classes = self.work.components_registry.lookup( - self.collection._name, - usage=usage, - model_name=model_name, - ) - - return component_classes + return self.work.component_by_name(name, model_name=model_name) def component(self, usage=None, model_name=None): - """ 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 :class:`odoo.addons.component.exception.SeveralComponentError` is - raised if more than one component match for the provided - ``usage``/``model_name``. - - A :class:`odoo.addons.component.exception.NoComponentError` is raised - if no component is found for the provided ``usage``/``model_name``. + """ Return a component + Shortcut to meth:`~WorkContext.component` """ - if isinstance(model_name, models.BaseModel): - model_name = model_name._name - model_name = model_name or self.work.model_name - component_classes = self._lookup_components( - usage=usage, model_name=model_name - ) - 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: - 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) - ) - if model_name == self.work.model_name: - work_context = self.work - else: - work_context = self.work.work_on(model_name) - return component_classes[0](work_context) + return self.work.component(usage=usage, model_name=model_name) def many_components(self, usage=None, model_name=None): - """ 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. + """ Return several components + Shortcut to meth:`~WorkContext.many_components` """ - if isinstance(model_name, models.BaseModel): - model_name = model_name._name - model_name = model_name or self.work.model_name - component_classes = self._lookup_components( - usage=usage, model_name=model_name - ) - if model_name == self.work.model_name: - work_context = self.work - else: - work_context = self.work.work_on(model_name) - return [comp(work_context) for comp in component_classes] + return self.work.many_components(usage=usage, model_name=model_name) def __str__(self): return "Component(%s)" % self._name From 676fd757c1d67225e9821fd72744df025518687a Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 19 Jun 2017 15:23:42 +0200 Subject: [PATCH 021/122] Continue the migration guide --- component/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/component/core.py b/component/core.py index 978667ba03..f4f5952c4c 100644 --- a/component/core.py +++ b/component/core.py @@ -277,7 +277,7 @@ def component_by_name(self, name, model_name=None): If the component exists, an instance of it will be returned, initialized with the current :class:`WorkContext`. - A :class:`odoo.addons.component.exception.NoComponentError` is raised + A :exc:`odoo.addons.component.exception.NoComponentError` is raised if: * no component with this name exists @@ -337,11 +337,11 @@ def component(self, usage=None, model_name=None): :meth:`ComponentRegistry.lookup`. When a component is found, it initialize it with the current :class:`WorkContext` and returned. - A :class:`odoo.addons.component.exception.SeveralComponentError` is + A :exc:`odoo.addons.component.exception.SeveralComponentError` is raised if more than one component match for the provided ``usage``/``model_name``. - A :class:`odoo.addons.component.exception.NoComponentError` is raised + A :exc:`odoo.addons.component.exception.NoComponentError` is raised if no component is found for the provided ``usage``/``model_name``. """ @@ -583,7 +583,7 @@ def vocalize(action, message): :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. + ``magento.xxx``) to prevent conflicts between addons. """ __metaclass__ = MetaComponent From 49d1c2cf772e3be2177aa6c15f74059e60a9ad05 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 19 Jun 2017 17:14:11 +0200 Subject: [PATCH 022/122] Fix test: post_install Tests using odoo transactions must run post install, because during the install the registry is not ready, so the components aren't neither. --- component/core.py | 11 ++++++++++- component/tests/test_work_on.py | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/component/core.py b/component/core.py index f4f5952c4c..47356b7bf6 100644 --- a/component/core.py +++ b/component/core.py @@ -233,7 +233,16 @@ def __init__(self, model_name=None, collection=None, self.components_registry = components_registry else: dbname = self.env.cr.dbname - self.components_registry = _component_databases[dbname] + try: + self.components_registry = _component_databases[dbname] + except KeyError: + _logger.error( + 'No component registry for database %s. ' + 'Probably because the Odoo registry has not been built ' + 'yet. Try reporting your call later.\n' + 'If you are in a test, activate post_install.' + ) + raise self._propagate_kwargs = [ 'collection', 'model_name', diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py index 37a92cfd65..cf56381d6d 100644 --- a/component/tests/test_work_on.py +++ b/component/tests/test_work_on.py @@ -14,6 +14,9 @@ class TestWorkOn(common.TransactionCase): """ + at_install = False + post_install = False + def setUp(self): super(TestWorkOn, self).setUp() self.collection = self.env['collection.base'] From 1981efc0aea59a9744187942c5e039c7e571b988 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 20 Jun 2017 09:35:09 +0200 Subject: [PATCH 023/122] Move base component in a components directory For consistency, this is where components should go (as for models, views, ...) --- component/__init__.py | 2 +- component/components/__init__.py | 2 ++ component/{base_components.py => components/base.py} | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 component/components/__init__.py rename component/{base_components.py => components/base.py} (90%) diff --git a/component/__init__.py b/component/__init__.py index 7d8580dec8..afa4a29290 100644 --- a/component/__init__.py +++ b/component/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from . import core -from . import base_components +from . import components from . import builder from . import models diff --git a/component/components/__init__.py b/component/components/__init__.py new file mode 100644 index 0000000000..a61d43eb9c --- /dev/null +++ b/component/components/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import base diff --git a/component/base_components.py b/component/components/base.py similarity index 90% rename from component/base_components.py rename to component/components/base.py index 5b1c7ad1ba..081b6a1547 100644 --- a/component/base_components.py +++ b/component/components/base.py @@ -2,7 +2,7 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -from .core import AbstractComponent +from ..core import AbstractComponent class BaseComponent(AbstractComponent): From 440aef19550bbcf57a23fcbd49936af50d7fde36 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 20 Jun 2017 09:58:33 +0200 Subject: [PATCH 024/122] Check that component registry is ready for events Checking that the odoo registry is ready is not working: in tests, the events are not working because they are run just before the odoo registry is set to ready. --- component/builder.py | 2 ++ component/core.py | 4 ++++ component/tests/common.py | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/component/builder.py b/component/builder.py index 0f73d2df08..393b3183d3 100644 --- a/component/builder.py +++ b/component/builder.py @@ -74,6 +74,8 @@ def _register_hook(self): for module in graph: self.load_components(module.name, components_registry) + components_registry.ready = True + def load_components(self, module, components_registry): """ Build every component known by MetaComponent for an odoo module diff --git a/component/core.py b/component/core.py index 47356b7bf6..d7543347aa 100644 --- a/component/core.py +++ b/component/core.py @@ -70,11 +70,15 @@ class ComponentRegistry(object): 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.ready = False def __getitem__(self, key): return self._components[key] diff --git a/component/tests/common.py b/component/tests/common.py index 93fdf446aa..e1251c22df 100644 --- a/component/tests/common.py +++ b/component/tests/common.py @@ -23,6 +23,11 @@ def setUp(self): # it will be our temporary component registry for our test session self.comp_registry = ComponentRegistry() + # Fake that we are ready to work with the registry + # normally, it is set to True and the end of the build + # of the components. Here, we'll add components later in + # the components registry, but we don't mind for the tests. + self.comp_registry.ready = True # there's always an implicit dependency on a 'base' component # so we must register one From c405df2891ed3f327445f39873935c34adeb6d76 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 20 Jun 2017 13:48:19 +0200 Subject: [PATCH 025/122] Use self.work.env, in case self.collection is empty (might be empty for EventWorkContext) --- component/core.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/component/core.py b/component/core.py index d7543347aa..e7f23657a0 100644 --- a/component/core.py +++ b/component/core.py @@ -407,7 +407,7 @@ def many_components(self, usage=None, model_name=None): return [comp(work_context) for comp in component_classes] def __str__(self): - return "WorkContext(%s,%s)" % (self.model_name, repr(self.collection)) + return "WorkContext(%s, %s)" % (self.model_name, repr(self.collection)) def __unicode__(self): return unicode(str(self)) @@ -628,7 +628,7 @@ def collection(self): @property def env(self): """ Current Odoo environment, the one of the collection record """ - return self.collection.env + return self.work.env @property def model(self): From 35f9b81c1799d6e648cf69349855f35f34d7ecff Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 22 Jun 2017 21:58:10 +0200 Subject: [PATCH 026/122] Change Collection.work_on() to a context manager It allows to open/close objects on start/end of a synchro / work session. The base method does none of that, but this is a really common use case. Example: at the beginning of the work session, I open a webservice client that is available in our WorkContext for the duration of our work. At the end, I close the client to end the connection. --- component/core.py | 34 +++--- component/models/collection.py | 37 +++++- component/tests/test_component.py | 194 +++++++++++++++++------------- component/tests/test_work_on.py | 12 +- 4 files changed, 166 insertions(+), 111 deletions(-) diff --git a/component/core.py b/component/core.py index e7f23657a0..830639616c 100644 --- a/component/core.py +++ b/component/core.py @@ -192,14 +192,13 @@ class WorkContext(object): work = WorkContext(model_name='res.partner', collection=collection) component = work.component(usage='record.importer') - Usually you will use the shortcut available thanks to the - `collection.base` Model: + Usually you will use the context manager on the ``collection.base`` Model: :: collection = self.env['my.collection'].browse(1) - work = collection.work_on('res.partner') - component = work.component(usage='record.importer') + 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. @@ -207,8 +206,8 @@ class WorkContext(object): :: collection = self.env['my.collection'].browse(1) - work = collection.work_on('res.partner', hello='world') - assert work.hello == 'world' + 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 @@ -217,13 +216,13 @@ class WorkContext(object): :: collection = self.env['my.collection'].browse(1) - work = collection.work_on('res.partner', hello='world') - 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' + 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' """ @@ -581,11 +580,12 @@ def vocalize(action, message): And their usage:: >>> coll = self.env['foo.bar.collection'].browse(1) - >>> work = coll.work_on('res.users') - >>> vocalizer = work.component(usage='vocalizer') - >>> vocalizer.vocalize('speak', 'hello world') + >>> 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') + ... vocalizer.vocalize('yell', 'hello world') HELLO WORLD!!! Hints: diff --git a/component/models/collection.py b/component/models/collection.py index 30699954a8..22033696d2 100644 --- a/component/models/collection.py +++ b/component/models/collection.py @@ -14,6 +14,7 @@ """ +from contextlib import contextmanager from odoo import models, api from ..core import WorkContext @@ -46,9 +47,9 @@ def run(self, magento_id): Use it:: >>> backend = self.env['magento.backend'].browse(1) - >>> work = backend.work_on('magento.sale.order') - >>> importer = work.component(usage='magento.sale.importer') - >>> importer.run(1) + >>> with backend.work_on('magento.sale.order') as work: + ... importer = work.component(usage='magento.sale.importer') + ... importer.run(1) See also: :class:`odoo.addons.component.core.WorkContext` @@ -57,14 +58,40 @@ def run(self, magento_id): _name = 'collection.base' _description = 'Base Abstract Collection' + @contextmanager @api.multi def work_on(self, model_name, **kwargs): - """ Entry-point for the components + """ Entry-point for the components, context manager Start a work using the components on the model. Any keyword argument will be assigned to the work context. See documentation of :class:`odoo.addons.component.core.WorkContext`. + It is a context manager, so you can attach objects and clean them + at the end of the work session, such as:: + + @contextmanager + @api.multi + def work_on(self, model_name, **kwargs): + self.ensure_one() + magento_location = MagentoLocation( + self.location, + self.username, + self.password, + ) + # We create a Magento Client API here, so we can create the + # client once (lazily on the first use) and propagate it + # through all the sync session, instead of recreating a client + # in each backend adapter usage. + with MagentoAPI(magento_location) as magento_api: + _super = super(MagentoBackend, self) + # from the components we'll be able to do: + # self.work.magento_api + with _super.work_on( + model_name, magento_api=magento_api, **kwargs + ) as work: + yield work + """ self.ensure_one() - return WorkContext(model_name=model_name, collection=self, **kwargs) + yield WorkContext(model_name=model_name, collection=self, **kwargs) diff --git a/component/tests/test_component.py b/component/tests/test_component.py index 6f6dda67cd..c6f6d0e50b 100644 --- a/component/tests/test_component.py +++ b/component/tests/test_component.py @@ -2,6 +2,7 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +from contextlib import contextmanager from odoo.addons.component.core import ( Component, ) @@ -48,101 +49,113 @@ class Component2(Component): # our collection, in a less abstract use case, it # could be a record of 'magento.backend' for instance self.collection_record = self.collection.new() - # Our WorkContext, it will be passed along in every - # components so we can share data transversally. - # We are working with res.partner in the following tests, - # unless we change it in the test. - self.work = self.collection_record.work_on( - 'res.partner', - # we use a custom registry only - # for the sake of the tests - components_registry=self.comp_registry - ) - # We get the 'base' component, handy to test the base - # methods component, many_components, ... - self.base = self.work.component_by_name('base') + + @contextmanager + def get_base(): + # Our WorkContext, it will be passed along in every + # components so we can share data transversally. + # We are working with res.partner in the following tests, + # unless we change it in the test. + with self.collection_record.work_on( + 'res.partner', + # we use a custom registry only + # for the sake of the tests + components_registry=self.comp_registry + ) as work: + # We get the 'base' component, handy to test the base + # methods component, many_components, ... + yield work.component_by_name('base') + self.get_base = get_base def test_component_attrs(self): """ Basic access to a Component's attribute """ - # as we are working on res.partner, we should get 'component1' - comp = self.work.component(usage='for.test') - # but this is not what we test here, we test the attributes: - self.assertEquals(self.collection_record, comp.collection) - self.assertEquals(self.work, comp.work) - self.assertEquals(self.env, comp.env) - self.assertEquals(self.env['res.partner'], comp.model) + with self.get_base() as base: + # as we are working on res.partner, we should get 'component1' + comp = base.work.component(usage='for.test') + # but this is not what we test here, we test the attributes: + self.assertEquals(self.collection_record, comp.collection) + self.assertEquals(base.work, comp.work) + self.assertEquals(self.env, comp.env) + self.assertEquals(self.env['res.partner'], comp.model) def test_component_get_by_name_same_model(self): """ Use component_by_name with current working model """ - # we ask a component directly by it's name, considering - # we work with res.partner, we should get 'component1' - # this is ok because it's _apply_on contains res.partner - comp = self.base.component_by_name('component1') - self.assertEquals('component1', comp._name) - self.assertEquals(self.env['res.partner'], comp.model) + with self.get_base() as base: + # we ask a component directly by it's name, considering + # we work with res.partner, we should get 'component1' + # this is ok because it's _apply_on contains res.partner + comp = base.component_by_name('component1') + self.assertEquals('component1', comp._name) + self.assertEquals(self.env['res.partner'], comp.model) def test_component_get_by_name_other_model(self): """ Use component_by_name with another model """ - # we ask a component directly by it's name, but we - # want to work with 'res.users', this is ok since - # component2's _apply_on contains res.users - comp = self.base.component_by_name( - 'component2', model_name='res.users' - ) - self.assertEquals('component2', comp._name) - self.assertEquals(self.env['res.users'], comp.model) - # what happens under the hood, is that a new WorkContext - # has been created for this model, with all the other values - # identical to the previous WorkContext (the one for res.partner) - # We can check that with: - self.assertNotEquals(self.work, comp.work) - self.assertEquals('res.partner', self.work.model_name) - self.assertEquals('res.users', comp.work.model_name) + with self.get_base() as base: + # we ask a component directly by it's name, but we + # want to work with 'res.users', this is ok since + # component2's _apply_on contains res.users + comp = base.component_by_name( + 'component2', model_name='res.users' + ) + self.assertEquals('component2', comp._name) + self.assertEquals(self.env['res.users'], comp.model) + # what happens under the hood, is that a new WorkContext + # has been created for this model, with all the other values + # identical to the previous WorkContext (the one for res.partner) + # We can check that with: + self.assertNotEquals(base.work, comp.work) + self.assertEquals('res.partner', base.work.model_name) + self.assertEquals('res.users', comp.work.model_name) def test_component_get_by_name_wrong_model(self): """ Use component_by_name with a model not in _apply_on """ msg = ("Component with name 'component2' can't be used " "for model 'res.partner'.*") - with self.assertRaisesRegexp(NoComponentError, msg): - # we ask for the model 'component2' but we are working - # with res.partner, and it only accepts res.users - self.base.component_by_name('component2') + with self.get_base() as base: + with self.assertRaisesRegexp(NoComponentError, msg): + # we ask for the model 'component2' but we are working + # with res.partner, and it only accepts res.users + base.component_by_name('component2') def test_component_get_by_name_not_exist(self): """ Use component_by_name on a component that do not exist """ msg = "No component with name 'foo' found." - with self.assertRaisesRegexp(NoComponentError, msg): - self.base.component_by_name('foo') + with self.get_base() as base: + with self.assertRaisesRegexp(NoComponentError, msg): + base.component_by_name('foo') def test_component_by_usage_same_model(self): """ Use component(usage=...) on the same model """ # we ask for a component having _usage == 'for.test', and # model being res.partner (the model in the current WorkContext) - comp = self.base.component(usage='for.test') - self.assertEquals('component1', comp._name) - self.assertEquals(self.env['res.partner'], comp.model) + with self.get_base() as base: + comp = base.component(usage='for.test') + self.assertEquals('component1', comp._name) + self.assertEquals(self.env['res.partner'], comp.model) def test_component_by_usage_other_model(self): """ Use component(usage=...) on a different model (name) """ # we ask for a component having _usage == 'for.test', and # a different model (res.users) - comp = self.base.component(usage='for.test', model_name='res.users') - self.assertEquals('component2', comp._name) - self.assertEquals(self.env['res.users'], comp.model) - # what happens under the hood, is that a new WorkContext - # has been created for this model, with all the other values - # identical to the previous WorkContext (the one for res.partner) - # We can check that with: - self.assertNotEquals(self.work, comp.work) - self.assertEquals('res.partner', self.work.model_name) - self.assertEquals('res.users', comp.work.model_name) + with self.get_base() as base: + comp = base.component(usage='for.test', model_name='res.users') + self.assertEquals('component2', comp._name) + self.assertEquals(self.env['res.users'], comp.model) + # what happens under the hood, is that a new WorkContext + # has been created for this model, with all the other values + # identical to the previous WorkContext (the one for res.partner) + # We can check that with: + self.assertNotEquals(base.work, comp.work) + self.assertEquals('res.partner', base.work.model_name) + self.assertEquals('res.users', comp.work.model_name) def test_component_by_usage_other_model_env(self): """ Use component(usage=...) on a different model (instance) """ - comp = self.base.component(usage='for.test', - model_name=self.env['res.users']) - self.assertEquals('component2', comp._name) - self.assertEquals(self.env['res.users'], comp.model) + with self.get_base() as base: + comp = base.component(usage='for.test', + model_name=self.env['res.users']) + self.assertEquals('component2', comp._name) + self.assertEquals(self.env['res.users'], comp.model) def test_component_error_several(self): """ Use component(usage=...) when more than one component match """ @@ -155,12 +168,13 @@ class Component3(Component): Component3._build_component(self.comp_registry) - with self.assertRaises(SeveralComponentError): - # When a component has no _apply_on, it means it can be applied - # on *any* model. Here, the candidates components would be: - # component1 (because we are working with res.partner), - # component3 (because it has no _apply_on so apply in any case) - self.base.component(usage='for.test') + with self.get_base() as base: + with self.assertRaises(SeveralComponentError): + # When a component has no _apply_on, it means it can be applied + # on *any* model. Here, the candidates components would be: + # component1 (because we are working with res.partner), + # component3 (because it has no _apply_on so apply in any case) + base.component(usage='for.test') def test_many_components(self): """ Use many_components(usage=...) on the same model """ @@ -170,7 +184,10 @@ class Component3(Component): _usage = 'for.test' Component3._build_component(self.comp_registry) - comps = self.base.many_components(usage='for.test') + + with self.get_base() as base: + comps = base.many_components(usage='for.test') + # When a component has no _apply_on, it means it can be applied # on *any* model. So here, both component1 and component3 match self.assertEqual( @@ -187,8 +204,11 @@ class Component3(Component): _usage = 'for.test' Component3._build_component(self.comp_registry) - comps = self.base.many_components(usage='for.test', - model_name='res.users') + + with self.get_base() as base: + comps = base.many_components(usage='for.test', + model_name='res.users') + self.assertEqual( ['component2', 'component3'], [c._name for c in comps] @@ -203,8 +223,11 @@ class Component3(Component): _usage = 'for.test' Component3._build_component(self.comp_registry) - comps = self.base.many_components(usage='for.test', - model_name=self.env['res.users']) + + with self.get_base() as base: + comps = base.many_components(usage='for.test', + model_name=self.env['res.users']) + self.assertEqual( ['component2', 'component3'], [c._name for c in comps] @@ -212,21 +235,26 @@ class Component3(Component): def test_no_component(self): """ No component found for asked usage """ - with self.assertRaises(NoComponentError): - self.base.component(usage='foo') + with self.get_base() as base: + with self.assertRaises(NoComponentError): + base.component(usage='foo') def test_no_many_component(self): """ No component found for asked usage for many_components() """ - self.assertEquals([], self.base.many_components(usage='foo')) + with self.get_base() as base: + self.assertEquals([], base.many_components(usage='foo')) def test_work_on_component(self): """ Check WorkContext.component() (shortcut to Component.component) """ - comp = self.work.component(usage='for.test') - self.assertEquals('component1', comp._name) + with self.get_base() as base: + comp = base.work.component(usage='for.test') + self.assertEquals('component1', comp._name) def test_work_on_many_components(self): """ Check WorkContext.many_components() - (shortcut to Component.many_components) """ - comps = self.work.many_components(usage='for.test') - self.assertEquals('component1', comps[0]._name) + (shortcut to Component.many_components) + """ + with self.get_base() as base: + comps = base.work.many_components(usage='for.test') + self.assertEquals('component1', comps[0]._name) diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py index cf56381d6d..4d1f732398 100644 --- a/component/tests/test_work_on.py +++ b/component/tests/test_work_on.py @@ -24,12 +24,12 @@ def setUp(self): def test_collection_work_on(self): """ Create a new instance and test attributes access """ collection_record = self.collection.new() - work = collection_record.work_on('res.partner') - self.assertEquals(collection_record, work.collection) - self.assertEquals('collection.base', work.collection._name) - self.assertEquals('res.partner', work.model_name) - self.assertEquals(self.env['res.partner'], work.model) - self.assertEquals(self.env, work.env) + with collection_record.work_on('res.partner') as work: + self.assertEquals(collection_record, work.collection) + self.assertEquals('collection.base', work.collection._name) + self.assertEquals('res.partner', work.model_name) + self.assertEquals(self.env['res.partner'], work.model) + self.assertEquals(self.env, work.env) def test_propagate_work_on(self): """ Check custom attributes and their propagation """ From a8d48776bb3cb82d681a1fbe3fa11ee54ca7a8ab Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Thu, 29 Jun 2017 23:14:42 +0200 Subject: [PATCH 027/122] Allow to build components for a specific module on demand. When a module is loaded with test-enable=True, the components of this module are not registerd by the call to the method _register_hook of the component.builder. Indeed the state of the module is still *to install*. This change allows to call the method *load_components* on the component.builder for a specific module by taking care to not reload the components of a module already loaded. --- component/builder.py | 11 ++++++----- component/core.py | 8 ++++++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/component/builder.py b/component/builder.py index 393b3183d3..3c3c1dbda1 100644 --- a/component/builder.py +++ b/component/builder.py @@ -10,11 +10,9 @@ Build the components at the build of a registry. """ - import odoo from odoo import api, models from .core import ( - MetaComponent, _component_databases, ComponentRegistry, DEFAULT_CACHE_SIZE, @@ -66,6 +64,7 @@ def _register_hook(self): "SELECT name " "FROM ir_module_module " "WHERE state IN ('installed', 'to upgrade', 'to update')" + ) module_list = [name for (name,) in self.env.cr.fetchall() if name not in graph] @@ -76,7 +75,7 @@ def _register_hook(self): components_registry.ready = True - def load_components(self, module, components_registry): + def load_components(self, module, components_registry=None): """ Build every component known by MetaComponent for an odoo module The final component (composed by all the Component classes in this @@ -88,5 +87,7 @@ def load_components(self, module, components_registry): :param registry: the registry in which we want to put the Component :type registry: :py:class:`~.core.ComponentRegistry` """ - for component_class in MetaComponent._modules_components[module]: - component_class._build_component(components_registry) + components_registry = ( + components_registry or + _component_databases[self.env.cr.dbname]) + components_registry.load_components(module) diff --git a/component/core.py b/component/core.py index 830639616c..4d9873997d 100644 --- a/component/core.py +++ b/component/core.py @@ -78,6 +78,7 @@ class ComponentRegistry(object): 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): @@ -95,6 +96,13 @@ def get(self, key, default=None): 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 From b21f761adce1cfbd604a1d0036ce3e080e41aad7 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 30 Jun 2017 11:02:29 +0200 Subject: [PATCH 028/122] Simplify tests by loading modules components It the previous commit, @lmignon added the possibility to load all components of an addon in a Component Registry. This commit takes benefit of this feature to simplify the existing tests and to add a base test case for the Connector (that loads all components of 'component', 'component_event', 'connector'). It can be used in implementations using the connector. --- component/builder.py | 5 ++- component/tests/common.py | 64 +++++++++++++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 14 deletions(-) diff --git a/component/builder.py b/component/builder.py index 3c3c1dbda1..aee44df12f 100644 --- a/component/builder.py +++ b/component/builder.py @@ -53,7 +53,9 @@ def _register_hook(self): cachesize=self._components_registry_cache_size ) _component_databases[self.env.cr.dbname] = components_registry + self.build_registry(components_registry) + def build_registry(self, components_registry): # 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 @@ -71,7 +73,8 @@ def _register_hook(self): graph.add_modules(self.env.cr, module_list) for module in graph: - self.load_components(module.name, components_registry) + self.load_components(module.name, + components_registry=components_registry) components_registry.ready = True diff --git a/component/tests/common.py b/component/tests/common.py index e1251c22df..7e0aaa1ca9 100644 --- a/component/tests/common.py +++ b/component/tests/common.py @@ -2,47 +2,81 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) +import copy + import unittest2 from odoo.tests import common from odoo.addons.component.core import ( - AbstractComponent, ComponentRegistry, MetaComponent, ) class ComponentRegistryCase(unittest2.TestCase): + """ This test case can be used as a base for writings tests on components + + It creates a special + :class:`odoo.addons.componenent.core.ComponentRegistry` for the purpose + of the tests. It loads the ``base`` component in it. In your tests, + you can add more components in 2 manners. + + All the components of an Odoo module:: + + self._load_module_components('connector') + + Only specific components:: + + self._build_components(MyComponent1, MyComponent2) + + Note: for the lookups of the components, the default component + registry is a global registry for the database. Here, you will + need to explicitly pass ``self.comp_registry`` in the + :class:`~odoo.addons.component.core.WorkContext`:: + + work = WorkContext(model_name='res.users', + collection='my.collection', + components_registry=self.comp_registry) + + Or:: + + collection_record = self.env['my.collection'].browse(1) + with collection_record.work_on( + 'res.partner', + components_registry=self.comp_registry) as work: + + """ def setUp(self): super(ComponentRegistryCase, self).setUp() # keep the original classes registered by the metaclass - # so we'll restore them at the end of the tests - self._original_components = MetaComponent._modules_components.copy() - MetaComponent._modules_components.clear() + # so we'll restore them at the end of the tests, it avoid + # to pollute it with Stub / Test components + self._original_components = copy.deepcopy( + MetaComponent._modules_components + ) # it will be our temporary component registry for our test session self.comp_registry = ComponentRegistry() + + # it builds the 'final component' for every component of the + # 'component' addon and push them in the component registry + self.comp_registry.load_components('component') + # Fake that we are ready to work with the registry # normally, it is set to True and the end of the build # of the components. Here, we'll add components later in # the components registry, but we don't mind for the tests. self.comp_registry.ready = True - # there's always an implicit dependency on a 'base' component - # so we must register one - class Base(AbstractComponent): - _name = 'base' - - # it builds the 'final component' and push it in the component - # registry - Base._build_component(self.comp_registry) - def tearDown(self): super(ComponentRegistryCase, self).tearDown() # restore the original metaclass' classes MetaComponent._modules_components = self._original_components + def _load_module_components(self, module): + self.comp_registry.load_components(module) + def _build_components(self, *classes): for cls in classes: cls._build_component(self.comp_registry) @@ -50,6 +84,7 @@ def _build_components(self, *classes): class TransactionComponentRegistryCase(common.TransactionCase, ComponentRegistryCase): + """ Adds Odoo Transaction in the base Component TestCase """ def setUp(self): # resolve an inheritance issue (common.TransactionCase does not use @@ -58,6 +93,9 @@ def setUp(self): ComponentRegistryCase.setUp(self) self.collection = self.env['collection.base'] + # build the components of every installed addons + self.env['component.builder'].build_registry(self.comp_registry) + def teardown(self): common.TransactionCase.tearDown(self) ComponentRegistryCase.tearDown(self) From d0102ef91e44e52f95b66083e49cbb884d538155 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 4 Jul 2017 13:13:22 +0200 Subject: [PATCH 029/122] Add a new method to refine match of components This method can be used to filter out some components. It is executed on every candidate component returned by a lookup for a collection/usage/model so must be kept light to execute if possible. A use case can be for instance to have more than one kind of export in a connector for a model. Say that you have a different way to export customers and suppliers: with self.work_on('res.partner', kind='customer') as work: exporter = work.component(usage='record.exporter) exporter.run() with self.work_on('res.partner', kind='supplier') as work: exporter = work.component(usage='record.exporter) exporter.run() You can declare your components as: class CustomerExporter(Component): _name = 'customer.exporter' _inherit = ['base.exporter'] _usage = 'record.exporter' _apply_on = ['res.partner'] @classmethod def _component_match(cls, work): return work.kind == 'customer' def run(self): # export customer class SupplierExporter(Component): _name = 'supplier.exporter' _inherit = ['base.exporter'] _usage = 'record.exporter' _apply_on = ['res.partner'] @classmethod def _component_match(cls, work): return work.kind == 'supplier' def run(self): # export supplier --- component/core.py | 23 ++++++++++++++++++++++- component/tests/test_component.py | 27 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/component/core.py b/component/core.py index 4d9873997d..822449d4c8 100644 --- a/component/core.py +++ b/component/core.py @@ -348,7 +348,7 @@ def _lookup_components(self, usage=None, model_name=None): model_name=model_name, ) - return component_classes + return [cls for cls in component_classes if cls._component_match(self)] def component(self, usage=None, model_name=None): """ Find a component by usage and model for the current collection @@ -628,6 +628,27 @@ def __init__(self, work_context): super(AbstractComponent, self).__init__() self.work = work_context + @classmethod + def _component_match(cls, work): + """ 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 """ diff --git a/component/tests/test_component.py b/component/tests/test_component.py index c6f6d0e50b..b14075eb2f 100644 --- a/component/tests/test_component.py +++ b/component/tests/test_component.py @@ -258,3 +258,30 @@ def test_work_on_many_components(self): with self.get_base() as base: comps = base.work.many_components(usage='for.test') self.assertEquals('component1', comps[0]._name) + + def test_component_match(self): + """ Lookup with match method """ + class Foo(Component): + _name = 'foo' + _collection = 'collection.base' + _usage = 'speaker' + _apply_on = ['res.partner'] + + @classmethod + def _component_match(cls, work): + return False + + class Bar(Component): + _name = 'bar' + _collection = 'collection.base' + _usage = 'speaker' + _apply_on = ['res.partner'] + + self._build_components(Foo, Bar) + + with self.get_base() as base: + # both components would we returned without the + # _component_match method + comp = base.component(usage='speaker', + model_name=self.env['res.partner']) + self.assertEquals('bar', comp._name) From b95760aea7fa5cda3907270f2b31bcfe93c493e9 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 4 Jul 2017 16:05:59 +0200 Subject: [PATCH 030/122] Add new TestCase classes to test connectors When we want to test the implementation of a connector, it must be simple. Add 2 new test cases for TransactionCase and SavepointCase which load all the components of the dependencies addons on the tested addons. Only the components of the addons in state 'installed' are loaded, so we don't load components of addons that depend on the tested addon. The tests must call _init_global_registry and build_registry instead of _register_hook, so the registry is not set to ready. If the global registry is set to ready during tests, the event will trigger during the installation of addons which is not what we want. The existing test case is meant only to be used when one has to manipulate components in a custom registry. See discussion on https://github.com/guewen/connector/commit/447b22f8e36c7e258eba5e8b0d93c1577bd50606#commitcomment-22851711 --- component/builder.py | 16 ++++-- component/core.py | 3 +- component/tests/common.py | 93 ++++++++++++++++++++++++++++++--- component/tests/test_work_on.py | 7 +-- 4 files changed, 100 insertions(+), 19 deletions(-) diff --git a/component/builder.py b/component/builder.py index aee44df12f..b4d9d46f0c 100644 --- a/component/builder.py +++ b/component/builder.py @@ -49,13 +49,20 @@ def _register_hook(self): # 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. + components_registry = self._init_global_registry() + self.build_registry(components_registry) + components_registry.ready = True + + def _init_global_registry(self): components_registry = ComponentRegistry( cachesize=self._components_registry_cache_size ) _component_databases[self.env.cr.dbname] = components_registry - self.build_registry(components_registry) + return components_registry - def build_registry(self, components_registry): + def build_registry(self, components_registry, states=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 @@ -65,7 +72,8 @@ def build_registry(self, components_registry): self.env.cr.execute( "SELECT name " "FROM ir_module_module " - "WHERE state IN ('installed', 'to upgrade', 'to update')" + "WHERE state IN %s", + (tuple(states),) ) module_list = [name for (name,) in self.env.cr.fetchall() @@ -76,8 +84,6 @@ def build_registry(self, components_registry): self.load_components(module.name, components_registry=components_registry) - components_registry.ready = True - def load_components(self, module, components_registry=None): """ Build every component known by MetaComponent for an odoo module diff --git a/component/core.py b/component/core.py index 822449d4c8..37963fdb9e 100644 --- a/component/core.py +++ b/component/core.py @@ -250,8 +250,7 @@ def __init__(self, model_name=None, collection=None, _logger.error( 'No component registry for database %s. ' 'Probably because the Odoo registry has not been built ' - 'yet. Try reporting your call later.\n' - 'If you are in a test, activate post_install.' + 'yet.' ) raise self._propagate_kwargs = [ diff --git a/component/tests/common.py b/component/tests/common.py index 7e0aaa1ca9..cc8c9ca99e 100644 --- a/component/tests/common.py +++ b/component/tests/common.py @@ -4,21 +4,97 @@ import copy +from contextlib import contextmanager + import unittest2 +import odoo +from odoo import api from odoo.tests import common from odoo.addons.component.core import ( ComponentRegistry, MetaComponent, + _get_addon_name, ) +@contextmanager +def new_rollbacked_env(): + registry = odoo.registry(common.get_db_name()) + uid = odoo.SUPERUSER_ID + cr = registry.cursor() + try: + yield api.Environment(cr, uid, {}) + finally: + cr.rollback() # we shouldn't have to commit anything + cr.close() + + +class ComponentMixin(object): + + @classmethod + def setUpComponent(cls): + with new_rollbacked_env() as env: + builder = env['component.builder'] + # build the components of every installed addons + comp_registry = builder._init_global_registry() + # ensure that we load only the components of the 'installed' + # modules, not 'to install', which means we load only the + # dependencies of the tested addons, not the siblings or + # chilren addons + builder.build_registry(comp_registry, states=('installed',)) + # build the components of the current tested addon + current_addon = _get_addon_name(cls.__module__) + env['component.builder'].load_components(current_addon) + + +class TransactionComponentCase(common.TransactionCase, ComponentMixin): + """ A TransactionCase that loads all the components + + It it used like an usual Odoo's TransactionCase, but it ensures + that all the components of the current addon and its dependencies + are loaded. + + """ + + @classmethod + def setUpClass(cls): + super(TransactionComponentCase, cls).setUpClass() + cls.setUpComponent() + + +class SavepointComponentCase(common.SavepointCase, ComponentMixin): + """ A SavepointCase that loads all the components + + It it used like an usual Odoo's SavepointCase, but it ensures + that all the components of the current addon and its dependencies + are loaded. + + """ + + @classmethod + def setUpClass(cls): + super(SavepointComponentCase, cls).setUpClass() + cls.setUpComponent() + + class ComponentRegistryCase(unittest2.TestCase): """ This test case can be used as a base for writings tests on components - It creates a special - :class:`odoo.addons.componenent.core.ComponentRegistry` for the purpose - of the tests. It loads the ``base`` component in it. In your tests, - you can add more components in 2 manners. + This test case is meant to test components in a special component registry, + where you want to have maximum control on which components are loaded + or not, or when you want to create additional components in your tests. + + If you only want to *use* the components of the tested addon in your tests, + then consider using one of: + + * :class:`TransactionComponentCase` + * :class:`SavepointComponentCase` + + This test case creates a special + :class:`odoo.addons.component.core.ComponentRegistry` for the purpose of + the tests. By default, it loads all the components of the dependencies, but + not the components of the current addon (which you have to handle + manually). In your tests, you can add more components in 2 manners. All the components of an Odoo module:: @@ -62,6 +138,12 @@ def setUp(self): # it builds the 'final component' for every component of the # 'component' addon and push them in the component registry self.comp_registry.load_components('component') + # build the components of every installed addons already installed + with new_rollbacked_env() as env: + env['component.builder'].build_registry( + self.comp_registry, + states=('installed',), + ) # Fake that we are ready to work with the registry # normally, it is set to True and the end of the build @@ -93,9 +175,6 @@ def setUp(self): ComponentRegistryCase.setUp(self) self.collection = self.env['collection.base'] - # build the components of every installed addons - self.env['component.builder'].build_registry(self.comp_registry) - def teardown(self): common.TransactionCase.tearDown(self) ComponentRegistryCase.tearDown(self) diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py index 4d1f732398..5a004f17c7 100644 --- a/component/tests/test_work_on.py +++ b/component/tests/test_work_on.py @@ -2,11 +2,11 @@ # Copyright 2017 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) -from odoo.tests import common from odoo.addons.component.core import WorkContext, ComponentRegistry +from .common import TransactionComponentCase -class TestWorkOn(common.TransactionCase): +class TestWorkOn(TransactionComponentCase): """ Test on WorkContext This model is mostly a container, so we check the access @@ -14,9 +14,6 @@ class TestWorkOn(common.TransactionCase): """ - at_install = False - post_install = False - def setUp(self): super(TestWorkOn, self).setUp() self.collection = self.env['collection.base'] From 91a47c3983e5fc3d1d9c5fcee07244190b77aaec Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 6 Jul 2017 16:26:37 +0200 Subject: [PATCH 031/122] Exclude current addon in ComponentRegistryCase setup When we run a ComponentRegistryCase test using odoo's way to run tests (--test-enable), the current addon is in state 'to install' or 'to upgrade', thus its components not loaded (which is what we want in this test Case, as it allow us to load manually the components / load different components). When we run the same test Case from pytest or nosetest, which are not run during an odoo upgrade, the state of the addon is 'installed', thus components are loaded. To ensure the same behavior with --test-enable and external tests runners, the current addon is always excluded from the components to load. --- component/builder.py | 15 ++++++++++----- component/tests/common.py | 5 +++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/component/builder.py b/component/builder.py index b4d9d46f0c..7e537755e5 100644 --- a/component/builder.py +++ b/component/builder.py @@ -60,7 +60,8 @@ def _init_global_registry(self): _component_databases[self.env.cr.dbname] = components_registry return components_registry - def build_registry(self, components_registry, states=None): + def build_registry(self, components_registry, states=None, + exclude_addons=None): if not states: states = ('installed', 'to upgrade') # lookup all the installed (or about to be) addons and generate @@ -69,13 +70,17 @@ def build_registry(self, components_registry, states=None): graph = odoo.modules.graph.Graph() graph.add_module(self.env.cr, 'base') - self.env.cr.execute( + query = ( "SELECT name " "FROM ir_module_module " - "WHERE state IN %s", - (tuple(states),) - + "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) diff --git a/component/tests/common.py b/component/tests/common.py index cc8c9ca99e..7607ea5257 100644 --- a/component/tests/common.py +++ b/component/tests/common.py @@ -139,10 +139,15 @@ def setUp(self): # 'component' addon and push them in the component registry self.comp_registry.load_components('component') # build the components of every installed addons already installed + # but the current addon (when running with pytest/nosetest, we + # simulate the --test-enable behavior by excluding the current addon + # which is in 'to install' / 'to upgrade' with --test-enable). + current_addon = _get_addon_name(self.__module__) with new_rollbacked_env() as env: env['component.builder'].build_registry( self.comp_registry, states=('installed',), + exclude_addons=[current_addon], ) # Fake that we are ready to work with the registry From a3f1847d1a869662cc78de6568d5036e10e9f867 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 6 Jul 2017 17:00:50 +0200 Subject: [PATCH 032/122] Set component registry to ready only during tests Events are not triggered if the component registry is not ready. Set the component registry to ready and disable it at the end of the tests, to prevent triggering events during installation of the addon. --- component/tests/common.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/component/tests/common.py b/component/tests/common.py index 7607ea5257..5a9b8f5077 100644 --- a/component/tests/common.py +++ b/component/tests/common.py @@ -37,6 +37,7 @@ def setUpComponent(cls): builder = env['component.builder'] # build the components of every installed addons comp_registry = builder._init_global_registry() + cls._components_registry = comp_registry # ensure that we load only the components of the 'installed' # modules, not 'to install', which means we load only the # dependencies of the tested addons, not the siblings or @@ -46,6 +47,15 @@ def setUpComponent(cls): current_addon = _get_addon_name(cls.__module__) env['component.builder'].load_components(current_addon) + def setUp(self): + # should be ready only during tests, never during installation + # of addons + self._components_registry.ready = True + + @self.addCleanup + def notready(): + self._components_registry.ready = False + class TransactionComponentCase(common.TransactionCase, ComponentMixin): """ A TransactionCase that loads all the components @@ -61,6 +71,12 @@ def setUpClass(cls): super(TransactionComponentCase, cls).setUpClass() cls.setUpComponent() + def setUp(self): + # resolve an inheritance issue (common.TransactionCase does not call + # super) + common.TransactionCase.setUp(self) + ComponentMixin.setUp(self) + class SavepointComponentCase(common.SavepointCase, ComponentMixin): """ A SavepointCase that loads all the components @@ -76,6 +92,12 @@ def setUpClass(cls): super(SavepointComponentCase, cls).setUpClass() cls.setUpComponent() + def setUp(self): + # resolve an inheritance issue (common.SavepointCase does not call + # super) + common.SavepointCase.setUp(self) + ComponentMixin.setUp(self) + class ComponentRegistryCase(unittest2.TestCase): """ This test case can be used as a base for writings tests on components From 0d0646a2fce63c0c31d6ffc0b0e0c1663db5d02f Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 11 Jul 2017 15:18:51 +0200 Subject: [PATCH 033/122] Add readme files --- component/README.rst | 92 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 component/README.rst diff --git a/component/README.rst b/component/README.rst new file mode 100644 index 0000000000..196c1809b4 --- /dev/null +++ b/component/README.rst @@ -0,0 +1,92 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +========== +Components +========== + +This module implements a component system and is a base block for the Connector +Framework. It can be used without using the full Connector though. + +Documentation: http://odoo-connector.com/ + +Installation +============ + +* Install ``component`` + +Configuration +============= + +The module does nothing by itself and has no configuration. + +Usage +===== + +As a developer, you have access to a component system. You can find the +documentation in the code or on http://odoo-connector.com + +In a nutshell, you can create components:: + + + from odoo.addons.component.core import Component + + class MagentoPartnerAdapter(Component): + _name = 'magento.partner.adapter' + _inherit = 'magento.adapter' + + _usage = 'backend.adapter' + _collection = 'magento.backend' + _apply_on = ['res.partner'] + +And later, find the component you need at runtime (dynamic dispatch at +component level):: + + def run(self, external_id): + backend_adapter = self.component(usage='backend.adapter') + external_data = backend_adapter.read(external_id) + + +Known issues / Roadmap +====================== + +* ... + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smash it by providing detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* Guewen Baconnier + +Do not contact contributors directly about support or help with technical issues. + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit https://odoo-community.org. From d206b6c24bc21244eb02e758408a94f696394a97 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 15 Aug 2017 14:19:27 +0200 Subject: [PATCH 034/122] Allow to add an inheritance on an existing component --- component/core.py | 4 ++-- component/tests/test_build_component.py | 26 ++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/component/core.py b/component/core.py index 37963fdb9e..7fb303b25d 100644 --- a/component/core.py +++ b/component/core.py @@ -749,7 +749,7 @@ def _build_component(cls, registry): # B2 ComponentA B1 # class B2(Component): \ | / # _name = 'b' \ | / - # _inherit = ['a', 'b'] \ | / + # _inherit = ['b', 'a'] \ | / # ComponentB # class A2(Component): # _inherit = 'a' @@ -761,7 +761,7 @@ def _build_component(cls, registry): elif parents is None: parents = [] - if cls._name in registry: + 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)) diff --git a/component/tests/test_build_component.py b/component/tests/test_build_component.py index b551ab02f7..0085d95727 100644 --- a/component/tests/test_build_component.py +++ b/component/tests/test_build_component.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import mock -from odoo.addons.component.core import Component +from odoo.addons.component.core import AbstractComponent, Component from .common import ComponentRegistryCase @@ -201,3 +201,27 @@ class Component2(Component): msg = 'Component.*inherits from non-existing component.*' with self.assertRaisesRegexp(TypeError, msg): Component2._build_component(self.comp_registry) + + def test_add_inheritance(self): + """ Ensure we can add a new inheritance """ + class Component1(Component): + _name = 'component1' + + class Component2(Component): + _name = 'component2' + + class Component2bis(Component): + _name = 'component2' + _inherit = ['component2', 'component1'] + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + Component2bis._build_component(self.comp_registry) + + self.assertEquals( + (Component2bis, + Component2, + self.comp_registry['component1'], + self.comp_registry['base']), + self.comp_registry['component2'].__bases__ + ) From 3b917128e9047fb2218a2b1ac4e85e67ce03315c Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 15 Aug 2017 14:20:05 +0200 Subject: [PATCH 035/122] Make abstract/non-abstract inheritance more robust * forbid to transform an AbstractComponent to a Component * forbid to have an AbstractComponent inherit from a Component --- component/core.py | 25 ++++++++++++++ component/tests/test_build_component.py | 45 +++++++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/component/core.py b/component/core.py index 7fb303b25d..88df11cb74 100644 --- a/component/core.py +++ b/component/core.py @@ -782,6 +782,8 @@ def _build_component(cls, 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,), @@ -790,6 +792,7 @@ def _build_component(cls, registry): # 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]) @@ -804,6 +807,7 @@ def _build_component(cls, registry): 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) @@ -814,6 +818,27 @@ def _build_component(cls, registry): 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): + """ 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 diff --git a/component/tests/test_build_component.py b/component/tests/test_build_component.py index 0085d95727..9e475bdf67 100644 --- a/component/tests/test_build_component.py +++ b/component/tests/test_build_component.py @@ -225,3 +225,48 @@ class Component2bis(Component): self.comp_registry['base']), self.comp_registry['component2'].__bases__ ) + + def test_check_parent_component_over_abstract(self): + """ Component can inherit from AbstractComponent """ + class Component1(AbstractComponent): + _name = 'component1' + + class Component2(Component): + _name = 'component2' + _inherit = 'component1' + + Component1._build_component(self.comp_registry) + Component2._build_component(self.comp_registry) + self.assertTrue( + self.comp_registry['component1']._abstract + ) + self.assertFalse( + self.comp_registry['component2']._abstract + ) + + def test_check_parent_abstract_over_component(self): + """ Prevent AbstractComponent to inherit from Component """ + class Component1(Component): + _name = 'component1' + + class Component2(AbstractComponent): + _name = 'component2' + _inherit = 'component1' + + Component1._build_component(self.comp_registry) + msg = '.*cannot inherit from the non-abstract.*' + with self.assertRaisesRegexp(TypeError, msg): + Component2._build_component(self.comp_registry) + + def test_check_transform_abstract_to_component(self): + """ Prevent AbstractComponent to be transformed to Component """ + class Component1(AbstractComponent): + _name = 'component1' + + class Component1bis(Component): + _inherit = 'component1' + + Component1._build_component(self.comp_registry) + msg = '.*transforms the abstract component.*into a non-abstract.*' + with self.assertRaisesRegexp(TypeError, msg): + Component1bis._build_component(self.comp_registry) From ca44417b684972ebcfb59a9f03889d4ba2ffc799 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 3 Oct 2017 10:16:49 +0200 Subject: [PATCH 036/122] Set modules uninstallable --- component/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component/__manifest__.py b/component/__manifest__.py index a4445d9839..c12ee3cb77 100644 --- a/component/__manifest__.py +++ b/component/__manifest__.py @@ -15,5 +15,5 @@ 'python': ['cachetools'], }, 'data': [], - 'installable': True, + 'installable': False, } From 77aa9e9cda39bb86720e42b67d312e360cd60fe9 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 3 Oct 2017 10:37:57 +0200 Subject: [PATCH 037/122] Make addons installable --- component/__manifest__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/component/__manifest__.py b/component/__manifest__.py index c12ee3cb77..5601832a70 100644 --- a/component/__manifest__.py +++ b/component/__manifest__.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) {'name': 'Components', - 'version': '10.0.1.0.0', + 'version': '11.0.1.0.0', 'author': 'Camptocamp,' 'Odoo Community Association (OCA)', 'website': 'https://www.camptocamp.com', @@ -15,5 +15,5 @@ 'python': ['cachetools'], }, 'data': [], - 'installable': False, + 'installable': True, } From a505428d97f64a21fa7894fd0234fe12c979cfdf Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 3 Oct 2017 10:43:50 +0200 Subject: [PATCH 038/122] PY3: apply automated changes by 2to3 on addons --- component/core.py | 17 +++----- component/tests/common.py | 4 +- component/tests/test_build_component.py | 34 ++++++++-------- component/tests/test_component.py | 52 ++++++++++++------------- component/tests/test_lookup.py | 4 +- component/tests/test_work_on.py | 22 +++++------ 6 files changed, 63 insertions(+), 70 deletions(-) diff --git a/component/core.py b/component/core.py index 88df11cb74..87761a93d4 100644 --- a/component/core.py +++ b/component/core.py @@ -136,7 +136,7 @@ def lookup(self, collection_name=None, usage=None, model_name=None): # keep the order so addons loaded first have components used first candidates = ( - component for component in self._components.itervalues() + component for component in self._components.values() if not component._abstract ) @@ -258,7 +258,7 @@ def __init__(self, model_name=None, collection=None, 'model_name', 'components_registry', ] - for attr_name, value in kwargs.iteritems(): + for attr_name, value in kwargs.items(): setattr(self, attr_name, value) self._propagate_kwargs.append(attr_name) @@ -415,9 +415,6 @@ def many_components(self, usage=None, model_name=None): def __str__(self): return "WorkContext(%s, %s)" % (self.model_name, repr(self.collection)) - def __unicode__(self): - return unicode(str(self)) - __repr__ = __str__ @@ -448,12 +445,12 @@ def apply_on_models(self): if self._apply_on is None: return None # always return a list, used for the lookup - elif isinstance(self._apply_on, basestring): + elif isinstance(self._apply_on, str): return [self._apply_on] return self._apply_on -class AbstractComponent(object): +class AbstractComponent(object, metaclass=MetaComponent): """ Main Component Model All components have a Python inheritance either on @@ -606,7 +603,6 @@ def vocalize(action, message): ``magento.xxx``) to prevent conflicts between addons. """ - __metaclass__ = MetaComponent _register = False _abstract = True @@ -687,9 +683,6 @@ def many_components(self, usage=None, model_name=None): def __str__(self): return "Component(%s)" % self._name - def __unicode__(self): - return unicode(str(self)) - __repr__ = __str__ @classmethod @@ -756,7 +749,7 @@ def _build_component(cls, registry): # determine inherited components parents = cls._inherit - if isinstance(parents, basestring): + if isinstance(parents, str): parents = [parents] elif parents is None: parents = [] diff --git a/component/tests/common.py b/component/tests/common.py index 5a9b8f5077..7a46218605 100644 --- a/component/tests/common.py +++ b/component/tests/common.py @@ -6,7 +6,7 @@ from contextlib import contextmanager -import unittest2 +import unittest import odoo from odoo import api from odoo.tests import common @@ -99,7 +99,7 @@ def setUp(self): ComponentMixin.setUp(self) -class ComponentRegistryCase(unittest2.TestCase): +class ComponentRegistryCase(unittest.TestCase): """ This test case can be used as a base for writings tests on components This test case is meant to test components in a special component registry, diff --git a/component/tests/test_build_component.py b/component/tests/test_build_component.py index 9e475bdf67..0d7bc35fd5 100644 --- a/component/tests/test_build_component.py +++ b/component/tests/test_build_component.py @@ -29,7 +29,7 @@ class Component1(Component): pass msg = '.*must have a _name.*' - with self.assertRaisesRegexp(TypeError, msg): + with self.assertRaisesRegex(TypeError, msg): Component1._build_component(self.comp_registry) def test_register(self): @@ -44,7 +44,7 @@ class Component2(Component): # them in the components registry Component1._build_component(self.comp_registry) Component2._build_component(self.comp_registry) - self.assertEquals( + self.assertEqual( ['base', 'component1', 'component2'], list(self.comp_registry) ) @@ -63,7 +63,7 @@ class Component3(Component): Component1._build_component(self.comp_registry) Component2._build_component(self.comp_registry) Component3._build_component(self.comp_registry) - self.assertEquals( + self.assertEqual( (Component3, Component2, Component1, @@ -92,24 +92,24 @@ class Component4(Component): Component2._build_component(self.comp_registry) Component3._build_component(self.comp_registry) Component4._build_component(self.comp_registry) - self.assertEquals( + self.assertEqual( (Component1, self.comp_registry['base']), self.comp_registry['component1'].__bases__ ) - self.assertEquals( + self.assertEqual( (Component2, self.comp_registry['component1'], self.comp_registry['base']), self.comp_registry['component2'].__bases__ ) - self.assertEquals( + self.assertEqual( (Component3, self.comp_registry['component1'], self.comp_registry['base']), self.comp_registry['component3'].__bases__ ) - self.assertEquals( + self.assertEqual( (Component4, self.comp_registry['component2'], self.comp_registry['component3'], @@ -160,10 +160,10 @@ def say(self): # for this test component1 = self.comp_registry['component1'](mock.Mock()) component2 = self.comp_registry['component2'](mock.Mock()) - self.assertEquals('ping', component1.msg) - self.assertEquals('pong', component2.msg) - self.assertEquals('foo', component1.say()) - self.assertEquals('foo bar', component2.say()) + self.assertEqual('ping', component1.msg) + self.assertEqual('pong', component2.msg) + self.assertEqual('foo', component1.say()) + self.assertEqual('foo bar', component2.say()) def test_duplicate_component(self): """ Check that we can't have 2 components with the same name """ @@ -175,7 +175,7 @@ class Component2(Component): Component1._build_component(self.comp_registry) msg = 'Component.*already exists.*' - with self.assertRaisesRegexp(TypeError, msg): + with self.assertRaisesRegex(TypeError, msg): Component2._build_component(self.comp_registry) def test_no_parent(self): @@ -185,7 +185,7 @@ class Component1(Component): _inherit = 'component1' msg = 'Component.*does not exist in registry.*' - with self.assertRaisesRegexp(TypeError, msg): + with self.assertRaisesRegex(TypeError, msg): Component1._build_component(self.comp_registry) def test_no_parent2(self): @@ -199,7 +199,7 @@ class Component2(Component): Component1._build_component(self.comp_registry) msg = 'Component.*inherits from non-existing component.*' - with self.assertRaisesRegexp(TypeError, msg): + with self.assertRaisesRegex(TypeError, msg): Component2._build_component(self.comp_registry) def test_add_inheritance(self): @@ -218,7 +218,7 @@ class Component2bis(Component): Component2._build_component(self.comp_registry) Component2bis._build_component(self.comp_registry) - self.assertEquals( + self.assertEqual( (Component2bis, Component2, self.comp_registry['component1'], @@ -255,7 +255,7 @@ class Component2(AbstractComponent): Component1._build_component(self.comp_registry) msg = '.*cannot inherit from the non-abstract.*' - with self.assertRaisesRegexp(TypeError, msg): + with self.assertRaisesRegex(TypeError, msg): Component2._build_component(self.comp_registry) def test_check_transform_abstract_to_component(self): @@ -268,5 +268,5 @@ class Component1bis(Component): Component1._build_component(self.comp_registry) msg = '.*transforms the abstract component.*into a non-abstract.*' - with self.assertRaisesRegexp(TypeError, msg): + with self.assertRaisesRegex(TypeError, msg): Component1bis._build_component(self.comp_registry) diff --git a/component/tests/test_component.py b/component/tests/test_component.py index b14075eb2f..ebc04e6879 100644 --- a/component/tests/test_component.py +++ b/component/tests/test_component.py @@ -73,10 +73,10 @@ def test_component_attrs(self): # as we are working on res.partner, we should get 'component1' comp = base.work.component(usage='for.test') # but this is not what we test here, we test the attributes: - self.assertEquals(self.collection_record, comp.collection) - self.assertEquals(base.work, comp.work) - self.assertEquals(self.env, comp.env) - self.assertEquals(self.env['res.partner'], comp.model) + self.assertEqual(self.collection_record, comp.collection) + self.assertEqual(base.work, comp.work) + self.assertEqual(self.env, comp.env) + self.assertEqual(self.env['res.partner'], comp.model) def test_component_get_by_name_same_model(self): """ Use component_by_name with current working model """ @@ -85,8 +85,8 @@ def test_component_get_by_name_same_model(self): # we work with res.partner, we should get 'component1' # this is ok because it's _apply_on contains res.partner comp = base.component_by_name('component1') - self.assertEquals('component1', comp._name) - self.assertEquals(self.env['res.partner'], comp.model) + self.assertEqual('component1', comp._name) + self.assertEqual(self.env['res.partner'], comp.model) def test_component_get_by_name_other_model(self): """ Use component_by_name with another model """ @@ -97,22 +97,22 @@ def test_component_get_by_name_other_model(self): comp = base.component_by_name( 'component2', model_name='res.users' ) - self.assertEquals('component2', comp._name) - self.assertEquals(self.env['res.users'], comp.model) + self.assertEqual('component2', comp._name) + self.assertEqual(self.env['res.users'], comp.model) # what happens under the hood, is that a new WorkContext # has been created for this model, with all the other values # identical to the previous WorkContext (the one for res.partner) # We can check that with: - self.assertNotEquals(base.work, comp.work) - self.assertEquals('res.partner', base.work.model_name) - self.assertEquals('res.users', comp.work.model_name) + self.assertNotEqual(base.work, comp.work) + self.assertEqual('res.partner', base.work.model_name) + self.assertEqual('res.users', comp.work.model_name) def test_component_get_by_name_wrong_model(self): """ Use component_by_name with a model not in _apply_on """ msg = ("Component with name 'component2' can't be used " "for model 'res.partner'.*") with self.get_base() as base: - with self.assertRaisesRegexp(NoComponentError, msg): + with self.assertRaisesRegex(NoComponentError, msg): # we ask for the model 'component2' but we are working # with res.partner, and it only accepts res.users base.component_by_name('component2') @@ -121,7 +121,7 @@ def test_component_get_by_name_not_exist(self): """ Use component_by_name on a component that do not exist """ msg = "No component with name 'foo' found." with self.get_base() as base: - with self.assertRaisesRegexp(NoComponentError, msg): + with self.assertRaisesRegex(NoComponentError, msg): base.component_by_name('foo') def test_component_by_usage_same_model(self): @@ -130,8 +130,8 @@ def test_component_by_usage_same_model(self): # model being res.partner (the model in the current WorkContext) with self.get_base() as base: comp = base.component(usage='for.test') - self.assertEquals('component1', comp._name) - self.assertEquals(self.env['res.partner'], comp.model) + self.assertEqual('component1', comp._name) + self.assertEqual(self.env['res.partner'], comp.model) def test_component_by_usage_other_model(self): """ Use component(usage=...) on a different model (name) """ @@ -139,23 +139,23 @@ def test_component_by_usage_other_model(self): # a different model (res.users) with self.get_base() as base: comp = base.component(usage='for.test', model_name='res.users') - self.assertEquals('component2', comp._name) - self.assertEquals(self.env['res.users'], comp.model) + self.assertEqual('component2', comp._name) + self.assertEqual(self.env['res.users'], comp.model) # what happens under the hood, is that a new WorkContext # has been created for this model, with all the other values # identical to the previous WorkContext (the one for res.partner) # We can check that with: - self.assertNotEquals(base.work, comp.work) - self.assertEquals('res.partner', base.work.model_name) - self.assertEquals('res.users', comp.work.model_name) + self.assertNotEqual(base.work, comp.work) + self.assertEqual('res.partner', base.work.model_name) + self.assertEqual('res.users', comp.work.model_name) def test_component_by_usage_other_model_env(self): """ Use component(usage=...) on a different model (instance) """ with self.get_base() as base: comp = base.component(usage='for.test', model_name=self.env['res.users']) - self.assertEquals('component2', comp._name) - self.assertEquals(self.env['res.users'], comp.model) + self.assertEqual('component2', comp._name) + self.assertEqual(self.env['res.users'], comp.model) def test_component_error_several(self): """ Use component(usage=...) when more than one component match """ @@ -242,13 +242,13 @@ def test_no_component(self): def test_no_many_component(self): """ No component found for asked usage for many_components() """ with self.get_base() as base: - self.assertEquals([], base.many_components(usage='foo')) + self.assertEqual([], base.many_components(usage='foo')) def test_work_on_component(self): """ Check WorkContext.component() (shortcut to Component.component) """ with self.get_base() as base: comp = base.work.component(usage='for.test') - self.assertEquals('component1', comp._name) + self.assertEqual('component1', comp._name) def test_work_on_many_components(self): """ Check WorkContext.many_components() @@ -257,7 +257,7 @@ def test_work_on_many_components(self): """ with self.get_base() as base: comps = base.work.many_components(usage='for.test') - self.assertEquals('component1', comps[0]._name) + self.assertEqual('component1', comps[0]._name) def test_component_match(self): """ Lookup with match method """ @@ -284,4 +284,4 @@ class Bar(Component): # _component_match method comp = base.component(usage='speaker', model_name=self.env['res.partner']) - self.assertEquals('bar', comp._name) + self.assertEqual('bar', comp._name) diff --git a/component/tests/test_lookup.py b/component/tests/test_lookup.py index a1d0ba6068..a4ae7071d0 100644 --- a/component/tests/test_lookup.py +++ b/component/tests/test_lookup.py @@ -81,7 +81,7 @@ def test_lookup_no_component(self): """ No component """ # we just expect an empty list when no component match, the error # handling is handled at an higher level - self.assertEquals( + self.assertEqual( [], self.comp_registry.lookup('something', usage='something') ) @@ -94,7 +94,7 @@ class Foo(AbstractComponent): self._build_components(Foo) # this is just a dict access - self.assertEquals('foo', self.comp_registry['foo']._name) + self.assertEqual('foo', self.comp_registry['foo']._name) def test_lookup_abstract(self): """ Do not include abstract components in lookup """ diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py index 5a004f17c7..645ff85484 100644 --- a/component/tests/test_work_on.py +++ b/component/tests/test_work_on.py @@ -22,11 +22,11 @@ def test_collection_work_on(self): """ Create a new instance and test attributes access """ collection_record = self.collection.new() with collection_record.work_on('res.partner') as work: - self.assertEquals(collection_record, work.collection) - self.assertEquals('collection.base', work.collection._name) - self.assertEquals('res.partner', work.model_name) - self.assertEquals(self.env['res.partner'], work.model) - self.assertEquals(self.env, work.env) + self.assertEqual(collection_record, work.collection) + self.assertEqual('collection.base', work.collection._name) + self.assertEqual('res.partner', work.model_name) + self.assertEqual(self.env['res.partner'], work.model) + self.assertEqual(self.env, work.env) def test_propagate_work_on(self): """ Check custom attributes and their propagation """ @@ -41,15 +41,15 @@ def test_propagate_work_on(self): ) self.assertIs(registry, work.components_registry) # check that our custom keyword is set as attribute - self.assertEquals('value', work.test_keyword) + self.assertEqual('value', work.test_keyword) # when we want to work on another model, work_on() create # another instance and propagate the attributes to it work2 = work.work_on('res.users') - self.assertNotEquals(work, work2) - self.assertEquals(self.env, work2.env) - self.assertEquals(self.collection, work2.collection) - self.assertEquals('res.users', work2.model_name) + self.assertNotEqual(work, work2) + self.assertEqual(self.env, work2.env) + self.assertEqual(self.collection, work2.collection) + self.assertEqual('res.users', work2.model_name) self.assertIs(registry, work2.components_registry) # test_keyword has been propagated to the new WorkContext instance - self.assertEquals('value', work2.test_keyword) + self.assertEqual('value', work2.test_keyword) From f3e977ffd882a2691ae766dea109bf0c524f84af Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 3 Oct 2017 16:58:21 +0200 Subject: [PATCH 039/122] Fix some pylint-odoo warnings --- component/README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/component/README.rst b/component/README.rst index 196c1809b4..9a6a40889e 100644 --- a/component/README.rst +++ b/component/README.rst @@ -51,8 +51,6 @@ component level):: Known issues / Roadmap ====================== -* ... - Bug Tracker =========== From f4f0670fb82aa8544d7e2ee7e258f7052e2808fe Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Sat, 6 Jan 2018 03:28:39 +0100 Subject: [PATCH 040/122] OCA Transbot updated translations from Transifex --- component/i18n/am.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/ca.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/de.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/el_GR.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/es.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/es_ES.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/fi.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/fr.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/gl.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/it.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/pt.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/pt_BR.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/pt_PT.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/sl.po | 47 +++++++++++++++++++++++++++++++++++++++++ component/i18n/tr.po | 47 +++++++++++++++++++++++++++++++++++++++++ 15 files changed, 705 insertions(+) create mode 100644 component/i18n/am.po create mode 100644 component/i18n/ca.po create mode 100644 component/i18n/de.po create mode 100644 component/i18n/el_GR.po create mode 100644 component/i18n/es.po create mode 100644 component/i18n/es_ES.po create mode 100644 component/i18n/fi.po create mode 100644 component/i18n/fr.po create mode 100644 component/i18n/gl.po create mode 100644 component/i18n/it.po create mode 100644 component/i18n/pt.po create mode 100644 component/i18n/pt_BR.po create mode 100644 component/i18n/pt_PT.po create mode 100644 component/i18n/sl.po create mode 100644 component/i18n/tr.po diff --git a/component/i18n/am.po b/component/i18n/am.po new file mode 100644 index 0000000000..d8face26d7 --- /dev/null +++ b/component/i18n/am.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Amharic (https://www.transifex.com/oca/teams/23907/am/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: am\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "" diff --git a/component/i18n/ca.po b/component/i18n/ca.po new file mode 100644 index 0000000000..d37b3ecdc1 --- /dev/null +++ b/component/i18n/ca.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Catalan (https://www.transifex.com/oca/teams/23907/ca/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: ca\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "" diff --git a/component/i18n/de.po b/component/i18n/de.po new file mode 100644 index 0000000000..c0c62dc863 --- /dev/null +++ b/component/i18n/de.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: German (https://www.transifex.com/oca/teams/23907/de/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: de\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "Anzeigebezeichnung" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "Zuletzt aktualisiert am" diff --git a/component/i18n/el_GR.po b/component/i18n/el_GR.po new file mode 100644 index 0000000000..8e467cc64c --- /dev/null +++ b/component/i18n/el_GR.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Greek (Greece) (https://www.transifex.com/oca/teams/23907/el_GR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: el_GR\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "Κωδικός" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "" diff --git a/component/i18n/es.po b/component/i18n/es.po new file mode 100644 index 0000000000..dd05413fbe --- /dev/null +++ b/component/i18n/es.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "Nombre mostrado" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "Última modificación el" diff --git a/component/i18n/es_ES.po b/component/i18n/es_ES.po new file mode 100644 index 0000000000..772dd10022 --- /dev/null +++ b/component/i18n/es_ES.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Spanish (Spain) (https://www.transifex.com/oca/teams/23907/es_ES/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: es_ES\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "" diff --git a/component/i18n/fi.po b/component/i18n/fi.po new file mode 100644 index 0000000000..d471af82dc --- /dev/null +++ b/component/i18n/fi.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Finnish (https://www.transifex.com/oca/teams/23907/fi/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fi\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "Nimi" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "Viimeksi muokattu" diff --git a/component/i18n/fr.po b/component/i18n/fr.po new file mode 100644 index 0000000000..08991d1396 --- /dev/null +++ b/component/i18n/fr.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "Dernière modification le" diff --git a/component/i18n/gl.po b/component/i18n/gl.po new file mode 100644 index 0000000000..0b4d7527b0 --- /dev/null +++ b/component/i18n/gl.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Galician (https://www.transifex.com/oca/teams/23907/gl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: gl\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "" diff --git a/component/i18n/it.po b/component/i18n/it.po new file mode 100644 index 0000000000..c6fc01029c --- /dev/null +++ b/component/i18n/it.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Italian (https://www.transifex.com/oca/teams/23907/it/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: it\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "Nome da visualizzare" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "Ultima modifica il" diff --git a/component/i18n/pt.po b/component/i18n/pt.po new file mode 100644 index 0000000000..3bd5803765 --- /dev/null +++ b/component/i18n/pt.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Portuguese (https://www.transifex.com/oca/teams/23907/pt/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: pt\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "" diff --git a/component/i18n/pt_BR.po b/component/i18n/pt_BR.po new file mode 100644 index 0000000000..b1f1ff5d1b --- /dev/null +++ b/component/i18n/pt_BR.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Portuguese (Brazil) (https://www.transifex.com/oca/teams/23907/pt_BR/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: pt_BR\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "" diff --git a/component/i18n/pt_PT.po b/component/i18n/pt_PT.po new file mode 100644 index 0000000000..435d542ffa --- /dev/null +++ b/component/i18n/pt_PT.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Portuguese (Portugal) (https://www.transifex.com/oca/teams/23907/pt_PT/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: pt_PT\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "" diff --git a/component/i18n/sl.po b/component/i18n/sl.po new file mode 100644 index 0000000000..eb7b7f11bc --- /dev/null +++ b/component/i18n/sl.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: sl\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "Prikazni naziv" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "Zadnjič spremenjeno" diff --git a/component/i18n/tr.po b/component/i18n/tr.po new file mode 100644 index 0000000000..104255e727 --- /dev/null +++ b/component/i18n/tr.po @@ -0,0 +1,47 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +# Translators: +# OCA Transbot , 2018 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-01-05 16:56+0000\n" +"PO-Revision-Date: 2018-01-05 16:56+0000\n" +"Last-Translator: OCA Transbot , 2018\n" +"Language-Team: Turkish (https://www.transifex.com/oca/teams/23907/tr/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Language: tr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "ID" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "" From 4d46dd4e7ffad066a3b937ee5cce74218294d140 Mon Sep 17 00:00:00 2001 From: "Laurent Mignon (ACSONE)" Date: Tue, 9 Jan 2018 16:39:17 +0100 Subject: [PATCH 041/122] component: Fix component lookup by usage If more than one component is found for a given usage, return the one linked to the current collection --- component/core.py | 6 ++++++ component/tests/test_component.py | 23 +++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/component/core.py b/component/core.py index 87761a93d4..0ce6938480 100644 --- a/component/core.py +++ b/component/core.py @@ -377,6 +377,12 @@ def component(self, usage=None, model_name=None): (self.collection._name, usage, model_name) ) elif len(component_classes) > 1: + # If we have more than one component, try to find the one + # specificaly linked to the collection + component_classes = [ + c for c in component_classes + if c._collection == self.collection._name] + if len(component_classes) != 1: raise SeveralComponentError( "Several components found for collection '%s', " "usage '%s', model_name '%s'. Found: %r" % diff --git a/component/tests/test_component.py b/component/tests/test_component.py index ebc04e6879..24a4a9d0ef 100644 --- a/component/tests/test_component.py +++ b/component/tests/test_component.py @@ -176,6 +176,29 @@ class Component3(Component): # component3 (because it has no _apply_on so apply in any case) base.component(usage='for.test') + def test_component_specific_collection(self): + """ Use component(usage=...) when more than one component match but + only one for the specific collection""" + # we create a new Component with _usage 'for.test', without collection + # and no _apply_on + class Component3(Component): + _name = 'component3' + _usage = 'for.test' + + Component3._build_component(self.comp_registry) + + with self.get_base() as base: + # When a component has no _apply_on, it means it can be applied + # on *any* model. Here, the candidates components would be: + # component1 (because we are working with res.partner), + # component3 (because it has no _apply_on so apply in any case). + # When a component has no _collection, it means it can be applied + # on all model if no component is found for the current collection: + # component3 must be ignored since a component (component1) exists + # and is specificaly linked to the expected collection. + comp = base.component(usage='for.test') + self.assertEquals('component1', comp._name) + def test_many_components(self): """ Use many_components(usage=...) on the same model """ class Component3(Component): From 7be6c44fc107b28741abf809da42a096cda4c2d0 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Thu, 25 Jan 2018 11:52:06 +0100 Subject: [PATCH 042/122] Refine lookup on specific model over generic component With components having a generic implementation and another one linked with a model such as: ```python class GenericExporter(Component): _name = 'generic.exporter' _inherit = ['base.exporter'] _usage = 'record.exporter' def run(self, records): return self._export_items(records) class SaleExporter(Component): _name = 'sale.exporter' _inherit = ['generic.exporter'] _apply_on = 'sale.order' def run(self, records): return self.do_stuff(records) ``` The previous behavior with this code: ```python exporter = work.component('record.exporter') ``` Was to raise an error: ``` SeveralComponentError: Several components found for collection 'xx.backend', usage 'record.exporter', model_name 'sale.order'. Found: [, ] ``` This commit refines the lookup so if a specific Component is linked with the asked model, it will be returned over the generic one. It allows to build generic components and override them only in some use cases. Follows the motivation of #272 but as a model's level instead of collection. Closes #276 --- component/core.py | 18 +++++- component/tests/test_component.py | 92 +++++++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 6 deletions(-) diff --git a/component/core.py b/component/core.py index 0ce6938480..6aa4acbc16 100644 --- a/component/core.py +++ b/component/core.py @@ -108,7 +108,7 @@ 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 might will be returned in any case (as far as + ``_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. @@ -356,6 +356,14 @@ def component(self, usage=None, model_name=None): :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``. @@ -378,10 +386,16 @@ def component(self, usage=None, model_name=None): ) elif len(component_classes) > 1: # If we have more than one component, try to find the one - # specificaly linked to the collection + # specifically linked to the collection... component_classes = [ c for c in component_classes if c._collection == self.collection._name] + if len(component_classes) > 1: + # ... or try to find the one specifically linked to the model + component_classes = [ + c for c in component_classes + if c.apply_on_models and model_name in c.apply_on_models + ] if len(component_classes) != 1: raise SeveralComponentError( "Several components found for collection '%s', " diff --git a/component/tests/test_component.py b/component/tests/test_component.py index 24a4a9d0ef..8433b370de 100644 --- a/component/tests/test_component.py +++ b/component/tests/test_component.py @@ -158,24 +158,79 @@ def test_component_by_usage_other_model_env(self): self.assertEqual(self.env['res.users'], comp.model) def test_component_error_several(self): - """ Use component(usage=...) when more than one component match """ - # we create a new Component with _usage 'for.test', in the same - # collection and no _apply_on + """ Use component(usage=...) when more than one generic component match + """ + # we create 1 new Component with _usage 'for.test', in the same + # collection and no _apply_on, and we remove the _apply_on of component + # 1 so they are generic components for a collection class Component3(Component): _name = 'component3' _collection = 'collection.base' _usage = 'for.test' + class Component1(Component): + _inherit = 'component1' + _collection = 'collection.base' + _usage = 'for.test' + _apply_on = None + Component3._build_component(self.comp_registry) + Component1._build_component(self.comp_registry) with self.get_base() as base: with self.assertRaises(SeveralComponentError): # When a component has no _apply_on, it means it can be applied # on *any* model. Here, the candidates components would be: - # component1 (because we are working with res.partner), # component3 (because it has no _apply_on so apply in any case) + # component4 (for the same reason) + base.component(usage='for.test') + + def test_component_error_several_same_model(self): + """ Use component(usage=...) when more than one component match a model + """ + # we create a new Component with _usage 'for.test', in the same + # collection and no _apply_on + class Component3(Component): + _name = 'component3' + _collection = 'collection.base' + _usage = 'for.test' + _apply_on = ['res.partner'] + + Component3._build_component(self.comp_registry) + + with self.get_base() as base: + with self.assertRaises(SeveralComponentError): + # Here, the candidates components would be: + # component1 (because we are working with res.partner), + # component3 (for the same reason) base.component(usage='for.test') + def test_component_specific_model(self): + """ Use component(usage=...) when more than one component match but + only one for the specific model""" + # we create a new Component with _usage 'for.test', in the same + # collection and no _apply_on. This is a generic component for the + # collection + class Component3(Component): + _name = 'component3' + _collection = 'collection.base' + _usage = 'for.test' + + Component3._build_component(self.comp_registry) + + with self.get_base() as base: + # When a component has no _apply_on, it means it can be applied on + # *any* model. Here, the candidates components would be: + # component1 # (because we are working with res.partner), + # component3 (because it # has no _apply_on so apply in any case). + # When a component is specifically linked to a model with + # _apply_on, it takes precedence over a generic component. It + # allows to create a generic implementation (component3 here) and + # override it only for a given model. So in this case, the final + # component is component1. + comp = base.component(usage='for.test') + self.assertEquals('component1', comp._name) + def test_component_specific_collection(self): """ Use component(usage=...) when more than one component match but only one for the specific collection""" @@ -199,6 +254,35 @@ class Component3(Component): comp = base.component(usage='for.test') self.assertEquals('component1', comp._name) + def test_component_specific_collection_specific_model(self): + """ Use component(usage=...) when more than one component match but + only one for the specific model and collection""" + # we create a new Component with _usage 'for.test', without collection + # and no _apply_on. This is a component generic for all collections and + # models + class Component3(Component): + _name = 'component3' + _usage = 'for.test' + + Component3._build_component(self.comp_registry) + + with self.get_base() as base: + # When a component has no _apply_on, it means it can be applied on + # *any* model, no _collection, it can be applied on *any* + # collection. + # Here, the candidates components would be: + # component1 (because we are working with res.partner), + # component3 (because it has no _apply_on and no _collection so + # apply in any case). + # When a component is specifically linked to a model with + # _apply_on, it takes precedence over a generic component, the same + # happens for collection. It allows to create a generic + # implementation (component3 here) and override it only for a given + # collection and model. So in this case, the final component is + # component1. + comp = base.component(usage='for.test') + self.assertEquals('component1', comp._name) + def test_many_components(self): """ Use many_components(usage=...) on the same model """ class Component3(Component): From 2eeb21e15f2c349ade27ae4b07a82d0e042bb32a Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 31 Jan 2018 15:03:41 +0100 Subject: [PATCH 043/122] Bump component at 11.0.1.1.0 --- component/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component/__manifest__.py b/component/__manifest__.py index 5601832a70..e06abb5eae 100644 --- a/component/__manifest__.py +++ b/component/__manifest__.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) {'name': 'Components', - 'version': '11.0.1.0.0', + 'version': '11.0.1.1.0', 'author': 'Camptocamp,' 'Odoo Community Association (OCA)', 'website': 'https://www.camptocamp.com', From 89b5f11d9e191076632677c4f50c8129961f9cee Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Sat, 17 Feb 2018 03:25:32 +0100 Subject: [PATCH 044/122] OCA Transbot updated translations from Transifex --- component/i18n/fr.po | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/component/i18n/fr.po b/component/i18n/fr.po index 08991d1396..d32daf9aa1 100644 --- a/component/i18n/fr.po +++ b/component/i18n/fr.po @@ -4,13 +4,14 @@ # # Translators: # OCA Transbot , 2018 +# Nicolas JEUDY , 2018 msgid "" msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-01-05 16:56+0000\n" -"PO-Revision-Date: 2018-01-05 16:56+0000\n" -"Last-Translator: OCA Transbot , 2018\n" +"POT-Creation-Date: 2018-02-01 01:48+0000\n" +"PO-Revision-Date: 2018-02-01 01:48+0000\n" +"Last-Translator: Nicolas JEUDY , 2018\n" "Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -21,12 +22,12 @@ msgstr "" #. module: component #: model:ir.model,name:component.model_collection_base msgid "Base Abstract Collection" -msgstr "" +msgstr "Absctract Model inital pour une collection" #. module: component #: model:ir.model,name:component.model_component_builder msgid "Component Builder" -msgstr "" +msgstr "Constructeur de composants" #. module: component #: model:ir.model.fields,field_description:component.field_collection_base_display_name From 42987c9298cde588a447b02bc1192b25eb8f2c79 Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Thu, 1 Mar 2018 15:16:57 +0200 Subject: [PATCH 045/122] component: Add missing argument in logging call --- component/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component/core.py b/component/core.py index 6aa4acbc16..2e5d4538a8 100644 --- a/component/core.py +++ b/component/core.py @@ -250,7 +250,7 @@ def __init__(self, model_name=None, collection=None, _logger.error( 'No component registry for database %s. ' 'Probably because the Odoo registry has not been built ' - 'yet.' + 'yet.', dbname ) raise self._propagate_kwargs = [ From f56d216790a7bcbd1ce4f9ca24d2a29589f88ee6 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 4 Jun 2018 13:58:01 +0200 Subject: [PATCH 046/122] Add SavepointComponentRegistryCase --- component/tests/common.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/component/tests/common.py b/component/tests/common.py index 7a46218605..074ad91eb2 100644 --- a/component/tests/common.py +++ b/component/tests/common.py @@ -205,3 +205,19 @@ def setUp(self): def teardown(self): common.TransactionCase.tearDown(self) ComponentRegistryCase.tearDown(self) + + +class SavepointComponentRegistryCase(common.SavepointCase, + ComponentRegistryCase): + """ Adds Odoo Transaction with Savepoint in the base Component TestCase """ + + def setUp(self): + # resolve an inheritance issue (common.SavepointCase does not use + # super) + common.SavepointCase.setUp(self) + ComponentRegistryCase.setUp(self) + self.collection = self.env['collection.base'] + + def teardown(self): + common.SavepointCase.tearDown(self) + ComponentRegistryCase.tearDown(self) From c24f5721044892cd38e04d7d098e9136ff132dcc Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Mon, 4 Jun 2018 17:24:05 +0200 Subject: [PATCH 047/122] Exclude components created by tests from the list of addon's components 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. --- component/core.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/component/core.py b/component/core.py index 2e5d4538a8..a7d391bc2d 100644 --- a/component/core.py +++ b/component/core.py @@ -454,6 +454,17 @@ def __init__(self, name, bases, attrs): super(MetaComponent, self).__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 self.__module__.split('.'): + return + if not hasattr(self, '_module'): self._module = _get_addon_name(self.__module__) From f9e8c079f9f3f44ac4dcec17a8ae176f24e2f607 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Sat, 23 Jun 2018 00:39:50 +0000 Subject: [PATCH 048/122] Update component.pot --- component/i18n/am.po | 4 ++-- component/i18n/ca.po | 4 ++-- component/i18n/component.pot | 43 ++++++++++++++++++++++++++++++++++++ component/i18n/de.po | 4 ++-- component/i18n/el_GR.po | 7 +++--- component/i18n/es.po | 4 ++-- component/i18n/es_ES.po | 7 +++--- component/i18n/fi.po | 4 ++-- component/i18n/fr.po | 4 ++-- component/i18n/gl.po | 4 ++-- component/i18n/it.po | 4 ++-- component/i18n/pt.po | 4 ++-- component/i18n/pt_BR.po | 7 +++--- component/i18n/pt_PT.po | 7 +++--- component/i18n/sl.po | 7 +++--- component/i18n/tr.po | 4 ++-- 16 files changed, 83 insertions(+), 35 deletions(-) create mode 100644 component/i18n/component.pot diff --git a/component/i18n/am.po b/component/i18n/am.po index d8face26d7..34fd638a73 100644 --- a/component/i18n/am.po +++ b/component/i18n/am.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Amharic (https://www.transifex.com/oca/teams/23907/am/)\n" +"Language: am\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: am\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: component diff --git a/component/i18n/ca.po b/component/i18n/ca.po index d37b3ecdc1..86dd926fc0 100644 --- a/component/i18n/ca.po +++ b/component/i18n/ca.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Catalan (https://www.transifex.com/oca/teams/23907/ca/)\n" +"Language: ca\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: ca\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: component diff --git a/component/i18n/component.pot b/component/i18n/component.pot new file mode 100644 index 0000000000..4e7a7fa566 --- /dev/null +++ b/component/i18n/component.pot @@ -0,0 +1,43 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * component +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: component +#: model:ir.model,name:component.model_collection_base +msgid "Base Abstract Collection" +msgstr "" + +#. module: component +#: model:ir.model,name:component.model_component_builder +msgid "Component Builder" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_display_name +#: model:ir.model.fields,field_description:component.field_component_builder_display_name +msgid "Display Name" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base_id +#: model:ir.model.fields,field_description:component.field_component_builder_id +msgid "ID" +msgstr "" + +#. module: component +#: model:ir.model.fields,field_description:component.field_collection_base___last_update +#: model:ir.model.fields,field_description:component.field_component_builder___last_update +msgid "Last Modified on" +msgstr "" + diff --git a/component/i18n/de.po b/component/i18n/de.po index c0c62dc863..4cc636b3f7 100644 --- a/component/i18n/de.po +++ b/component/i18n/de.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: German (https://www.transifex.com/oca/teams/23907/de/)\n" +"Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: de\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: component diff --git a/component/i18n/el_GR.po b/component/i18n/el_GR.po index 8e467cc64c..343fdd6102 100644 --- a/component/i18n/el_GR.po +++ b/component/i18n/el_GR.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2018-01-05 16:56+0000\n" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" -"Language-Team: Greek (Greece) (https://www.transifex.com/oca/teams/23907/el_GR/)\n" +"Language-Team: Greek (Greece) (https://www.transifex.com/oca/teams/23907/" +"el_GR/)\n" +"Language: el_GR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: el_GR\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: component diff --git a/component/i18n/es.po b/component/i18n/es.po index dd05413fbe..f94ec0aa03 100644 --- a/component/i18n/es.po +++ b/component/i18n/es.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Spanish (https://www.transifex.com/oca/teams/23907/es/)\n" +"Language: es\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: component diff --git a/component/i18n/es_ES.po b/component/i18n/es_ES.po index 772dd10022..12117f5f17 100644 --- a/component/i18n/es_ES.po +++ b/component/i18n/es_ES.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2018-01-05 16:56+0000\n" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" -"Language-Team: Spanish (Spain) (https://www.transifex.com/oca/teams/23907/es_ES/)\n" +"Language-Team: Spanish (Spain) (https://www.transifex.com/oca/teams/23907/" +"es_ES/)\n" +"Language: es_ES\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: es_ES\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: component diff --git a/component/i18n/fi.po b/component/i18n/fi.po index d471af82dc..e0a955249b 100644 --- a/component/i18n/fi.po +++ b/component/i18n/fi.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Finnish (https://www.transifex.com/oca/teams/23907/fi/)\n" +"Language: fi\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: fi\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: component diff --git a/component/i18n/fr.po b/component/i18n/fr.po index d32daf9aa1..5be56f59fe 100644 --- a/component/i18n/fr.po +++ b/component/i18n/fr.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 # Nicolas JEUDY , 2018 @@ -13,10 +13,10 @@ msgstr "" "PO-Revision-Date: 2018-02-01 01:48+0000\n" "Last-Translator: Nicolas JEUDY , 2018\n" "Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: component diff --git a/component/i18n/gl.po b/component/i18n/gl.po index 0b4d7527b0..e0c5720b34 100644 --- a/component/i18n/gl.po +++ b/component/i18n/gl.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Galician (https://www.transifex.com/oca/teams/23907/gl/)\n" +"Language: gl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: gl\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: component diff --git a/component/i18n/it.po b/component/i18n/it.po index c6fc01029c..3496dd6c81 100644 --- a/component/i18n/it.po +++ b/component/i18n/it.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Italian (https://www.transifex.com/oca/teams/23907/it/)\n" +"Language: it\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: it\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: component diff --git a/component/i18n/pt.po b/component/i18n/pt.po index 3bd5803765..70a8d30ae1 100644 --- a/component/i18n/pt.po +++ b/component/i18n/pt.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Portuguese (https://www.transifex.com/oca/teams/23907/pt/)\n" +"Language: pt\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: pt\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: component diff --git a/component/i18n/pt_BR.po b/component/i18n/pt_BR.po index b1f1ff5d1b..8ab57dc7da 100644 --- a/component/i18n/pt_BR.po +++ b/component/i18n/pt_BR.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2018-01-05 16:56+0000\n" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" -"Language-Team: Portuguese (Brazil) (https://www.transifex.com/oca/teams/23907/pt_BR/)\n" +"Language-Team: Portuguese (Brazil) (https://www.transifex.com/oca/" +"teams/23907/pt_BR/)\n" +"Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: pt_BR\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: component diff --git a/component/i18n/pt_PT.po b/component/i18n/pt_PT.po index 435d542ffa..31a409fe96 100644 --- a/component/i18n/pt_PT.po +++ b/component/i18n/pt_PT.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -11,11 +11,12 @@ msgstr "" "POT-Creation-Date: 2018-01-05 16:56+0000\n" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" -"Language-Team: Portuguese (Portugal) (https://www.transifex.com/oca/teams/23907/pt_PT/)\n" +"Language-Team: Portuguese (Portugal) (https://www.transifex.com/oca/" +"teams/23907/pt_PT/)\n" +"Language: pt_PT\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: pt_PT\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #. module: component diff --git a/component/i18n/sl.po b/component/i18n/sl.po index eb7b7f11bc..0f513ad550 100644 --- a/component/i18n/sl.po +++ b/component/i18n/sl.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -12,11 +12,12 @@ msgstr "" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Slovenian (https://www.transifex.com/oca/teams/23907/sl/)\n" +"Language: sl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: sl\n" -"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n" +"%100==4 ? 2 : 3);\n" #. module: component #: model:ir.model,name:component.model_collection_base diff --git a/component/i18n/tr.po b/component/i18n/tr.po index 104255e727..be9deb85f1 100644 --- a/component/i18n/tr.po +++ b/component/i18n/tr.po @@ -1,7 +1,7 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: # * component -# +# # Translators: # OCA Transbot , 2018 msgid "" @@ -12,10 +12,10 @@ msgstr "" "PO-Revision-Date: 2018-01-05 16:56+0000\n" "Last-Translator: OCA Transbot , 2018\n" "Language-Team: Turkish (https://www.transifex.com/oca/teams/23907/tr/)\n" +"Language: tr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Language: tr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #. module: component From 2e32e52aaa07b0aa6674823ca7c0272dae3fb1d7 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Wed, 27 Jun 2018 06:40:03 +0000 Subject: [PATCH 049/122] Translated using Weblate (French) Currently translated at 100.0% (5 of 5 strings) Translation: connector-11.0/connector-11.0-component Translate-URL: https://translation.odoo-community.org/projects/connector-11-0/connector-11-0-component/fr/ --- component/i18n/fr.po | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/component/i18n/fr.po b/component/i18n/fr.po index 5be56f59fe..5f14280205 100644 --- a/component/i18n/fr.po +++ b/component/i18n/fr.po @@ -10,19 +10,20 @@ msgstr "" "Project-Id-Version: Odoo Server 11.0\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2018-02-01 01:48+0000\n" -"PO-Revision-Date: 2018-02-01 01:48+0000\n" -"Last-Translator: Nicolas JEUDY , 2018\n" +"PO-Revision-Date: 2018-06-28 07:13+0000\n" +"Last-Translator: Guewen Baconnier \n" "Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: \n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 3.0.1\n" #. module: component #: model:ir.model,name:component.model_collection_base msgid "Base Abstract Collection" -msgstr "Absctract Model inital pour une collection" +msgstr "Abstract Model inital pour une collection" #. module: component #: model:ir.model,name:component.model_component_builder From bc2a9b913e25a0e29bd763a55e370d6735f40252 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Fri, 30 Mar 2018 09:22:09 +0200 Subject: [PATCH 050/122] Improve documentation of APIs * Sphinx crashes with a recursion error when trying to document Models fields. Remove the fields from the documentation altogether, they didn't bring much value anyway * Gone through all the API pages and removed many useless autodoc with a mix of __all__, :exclude-members: or by defining more precisely what we want with :autoclass:, :autoatttribute:, ... or simply removing them * Changed the order of a few autodocs by putting the most useful classes/functions first * Improved a few docstrings --- component/core.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/component/core.py b/component/core.py index a7d391bc2d..9cd5308ced 100644 --- a/component/core.py +++ b/component/core.py @@ -639,15 +639,19 @@ def vocalize(action, message): _abstract = True # used for inheritance - _name = None + _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 + #: name of the collection to subscribe in _collection = None - # None means any Model, can be a list ['res.users', ...] + #: 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', ...) + + #: Component purpose ('import.mapper', ...). _usage = None def __init__(self, work_context): From e76a6a5ee10f2598f7345e15af0db631009f079e Mon Sep 17 00:00:00 2001 From: OCA git bot Date: Thu, 27 Sep 2018 01:59:42 +0200 Subject: [PATCH 051/122] Make modules uninstallable --- component/__manifest__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component/__manifest__.py b/component/__manifest__.py index e06abb5eae..e79e63bfba 100644 --- a/component/__manifest__.py +++ b/component/__manifest__.py @@ -15,5 +15,5 @@ 'python': ['cachetools'], }, 'data': [], - 'installable': True, + 'installable': False, } From fe9ab7f41bb12050a92f97c049a691e804e8f5c8 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 2 Oct 2018 15:24:01 +0200 Subject: [PATCH 052/122] Migrate component to version 12.0 --- component/README.rst | 91 +------------------------------ component/__manifest__.py | 7 ++- component/readme/CONTRIBUTORS.rst | 2 + component/readme/DESCRIPTION.rst | 4 ++ component/readme/HISTORY.rst | 17 ++++++ component/readme/USAGE.rst | 23 ++++++++ 6 files changed, 53 insertions(+), 91 deletions(-) create mode 100644 component/readme/CONTRIBUTORS.rst create mode 100644 component/readme/DESCRIPTION.rst create mode 100644 component/readme/HISTORY.rst create mode 100644 component/readme/USAGE.rst diff --git a/component/README.rst b/component/README.rst index 9a6a40889e..7538a79c52 100644 --- a/component/README.rst +++ b/component/README.rst @@ -1,90 +1,3 @@ -.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg - :target: http://www.gnu.org/licenses/agpl - :alt: License: AGPL-3 +**This file is going to be generated by oca-gen-addon-readme.** -========== -Components -========== - -This module implements a component system and is a base block for the Connector -Framework. It can be used without using the full Connector though. - -Documentation: http://odoo-connector.com/ - -Installation -============ - -* Install ``component`` - -Configuration -============= - -The module does nothing by itself and has no configuration. - -Usage -===== - -As a developer, you have access to a component system. You can find the -documentation in the code or on http://odoo-connector.com - -In a nutshell, you can create components:: - - - from odoo.addons.component.core import Component - - class MagentoPartnerAdapter(Component): - _name = 'magento.partner.adapter' - _inherit = 'magento.adapter' - - _usage = 'backend.adapter' - _collection = 'magento.backend' - _apply_on = ['res.partner'] - -And later, find the component you need at runtime (dynamic dispatch at -component level):: - - def run(self, external_id): - backend_adapter = self.component(usage='backend.adapter') - external_data = backend_adapter.read(external_id) - - -Known issues / Roadmap -====================== - -Bug Tracker -=========== - -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smash it by providing detailed and welcomed feedback. - -Credits -======= - -Images ------- - -* Odoo Community Association: `Icon `_. - -Contributors ------------- - -* Guewen Baconnier - -Do not contact contributors directly about support or help with technical issues. - -Maintainer ----------- - -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -This module is maintained by the OCA. - -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. - -To contribute to this module, please visit https://odoo-community.org. +*Manual changes will be overwritten.* diff --git a/component/__manifest__.py b/component/__manifest__.py index e79e63bfba..10c609b94d 100644 --- a/component/__manifest__.py +++ b/component/__manifest__.py @@ -3,7 +3,9 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) {'name': 'Components', - 'version': '11.0.1.1.0', + 'summary': 'Add capabilities to register and use decoupled components,' + ' as an alternative to model classes', + 'version': '12.0.1.0.0', 'author': 'Camptocamp,' 'Odoo Community Association (OCA)', 'website': 'https://www.camptocamp.com', @@ -15,5 +17,6 @@ 'python': ['cachetools'], }, 'data': [], - 'installable': False, + 'installable': True, + 'maintainers': ['guewen'], } diff --git a/component/readme/CONTRIBUTORS.rst b/component/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..60a076dc17 --- /dev/null +++ b/component/readme/CONTRIBUTORS.rst @@ -0,0 +1,2 @@ +* Guewen Baconnier +* Laurent Mignon diff --git a/component/readme/DESCRIPTION.rst b/component/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..db731f8d6e --- /dev/null +++ b/component/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +This module implements a component system and is a base block for the Connector +Framework. It can be used without using the full Connector though. + +Documentation: http://odoo-connector.com/ diff --git a/component/readme/HISTORY.rst b/component/readme/HISTORY.rst new file mode 100644 index 0000000000..59a390d207 --- /dev/null +++ b/component/readme/HISTORY.rst @@ -0,0 +1,17 @@ +.. [ The change log. The goal of this file is to help readers + understand changes between version. The primary audience is + end users and integrators. Purely technical changes such as + code refactoring must not be mentioned here. + + This file may contain ONE level of section titles, underlined + with the ~ (tilde) character. Other section markers are + forbidden and will likely break the structure of the README.rst + or other documents where this fragment is included. ] + +Next +~~~~ + +12.0.1.0.0 (2018-10-02) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [MIGRATION] from 11.0 branched at rev. 324e006 diff --git a/component/readme/USAGE.rst b/component/readme/USAGE.rst new file mode 100644 index 0000000000..371a2570b0 --- /dev/null +++ b/component/readme/USAGE.rst @@ -0,0 +1,23 @@ +As a developer, you have access to a component system. You can find the +documentation in the code or on http://odoo-connector.com + +In a nutshell, you can create components:: + + + from odoo.addons.component.core import Component + + class MagentoPartnerAdapter(Component): + _name = 'magento.partner.adapter' + _inherit = 'magento.adapter' + + _usage = 'backend.adapter' + _collection = 'magento.backend' + _apply_on = ['res.partner'] + +And later, find the component you need at runtime (dynamic dispatch at +component level):: + + def run(self, external_id): + backend_adapter = self.component(usage='backend.adapter') + external_data = backend_adapter.read(external_id) + From 4db7bc617d07aeabadd7c2edd6c4fcd3dc5ff331 Mon Sep 17 00:00:00 2001 From: Guewen Baconnier Date: Tue, 2 Oct 2018 15:43:44 +0200 Subject: [PATCH 053/122] Add OCA development status --- component/__manifest__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/component/__manifest__.py b/component/__manifest__.py index 10c609b94d..55e51e2d25 100644 --- a/component/__manifest__.py +++ b/component/__manifest__.py @@ -8,7 +8,7 @@ 'version': '12.0.1.0.0', 'author': 'Camptocamp,' 'Odoo Community Association (OCA)', - 'website': 'https://www.camptocamp.com', + 'website': 'https://github.com/OCA/connector', 'license': 'AGPL-3', 'category': 'Generic Modules', 'depends': ['base', @@ -16,7 +16,7 @@ 'external_dependencies': { 'python': ['cachetools'], }, - 'data': [], 'installable': True, + 'development_status': 'Stable', 'maintainers': ['guewen'], } From 1632446221a203e2d2dfafaed2b885c9097b8e23 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Fri, 9 Nov 2018 11:02:21 +0000 Subject: [PATCH 054/122] README.rst --- component/README.rst | 131 ++++++- component/static/description/index.html | 475 ++++++++++++++++++++++++ 2 files changed, 604 insertions(+), 2 deletions(-) create mode 100644 component/static/description/index.html diff --git a/component/README.rst b/component/README.rst index 7538a79c52..5365bba2cf 100644 --- a/component/README.rst +++ b/component/README.rst @@ -1,3 +1,130 @@ -**This file is going to be generated by oca-gen-addon-readme.** +========== +Components +========== -*Manual changes will be overwritten.* +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge2| image:: https://img.shields.io/badge/github-OCA%2Fconnector-lightgray.png?logo=github + :target: https://github.com/OCA/connector/tree/12.0/component + :alt: OCA/connector +.. |badge3| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/connector-12-0/connector-12-0-component + :alt: Translate me on Weblate +.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/102/12.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| + +This module implements a component system and is a base block for the Connector +Framework. It can be used without using the full Connector though. + +Documentation: http://odoo-connector.com/ + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +As a developer, you have access to a component system. You can find the +documentation in the code or on http://odoo-connector.com + +In a nutshell, you can create components:: + + + from odoo.addons.component.core import Component + + class MagentoPartnerAdapter(Component): + _name = 'magento.partner.adapter' + _inherit = 'magento.adapter' + + _usage = 'backend.adapter' + _collection = 'magento.backend' + _apply_on = ['res.partner'] + +And later, find the component you need at runtime (dynamic dispatch at +component level):: + + def run(self, external_id): + backend_adapter = self.component(usage='backend.adapter') + external_data = backend_adapter.read(external_id) + + +Changelog +========= + +.. [ The change log. The goal of this file is to help readers + understand changes between version. The primary audience is + end users and integrators. Purely technical changes such as + code refactoring must not be mentioned here. + + This file may contain ONE level of section titles, underlined + with the ~ (tilde) character. Other section markers are + forbidden and will likely break the structure of the README.rst + or other documents where this fragment is included. ] + +Next +~~~~ + +12.0.1.0.0 (2018-10-02) +~~~~~~~~~~~~~~~~~~~~~~~ + +* [MIGRATION] from 11.0 branched at rev. 324e006 + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Camptocamp + +Contributors +~~~~~~~~~~~~ + +* Guewen Baconnier +* Laurent Mignon + +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-guewen| image:: https://github.com/guewen.png?size=40px + :target: https://github.com/guewen + :alt: guewen + +Current `maintainer `__: + +|maintainer-guewen| + +This module is part of the `OCA/connector `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/component/static/description/index.html b/component/static/description/index.html new file mode 100644 index 0000000000..8f4c302c69 --- /dev/null +++ b/component/static/description/index.html @@ -0,0 +1,475 @@ + + + + + + +Components + + + +
+

Components

+ + +

License: AGPL-3 OCA/connector Translate me on Weblate Try me on Runbot

+

This module implements a component system and is a base block for the Connector +Framework. It can be used without using the full Connector though.

+

Documentation: http://odoo-connector.com/

+

Table of contents

+ +
+

Usage

+

As a developer, you have access to a component system. You can find the +documentation in the code or on http://odoo-connector.com

+

In a nutshell, you can create components:

+
+from odoo.addons.component.core import Component
+
+class MagentoPartnerAdapter(Component):
+    _name = 'magento.partner.adapter'
+    _inherit = 'magento.adapter'
+
+    _usage = 'backend.adapter'
+    _collection = 'magento.backend'
+    _apply_on = ['res.partner']
+
+

And later, find the component you need at runtime (dynamic dispatch at +component level):

+
+def run(self, external_id):
+    backend_adapter = self.component(usage='backend.adapter')
+    external_data = backend_adapter.read(external_id)
+
+
+
+

Changelog

+ + +
+

12.0.1.0.0 (2018-10-02)

+
    +
  • [MIGRATION] from 11.0 branched at rev. 324e006
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

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

+
+
+

Credits

+
+

Authors

+
    +
  • Camptocamp
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainer:

+

guewen

+

This module is part of the OCA/connector project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + From 8a09e2695efb54b2d6023202788e11c510ab9a21 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Fri, 9 Nov 2018 11:11:13 +0000 Subject: [PATCH 055/122] Update component.pot --- component/i18n/component.pot | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/component/i18n/component.pot b/component/i18n/component.pot index 4e7a7fa566..74b8a417a8 100644 --- a/component/i18n/component.pot +++ b/component/i18n/component.pot @@ -4,7 +4,7 @@ # msgid "" msgstr "" -"Project-Id-Version: Odoo Server 11.0\n" +"Project-Id-Version: Odoo Server 12.0\n" "Report-Msgid-Bugs-To: \n" "Last-Translator: <>\n" "Language-Team: \n" @@ -24,20 +24,20 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "" From 137c97109f38672d4429583b88de98af33410c4d Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Sun, 9 Dec 2018 11:53:37 +0000 Subject: [PATCH 056/122] Update translation files Updated by Update PO files to match POT (msgmerge) hook in Weblate. --- component/i18n/am.po | 12 ++++++------ component/i18n/ca.po | 12 ++++++------ component/i18n/de.po | 12 ++++++------ component/i18n/el_GR.po | 12 ++++++------ component/i18n/es.po | 12 ++++++------ component/i18n/es_ES.po | 12 ++++++------ component/i18n/fi.po | 12 ++++++------ component/i18n/fr.po | 12 ++++++------ component/i18n/gl.po | 12 ++++++------ component/i18n/it.po | 12 ++++++------ component/i18n/pt.po | 12 ++++++------ component/i18n/pt_BR.po | 12 ++++++------ component/i18n/pt_PT.po | 12 ++++++------ component/i18n/sl.po | 12 ++++++------ component/i18n/tr.po | 12 ++++++------ 15 files changed, 90 insertions(+), 90 deletions(-) diff --git a/component/i18n/am.po b/component/i18n/am.po index 34fd638a73..4982064d37 100644 --- a/component/i18n/am.po +++ b/component/i18n/am.po @@ -29,19 +29,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "" diff --git a/component/i18n/ca.po b/component/i18n/ca.po index 86dd926fc0..97d71ea347 100644 --- a/component/i18n/ca.po +++ b/component/i18n/ca.po @@ -29,19 +29,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "" diff --git a/component/i18n/de.po b/component/i18n/de.po index 4cc636b3f7..66887f2e1f 100644 --- a/component/i18n/de.po +++ b/component/i18n/de.po @@ -29,19 +29,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "Anzeigebezeichnung" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "Zuletzt aktualisiert am" diff --git a/component/i18n/el_GR.po b/component/i18n/el_GR.po index 343fdd6102..8dd01da331 100644 --- a/component/i18n/el_GR.po +++ b/component/i18n/el_GR.po @@ -30,19 +30,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "Κωδικός" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "" diff --git a/component/i18n/es.po b/component/i18n/es.po index f94ec0aa03..3f979dc254 100644 --- a/component/i18n/es.po +++ b/component/i18n/es.po @@ -29,19 +29,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "Nombre mostrado" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "Última modificación el" diff --git a/component/i18n/es_ES.po b/component/i18n/es_ES.po index 12117f5f17..ed5a9b2aaf 100644 --- a/component/i18n/es_ES.po +++ b/component/i18n/es_ES.po @@ -30,19 +30,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "" diff --git a/component/i18n/fi.po b/component/i18n/fi.po index e0a955249b..7130d1555a 100644 --- a/component/i18n/fi.po +++ b/component/i18n/fi.po @@ -29,19 +29,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "Nimi" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "Viimeksi muokattu" diff --git a/component/i18n/fr.po b/component/i18n/fr.po index 5f14280205..4f9ada5135 100644 --- a/component/i18n/fr.po +++ b/component/i18n/fr.po @@ -31,19 +31,19 @@ msgid "Component Builder" msgstr "Constructeur de composants" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "Nom affiché" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "Dernière modification le" diff --git a/component/i18n/gl.po b/component/i18n/gl.po index e0c5720b34..56242830c8 100644 --- a/component/i18n/gl.po +++ b/component/i18n/gl.po @@ -29,19 +29,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "" diff --git a/component/i18n/it.po b/component/i18n/it.po index 3496dd6c81..872b5d2910 100644 --- a/component/i18n/it.po +++ b/component/i18n/it.po @@ -29,19 +29,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "Nome da visualizzare" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "Ultima modifica il" diff --git a/component/i18n/pt.po b/component/i18n/pt.po index 70a8d30ae1..079342f9e7 100644 --- a/component/i18n/pt.po +++ b/component/i18n/pt.po @@ -29,19 +29,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "" diff --git a/component/i18n/pt_BR.po b/component/i18n/pt_BR.po index 8ab57dc7da..35aeaf44f3 100644 --- a/component/i18n/pt_BR.po +++ b/component/i18n/pt_BR.po @@ -30,19 +30,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "" diff --git a/component/i18n/pt_PT.po b/component/i18n/pt_PT.po index 31a409fe96..f59f1d03de 100644 --- a/component/i18n/pt_PT.po +++ b/component/i18n/pt_PT.po @@ -30,19 +30,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "" diff --git a/component/i18n/sl.po b/component/i18n/sl.po index 0f513ad550..b231dd12ef 100644 --- a/component/i18n/sl.po +++ b/component/i18n/sl.po @@ -30,19 +30,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "Prikazni naziv" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "Zadnjič spremenjeno" diff --git a/component/i18n/tr.po b/component/i18n/tr.po index be9deb85f1..77d7d74a23 100644 --- a/component/i18n/tr.po +++ b/component/i18n/tr.po @@ -29,19 +29,19 @@ msgid "Component Builder" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_display_name -#: model:ir.model.fields,field_description:component.field_component_builder_display_name +#: model:ir.model.fields,field_description:component.field_collection_base__display_name +#: model:ir.model.fields,field_description:component.field_component_builder__display_name msgid "Display Name" msgstr "" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base_id -#: model:ir.model.fields,field_description:component.field_component_builder_id +#: model:ir.model.fields,field_description:component.field_collection_base__id +#: model:ir.model.fields,field_description:component.field_component_builder__id msgid "ID" msgstr "ID" #. module: component -#: model:ir.model.fields,field_description:component.field_collection_base___last_update -#: model:ir.model.fields,field_description:component.field_component_builder___last_update +#: model:ir.model.fields,field_description:component.field_collection_base____last_update +#: model:ir.model.fields,field_description:component.field_component_builder____last_update msgid "Last Modified on" msgstr "" From dd60588346d610f93a212c81801a10bd45f11edc Mon Sep 17 00:00:00 2001 From: Naglis Jonaitis Date: Tue, 19 Mar 2019 21:55:27 +0200 Subject: [PATCH 057/122] component, component_event: tag unittest.TestCase subclasses Since v12.0, direct subclasses of `unittest.TestCase` which do not have :class:`odoo.tests.common.MetaCase` as a meta-class must be explicitly tagged with :method:`odoo.tests.common.tagged`, otherwise they will not be picked up by the test runner. E.g.: ```python @tagged('standard', 'at_install') ``` --- component/tests/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/component/tests/common.py b/component/tests/common.py index 074ad91eb2..99a4a7b7ca 100644 --- a/component/tests/common.py +++ b/component/tests/common.py @@ -99,7 +99,8 @@ def setUp(self): ComponentMixin.setUp(self) -class ComponentRegistryCase(unittest.TestCase): +class ComponentRegistryCase( + unittest.TestCase, common.MetaCase('DummyCase', (object,), {})): """ This test case can be used as a base for writings tests on components This test case is meant to test components in a special component registry, From 8c30b23ec608be894b9e2e864898a41c1341948b Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 3 Apr 2019 02:39:17 +0000 Subject: [PATCH 058/122] icon.png --- component/static/description/icon.png | Bin 0 -> 9455 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 component/static/description/icon.png diff --git a/component/static/description/icon.png b/component/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 zcmW++2RxMjAAjx~&dlBk9S+%}OXg)AGE&Cb*&}d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~! zVpnB`o+K7|Al`Q_U;eD$B zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__ zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_ zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)( z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9 zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz# z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K= z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C zuVl&0duN<;uOsB3%T9Fp8t{ED108<+W(nOZd?gDnfNBC3>M8WE61$So|P zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1 zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_ zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8 zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ> zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD z#z-)AXwSRY?OPefw^iI+ z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$ z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6 zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+ z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC) zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x! zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8 z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n= z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@ zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y< zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6 zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6% z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(| z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6 z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d} z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB z z?n?eq1XP>p-IM$Z^C;2L3itnbJZAip*Zo0aw2bs8@(s^~*8T9go!%dHcAz2lM;`yp zD=7&xjFV$S&5uDaiScyD?B-i1ze`+CoRtz`Wn+Zl&#s4&}MO{@N!ufrzjG$B79)Y2d3tBk&)TxUTw@QS0TEL_?njX|@vq?Uz(nBFK5Pq7*xj#u*R&i|?7+6# z+|r_n#SW&LXhtheZdah{ZVoqwyT{D>MC3nkFF#N)xLi{p7J1jXlmVeb;cP5?e(=f# zuT7fvjSbjS781v?7{)-X3*?>tq?)Yd)~|1{BDS(pqC zC}~H#WXlkUW*H5CDOo<)#x7%RY)A;ShGhI5s*#cRDA8YgqG(HeKDx+#(ZQ?386dv! zlXCO)w91~Vw4AmOcATuV653fa9R$fyK8ul%rG z-wfS zihugoZyr38Im?Zuh6@RcF~t1anQu7>#lPpb#}4cOA!EM11`%f*07RqOVkmX{p~KJ9 z^zP;K#|)$`^Rb{rnHGH{~>1(fawV0*Z#)}M`m8-?ZJV<+e}s9wE# z)l&az?w^5{)`S(%MRzxdNqrs1n*-=jS^_jqE*5XDrA0+VE`5^*p3CuM<&dZEeCjoz zR;uu_H9ZPZV|fQq`Cyw4nscrVwi!fE6ciMmX$!_hN7uF;jjKG)d2@aC4ropY)8etW=xJvni)8eHi`H$%#zn^WJ5NLc-rqk|u&&4Z6fD_m&JfSI1Bvb?b<*n&sfl0^t z=HnmRl`XrFvMKB%9}>PaA`m-fK6a0(8=qPkWS5bb4=v?XcWi&hRY?O5HdulRi4?fN zlsJ*N-0Qw+Yic@s0(2uy%F@ib;GjXt01Fmx5XbRo6+n|pP(&nodMoap^z{~q ziEeaUT@Mxe3vJSfI6?uLND(CNr=#^W<1b}jzW58bIfyWTDle$mmS(|x-0|2UlX+9k zQ^EX7Nw}?EzVoBfT(-LT|=9N@^hcn-_p&sqG z&*oVs2JSU+N4ZD`FhCAWaS;>|wH2G*Id|?pa#@>tyxX`+4HyIArWDvVrX)2WAOQff z0qyHu&-S@i^MS-+j--!pr4fPBj~_8({~e1bfcl0wI1kaoN>mJL6KUPQm5N7lB(ui1 zE-o%kq)&djzWJ}ob<-GfDlkB;F31j-VHKvQUGQ3sp`CwyGJk_i!y^sD0fqC@$9|jO zOqN!r!8-p==F@ZVP=U$qSpY(gQ0)59P1&t@y?5rvg<}E+GB}26NYPp4f2YFQrQtot5mn3wu_qprZ=>Ig-$ zbW26Ws~IgY>}^5w`vTB(G`PTZaDiGBo5o(tp)qli|NeV( z@H_=R8V39rt5J5YB2Ky?4eJJ#b`_iBe2ot~6%7mLt5t8Vwi^Jy7|jWXqa3amOIoRb zOr}WVFP--DsS`1WpN%~)t3R!arKF^Q$e12KEqU36AWwnCBICpH4XCsfnyrHr>$I$4 z!DpKX$OKLWarN7nv@!uIA+~RNO)l$$w}p(;b>mx8pwYvu;dD_unryX_NhT8*Tj>BTrTTL&!?O+%Rv;b?B??gSzdp?6Uug9{ zd@V08Z$BdI?fpoCS$)t4mg4rT8Q_I}h`0d-vYZ^|dOB*Q^S|xqTV*vIg?@fVFSmMpaw0qtTRbx} z({Pg?#{2`sc9)M5N$*N|4;^t$+QP?#mov zGVC@I*lBVrOU-%2y!7%)fAKjpEFsgQc4{amtiHb95KQEwvf<(3T<9-Zm$xIew#P22 zc2Ix|App^>v6(3L_MCU0d3W##AB0M~3D00EWoKZqsJYT(#@w$Y_H7G22M~ApVFTRHMI_3be)Lkn#0F*V8Pq zc}`Cjy$bE;FJ6H7p=0y#R>`}-m4(0F>%@P|?7fx{=R^uFdISRnZ2W_xQhD{YuR3t< z{6yxu=4~JkeA;|(J6_nv#>Nvs&FuLA&PW^he@t(UwFFE8)|a!R{`E`K`i^ZnyE4$k z;(749Ix|oi$c3QbEJ3b~D_kQsPz~fIUKym($a_7dJ?o+40*OLl^{=&oq$<#Q(yyrp z{J-FAniyAw9tPbe&IhQ|a`DqFTVQGQ&Gq3!C2==4x{6EJwiPZ8zub-iXoUtkJiG{} zPaR&}_fn8_z~(=;5lD-aPWD3z8PZS@AaUiomF!G8I}Mf>e~0g#BelA-5#`cj;O5>N Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 From 6b155caa22a44330e20b54cc5071ba408c71355d Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 29 Jul 2019 02:44:06 +0000 Subject: [PATCH 059/122] README.rst --- component/static/description/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/component/static/description/index.html b/component/static/description/index.html index 8f4c302c69..1f503a8b2f 100644 --- a/component/static/description/index.html +++ b/component/static/description/index.html @@ -3,7 +3,7 @@ - + Components -
-

Components

+
+ + +Odoo Community Association + +
+

Components

-

Production/Stable License: LGPL-3 OCA/connector Translate me on Weblate Try me on Runboat

+

Production/Stable License: LGPL-3 OCA/connector Translate me on Weblate Try me on Runboat

This module implements a component system and is a base block for the Connector Framework. It can be used without using the full Connector though.

@@ -393,14 +398,13 @@

Components

  • Credits
  • -

    Usage

    +

    Usage

    As a developer, you have access to a component system. You can find the documentation in the code or on http://odoo-connector.com

    In a nutshell, you can create components:

    @@ -432,56 +436,56 @@

    Usage

    docstrings in tests/common.py.

    -

    Bug Tracker

    +

    Bug Tracker

    Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us to smash it by providing a detailed and welcomed -feedback.

    +feedback.

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

    -

    Credits

    +

    Credits

    -

    Authors

    +

    Authors

    • Camptocamp
    -

    Contributors

    +

    Contributors

    -
    -

    Other credits

    -

    The migration of this module from 17.0 to 18.0 was financially supported -by Camptocamp.

    -
    -

    Maintainers

    +

    Maintainers

    This module is maintained by the OCA.

    Odoo Community Association @@ -505,10 +504,11 @@

    Maintainers

    promote its widespread use.

    Current maintainer:

    guewen

    -

    This module is part of the OCA/connector project on GitHub.

    +

    This module is part of the OCA/connector project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    +
    diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..d3dfeea70b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# generated from manifests external_dependencies +cachetools From 807e0e122d65a4d3a7c7304be4c0fd84331c33c7 Mon Sep 17 00:00:00 2001 From: Sergij Pfaifer Date: Thu, 12 Mar 2026 10:53:04 +0200 Subject: [PATCH 122/122] [MIG] component: Migration to 19.0 --- component/builder.py | 11 +++++------ component/tests/common.py | 22 ++++++++++++++-------- component/tests/test_component.py | 7 ++----- component/tests/test_work_on.py | 1 + 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/component/builder.py b/component/builder.py index 913739159a..8ec8a1cfbd 100644 --- a/component/builder.py +++ b/component/builder.py @@ -10,8 +10,8 @@ """ -import odoo from odoo import models +from odoo.modules.module_graph import ModuleGraph from .core import DEFAULT_CACHE_SIZE, ComponentRegistry, _component_databases @@ -63,18 +63,17 @@ def build_registry(self, components_registry, states=None, exclude_addons=None): # 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 " + 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) + module_list = [name for (name,) in self.env.cr.fetchall()] + graph = ModuleGraph(self.env.cr) + graph.extend(module_list) for module in graph: self.load_components(module.name, components_registry=components_registry) diff --git a/component/tests/common.py b/component/tests/common.py index f7cf5d1fe1..1b264b7b2c 100644 --- a/component/tests/common.py +++ b/component/tests/common.py @@ -14,7 +14,7 @@ @contextmanager def new_rollbacked_env(): registry = odoo.modules.registry.Registry(common.get_db_name()) - uid = odoo.SUPERUSER_ID + uid = api.SUPERUSER_ID cr = registry.cursor() try: yield api.Environment(cr, uid, {}) @@ -40,8 +40,10 @@ def setUpComponent(cls): current_addon = _get_addon_name(cls.__module__) env["component.builder"].load_components(current_addon) if hasattr(cls, "env"): - cls.env.context = dict( - cls.env.context, components_registry=cls._components_registry + cls.env = cls.env( + context=dict( + cls.env.context, components_registry=cls._components_registry + ), ) # pylint: disable=W8106 @@ -76,8 +78,10 @@ def setUp(self): common.TransactionCase.setUp(self) ComponentMixin.setUp(self) # There's no env on setUpClass of TransactionCase, must do it here. - self.env.context = dict( - self.env.context, components_registry=self._components_registry + self.env = self.env( + context=dict( + self.env.context, components_registry=self._components_registry + ), ) @@ -159,9 +163,11 @@ def _setup_registry(class_or_instance): class_or_instance.comp_registry.ready = True if hasattr(class_or_instance, "env"): # let it propagate via ctx - class_or_instance.env.context = dict( - class_or_instance.env.context, - components_registry=class_or_instance.comp_registry, + class_or_instance.env = class_or_instance.env( + context=dict( + class_or_instance.env.context, + components_registry=class_or_instance.comp_registry, + ), ) @staticmethod diff --git a/component/tests/test_component.py b/component/tests/test_component.py index 69f168957a..f1eb363d8a 100644 --- a/component/tests/test_component.py +++ b/component/tests/test_component.py @@ -50,7 +50,7 @@ class Component2(Component): # our collection, in a less abstract use case, it # could be a record of 'magento.backend' for instance - self.collection_record = self.collection.new() + self.collection_record = self.env[self.collection._name].new() @contextmanager def get_base(): @@ -110,10 +110,7 @@ def test_component_get_by_name_other_model(self): def test_component_get_by_name_wrong_model(self): """Use component_by_name with a model not in _apply_on""" - msg = ( - "Component with name 'component2' can't be used " - "for model 'res.partner'.*" - ) + msg = "Component with name 'component2' can't be used for model 'res.partner'.*" with self.get_base() as base: with self.assertRaisesRegex(NoComponentError, msg): # we ask for the model 'component2' but we are working diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py index 2004ba7e56..90d5bcf2dd 100644 --- a/component/tests/test_work_on.py +++ b/component/tests/test_work_on.py @@ -17,6 +17,7 @@ class TestWorkOn(TransactionComponentRegistryCase): def setUp(self): super().setUp() self._setup_registry(self) + self.collection = self.env[self.collection._name] def tearDown(self): self._teardown_registry(self)