From 9ccbb25c43ba3a7fb682a5d273d8e478af618fe4 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Sun, 15 Jan 2017 22:24:44 +0100
Subject: [PATCH 001/100] 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 816dc0466f049a99abf7e98b29c273730119b15e Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Sun, 15 Jan 2017 22:50:24 +0100
Subject: [PATCH 002/100] 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 52e24d3b6a4b00d37a52fa8536e73d18eb6e7a99 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Thu, 19 Jan 2017 21:40:38 +0100
Subject: [PATCH 003/100] 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 3373aa15f0df96293ea742183d10ff7668b234c6 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Mon, 12 Jun 2017 15:13:26 +0200
Subject: [PATCH 004/100] 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 d4c6c47dd775f2f9668f4c4c588b250133858313 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 13 Jun 2017 13:10:14 +0200
Subject: [PATCH 005/100] 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 343676262cd40b45f1319366d6c3c3e64be027d4 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 13 Jun 2017 13:40:46 +0200
Subject: [PATCH 006/100] 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 87764ec32cf0fd7a25f81a94459d7539229e5bb1 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 13 Jun 2017 16:23:22 +0200
Subject: [PATCH 007/100] 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 18c838317530f8233434060b11efa1577341e30a Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 13 Jun 2017 17:09:01 +0200
Subject: [PATCH 008/100] 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 c8ced3afb247a72c985ff48a75183d85ac7ff257 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Wed, 14 Jun 2017 10:02:08 +0200
Subject: [PATCH 009/100] 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 3ec5416d44b50d2d9e79b995e3e1c2046a45480c Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Wed, 14 Jun 2017 12:30:00 +0200
Subject: [PATCH 010/100] 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 d19a40485f40821d25940cafdfce100a17ab0d2e Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Wed, 14 Jun 2017 22:36:15 +0200
Subject: [PATCH 011/100] 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 6bfe808174cba8129ee4b682ef4c3878755aaa5c Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Thu, 15 Jun 2017 11:23:24 +0200
Subject: [PATCH 012/100] 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 e49be674bb593ed79d35a9321a190c74711110a2 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Thu, 15 Jun 2017 16:06:17 +0200
Subject: [PATCH 013/100] 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 1fbc821aa021a01a9871f62ef1df746b6a453150 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Thu, 15 Jun 2017 21:18:53 +0200
Subject: [PATCH 014/100] 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 db24242e7db1a2b10130d5aca3cafc0410e61a0e Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Fri, 16 Jun 2017 12:29:17 +0200
Subject: [PATCH 015/100] 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 d5a311371aae53aa37204cff245032a37e4c4de0 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Sat, 17 Jun 2017 11:35:28 +0200
Subject: [PATCH 016/100] 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 02785811f64b93c982b9099bb1f9ac7f7e858b90 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Sat, 17 Jun 2017 22:20:49 +0200
Subject: [PATCH 017/100] 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 dda869403a477891c41ce2faf456ccfcc3a88201 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Sat, 17 Jun 2017 23:10:13 +0200
Subject: [PATCH 018/100] 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 c5bf7905ee58e5e87913e3fa79e6340a6c8f6f89 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Sat, 17 Jun 2017 23:48:33 +0200
Subject: [PATCH 019/100] 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 59f06be9dbd3d0a718bc72de9453dfc4ee8c7f4e Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Mon, 19 Jun 2017 11:44:56 +0200
Subject: [PATCH 020/100] 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 ad3d04951091e1b09b1061742ed6c68a9e1eec48 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Mon, 19 Jun 2017 15:23:42 +0200
Subject: [PATCH 021/100] 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 6db23e5fe99577aaa434f49f01294e1c4415e565 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Mon, 19 Jun 2017 17:14:11 +0200
Subject: [PATCH 022/100] 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 72c602921cfb5e42be0dcc94dc8fecc07045c428 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 20 Jun 2017 09:35:09 +0200
Subject: [PATCH 023/100] 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 cb50e907fb27261603b662a072ff8718bdae589d Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 20 Jun 2017 09:58:33 +0200
Subject: [PATCH 024/100] 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 9565eb1de8c32e1c79c43230b94690157571d5c6 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 20 Jun 2017 13:48:19 +0200
Subject: [PATCH 025/100] 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 76f81d494cc2eb2b0fc285d8a41ac99e414ef028 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Thu, 22 Jun 2017 21:58:10 +0200
Subject: [PATCH 026/100] 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 684dd9e04985191f04241407fadfd311ce3e8be3 Mon Sep 17 00:00:00 2001
From: "Laurent Mignon (ACSONE)"
Date: Thu, 29 Jun 2017 23:14:42 +0200
Subject: [PATCH 027/100] 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 5830276f7ba1cb8f5f47f745806c89e197b2b215 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Fri, 30 Jun 2017 11:02:29 +0200
Subject: [PATCH 028/100] 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 0fb64f608f3a25b60abaf3b77fb11fd23045282c Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 4 Jul 2017 13:13:22 +0200
Subject: [PATCH 029/100] 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 0ce3fc72b70987a8c6938e2410a9cbea7376b92b Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 4 Jul 2017 16:05:59 +0200
Subject: [PATCH 030/100] 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 eac18f3c1441e66075174d6438f47999cc544c6c Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Thu, 6 Jul 2017 16:26:37 +0200
Subject: [PATCH 031/100] 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 aed3eef6460a10b50dfad9401be1bb8e465c660a Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Thu, 6 Jul 2017 17:00:50 +0200
Subject: [PATCH 032/100] 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 e29b765c6733fda1a240c736e848d82d7648646a Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 11 Jul 2017 15:18:51 +0200
Subject: [PATCH 033/100] 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 5830ef3a78fb8af1e110caf5301314939421809b Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 15 Aug 2017 14:19:27 +0200
Subject: [PATCH 034/100] 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 1f2f447e84af149a7cd8d76b7b5869cd19024ac0 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 15 Aug 2017 14:20:05 +0200
Subject: [PATCH 035/100] 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 4c458ef705d487eee4e9886f4c8086043a0e20b9 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 3 Oct 2017 10:16:49 +0200
Subject: [PATCH 036/100] 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 395b7f04f8add317a0d94e81d65b846df7825415 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 3 Oct 2017 10:37:57 +0200
Subject: [PATCH 037/100] 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 ebecc5fb9878667177d706420bb5344578f8d22c Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 3 Oct 2017 10:43:50 +0200
Subject: [PATCH 038/100] 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 61b7cf3d6c31c88b6630382dfdb425d6ba70e63b Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 3 Oct 2017 16:58:21 +0200
Subject: [PATCH 039/100] 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 c6a4b2e366bea0bae3135ed750485e8aef509c8c Mon Sep 17 00:00:00 2001
From: OCA Transbot
Date: Sat, 6 Jan 2018 03:28:39 +0100
Subject: [PATCH 040/100] 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 50509291f1d19c00a3c293ea7ec0ff47e5328ff9 Mon Sep 17 00:00:00 2001
From: "Laurent Mignon (ACSONE)"
Date: Tue, 9 Jan 2018 16:39:17 +0100
Subject: [PATCH 041/100] 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 3de3b576e6be05f9aed63f7b63f3343a6eaa5c12 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Thu, 25 Jan 2018 11:52:06 +0100
Subject: [PATCH 042/100] 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 9fe9b2e27f3fd5ff34f0f605cd2f643a8df5cff7 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Wed, 31 Jan 2018 15:03:41 +0100
Subject: [PATCH 043/100] 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 af471e469c11b775c87d40a7327b3b92a7702c03 Mon Sep 17 00:00:00 2001
From: OCA Transbot
Date: Sat, 17 Feb 2018 03:25:32 +0100
Subject: [PATCH 044/100] 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 1da12eb34b446e529c3b74c03c8b5899457b9fb1 Mon Sep 17 00:00:00 2001
From: Naglis Jonaitis
Date: Thu, 1 Mar 2018 15:16:57 +0200
Subject: [PATCH 045/100] 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 e5c91c10efcaeff5b91c73214c5107cd1db4744c Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Mon, 4 Jun 2018 13:58:01 +0200
Subject: [PATCH 046/100] 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 6ed47bc89ddb60efdaf64398281149293e8114c7 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Mon, 4 Jun 2018 17:24:05 +0200
Subject: [PATCH 047/100] 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 +++++++++
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 ++--
17 files changed, 94 insertions(+), 35 deletions(-)
create mode 100644 component/i18n/component.pot
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__)
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 647cbe4cf5b0701482106edb413df818bc71adb8 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Wed, 27 Jun 2018 06:40:03 +0000
Subject: [PATCH 048/100] 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 21ca1bc4b695e437608a7e7c5674fb5fb553f328 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Fri, 30 Mar 2018 09:22:09 +0200
Subject: [PATCH 049/100] 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 a195c334536168a08849110aa93343fb021141e3 Mon Sep 17 00:00:00 2001
From: OCA git bot
Date: Thu, 27 Sep 2018 01:59:42 +0200
Subject: [PATCH 050/100] 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 c251148878e731a4f7ce7b436d6e4ac728889957 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 2 Oct 2018 15:24:01 +0200
Subject: [PATCH 051/100] 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 b95e2632560259c63a508992b9f6088d7d4d87a3 Mon Sep 17 00:00:00 2001
From: Guewen Baconnier
Date: Tue, 2 Oct 2018 15:43:44 +0200
Subject: [PATCH 052/100] Add OCA development status
---
component/README.rst | 131 ++++++-
component/__manifest__.py | 4 +-
component/i18n/am.po | 12 +-
component/i18n/ca.po | 12 +-
component/i18n/component.pot | 14 +-
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 +-
component/static/description/index.html | 475 ++++++++++++++++++++++++
19 files changed, 703 insertions(+), 101 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/__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'],
}
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/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 ""
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 ""
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
+
+
+

+
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
+
+
+
+
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)
+
+
+
+
+
+
+
+
+
+- [MIGRATION] from 11.0 branched at rev. 324e006
+
+
+
+
+
+
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.
+
+
+
+
+
+
+
+
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.
+
Current maintainer:
+

+
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 41c850303386ffd67d521246599faddf6960a4cd Mon Sep 17 00:00:00 2001
From: Naglis Jonaitis
Date: Tue, 19 Mar 2019 21:55:27 +0200
Subject: [PATCH 053/100] 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/static/description/icon.png | Bin 0 -> 9455 bytes
component/static/description/index.html | 2 +-
component/tests/common.py | 3 ++-
3 files changed, 3 insertions(+), 2 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)`y_~Hnd9AUX7h-H?jVuU|}My+C=TjH(jKz
zqMVr0re3S$H@t{zI95qa)+Crz*5Zj}Ao%4Z><+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+Zls4&}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
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
+
+
+
+
+
+
+
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.
@@ -400,7 +405,7 @@
Components
-
+
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,40 +437,40 @@
docstrings in tests/common.py.
-
+
-
+
- [MIGRATION] from 11.0 branched at rev. 324e006
-
+
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
@@ -473,15 +478,15 @@
Do not contact contributors directly about support or help with technical issues.
-
+
-
+
The migration of this module from 17.0 to 18.0 was financially supported
by Camptocamp.
-
+
This module is maintained by the OCA.
@@ -510,5 +515,6 @@
+
diff --git a/component/tests/common.py b/component/tests/common.py
index f7cf5d1fe1..17da7f0f85 100644
--- a/component/tests/common.py
+++ b/component/tests/common.py
@@ -46,6 +46,9 @@ def setUpComponent(cls):
# pylint: disable=W8106
def setUp(self):
+ self.setUpComponentRegistryReady()
+
+ def setUpComponentRegistryReady(self):
# should be ready only during tests, never during installation
# of addons
self._components_registry.ready = True
From 38fec9e8fe152e0c6e986f31df99562a5931fb6b Mon Sep 17 00:00:00 2001
From: Maksym Yankin
Date: Wed, 1 Apr 2026 14:15:45 +0300
Subject: [PATCH 099/100] [IMP] component: pre-commit auto fixes
---
component/__manifest__.py | 2 +-
component/builder.py | 2 +-
component/tests/test_component.py | 5 +----
requirements.txt | 2 ++
4 files changed, 5 insertions(+), 6 deletions(-)
create mode 100644 requirements.txt
diff --git a/component/__manifest__.py b/component/__manifest__.py
index 147bcec882..c0b2f70e78 100644
--- a/component/__manifest__.py
+++ b/component/__manifest__.py
@@ -6,7 +6,7 @@
"summary": "Add capabilities to register and use decoupled components,"
" as an alternative to model classes",
"version": "18.0.1.0.1",
- "author": "Camptocamp," "Odoo Community Association (OCA)",
+ "author": "Camptocamp,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/connector",
"license": "LGPL-3",
"category": "Generic Modules",
diff --git a/component/builder.py b/component/builder.py
index 913739159a..70fc0d5310 100644
--- a/component/builder.py
+++ b/component/builder.py
@@ -66,7 +66,7 @@ def build_registry(self, components_registry, states=None, exclude_addons=None):
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 "
diff --git a/component/tests/test_component.py b/component/tests/test_component.py
index 69f168957a..37ff84d64a 100644
--- a/component/tests/test_component.py
+++ b/component/tests/test_component.py
@@ -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/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 22fc655b4b699e7d3364aa12fef6d86ac76e05e6 Mon Sep 17 00:00:00 2001
From: sanazzzmi
Date: Sat, 11 Oct 2025 21:15:31 +0330
Subject: [PATCH 100/100] [MIG] component: Migration to 19.0
---
component/README.rst | 13 ++++++-------
component/__manifest__.py | 2 +-
component/builder.py | 12 +++++++-----
component/readme/CREDITS.md | 1 -
component/requirements.txt | 2 ++
component/static/description/index.html | 8 +++-----
component/tests/common.py | 23 +++++++++++++++--------
component/tests/test_component.py | 2 +-
component/tests/test_work_on.py | 4 ++--
9 files changed, 37 insertions(+), 30 deletions(-)
create mode 100644 component/requirements.txt
diff --git a/component/README.rst b/component/README.rst
index ad96e59851..28aac25302 100644
--- a/component/README.rst
+++ b/component/README.rst
@@ -21,13 +21,13 @@ Components
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fconnector-lightgray.png?logo=github
- :target: https://github.com/OCA/connector/tree/18.0/component
+ :target: https://github.com/OCA/connector/tree/19.0/component
:alt: OCA/connector
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
- :target: https://translation.odoo-community.org/projects/connector-18-0/connector-18-0-component
+ :target: https://translation.odoo-community.org/projects/connector-19-0/connector-19-0-component
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
- :target: https://runboat.odoo-community.org/builds?repo=OCA/connector&target_branch=18.0
+ :target: https://runboat.odoo-community.org/builds?repo=OCA/connector&target_branch=19.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
@@ -119,7 +119,7 @@ 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.
@@ -142,8 +142,7 @@ Contributors
Other credits
-------------
-The migration of this module from 17.0 to 18.0 was financially supported
-by Camptocamp.
+
Maintainers
-----------
@@ -166,6 +165,6 @@ Current `maintainer `__:
|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/component/__manifest__.py b/component/__manifest__.py
index c0b2f70e78..128d583a1e 100644
--- a/component/__manifest__.py
+++ b/component/__manifest__.py
@@ -5,7 +5,7 @@
"name": "Components",
"summary": "Add capabilities to register and use decoupled components,"
" as an alternative to model classes",
- "version": "18.0.1.0.1",
+ "version": "19.0.1.0.0",
"author": "Camptocamp,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/connector",
"license": "LGPL-3",
diff --git a/component/builder.py b/component/builder.py
index 70fc0d5310..f5894679ba 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
@@ -60,21 +60,23 @@ def _init_global_registry(self):
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
# the graph, so we can load the components following the order
# of the addons' dependencies
- graph = odoo.modules.graph.Graph()
- graph.add_module(self.env.cr, "base")
query = "SELECT name FROM ir_module_module WHERE state IN %s "
params = [tuple(states)]
if exclude_addons:
query += " AND name NOT IN %s "
params.append(tuple(exclude_addons))
+
self.env.cr.execute(query, params)
- module_list = [name for (name,) in self.env.cr.fetchall() if name not in graph]
- graph.add_modules(self.env.cr, module_list)
+ 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/readme/CREDITS.md b/component/readme/CREDITS.md
index 83b3ec91f7..e69de29bb2 100644
--- a/component/readme/CREDITS.md
+++ b/component/readme/CREDITS.md
@@ -1 +0,0 @@
-The migration of this module from 17.0 to 18.0 was financially supported by Camptocamp.
diff --git a/component/requirements.txt b/component/requirements.txt
new file mode 100644
index 0000000000..d3dfeea70b
--- /dev/null
+++ b/component/requirements.txt
@@ -0,0 +1,2 @@
+# generated from manifests external_dependencies
+cachetools
diff --git a/component/static/description/index.html b/component/static/description/index.html
index abd42185c5..5c15574691 100644
--- a/component/static/description/index.html
+++ b/component/static/description/index.html
@@ -374,7 +374,7 @@ Components
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:c9cfc8d785a837cead4bbc1ec4f356f5975a9387310b0e72f7f5e6e0ef530916
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
-

+

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.
@@ -474,7 +474,7 @@
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.
-
The migration of this module from 17.0 to 18.0 was financially supported
-by Camptocamp.
@@ -510,7 +508,7 @@
promote its widespread use.
Current maintainer:

-
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/component/tests/common.py b/component/tests/common.py
index 17da7f0f85..49133c4ee6 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
@@ -79,8 +81,11 @@ 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,
+ )
)
@@ -162,9 +167,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 37ff84d64a..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():
diff --git a/component/tests/test_work_on.py b/component/tests/test_work_on.py
index 2004ba7e56..18ad3e089c 100644
--- a/component/tests/test_work_on.py
+++ b/component/tests/test_work_on.py
@@ -24,7 +24,7 @@ def tearDown(self):
def test_collection_work_on(self):
"""Create a new instance and test attributes access"""
- collection_record = self.collection.new()
+ collection_record = self.env[self.collection._name].new()
with collection_record.work_on("res.partner") as work:
self.assertEqual(collection_record, work.collection)
self.assertEqual("collection.base", work.collection._name)
@@ -51,7 +51,7 @@ def test_propagate_work_on(self):
registry = ComponentRegistry()
work = WorkContext(
model_name="res.partner",
- collection=self.collection,
+ collection=self.env[self.collection._name],
# 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