Browse Source
Merge pull request #979 from LasLabs/feature/9.0/LABS-474-base_manifest_extension-migrate
Merge pull request #979 from LasLabs/feature/9.0/LABS-474-base_manifest_extension-migrate
[9.0][MIG][ADD] base_manifest_extension: Migrate to v9 and rdepends_if_installedpull/1001/head
Dave Lasley
7 years ago
committed by
GitHub
7 changed files with 422 additions and 0 deletions
-
79base_manifest_extension/README.rst
-
5base_manifest_extension/__init__.py
-
15base_manifest_extension/__openerp__.py
-
132base_manifest_extension/hooks.py
-
BINbase_manifest_extension/static/description/icon.png
-
6base_manifest_extension/tests/__init__.py
-
185base_manifest_extension/tests/test_hooks.py
@ -0,0 +1,79 @@ |
|||||
|
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg |
||||
|
:target: https://www.gnu.org/licenses/lgpl.html |
||||
|
:alt: License: LGPL-3 |
||||
|
|
||||
|
=============================== |
||||
|
Module Manifest - Extra Options |
||||
|
=============================== |
||||
|
|
||||
|
This is a technical module that allows developers to make use of extra keys in |
||||
|
module manifests. The following keys are available currently: |
||||
|
|
||||
|
* ``depends_if_installed`` - Your module will depend on modules listed here but |
||||
|
only if those modules are already installed. This is useful if your module |
||||
|
needs to override the behavior of a certain module (dependencies determine |
||||
|
load order) but would also work without it. |
||||
|
* ``rdepends_if_installed`` - The modules listed here will depend on your |
||||
|
module if they are already installed. This is useful if you want your module |
||||
|
to be higher in the inheritance chain than the target modules and do not want |
||||
|
to or cannot make changes to those modules. |
||||
|
|
||||
|
Usage |
||||
|
===== |
||||
|
|
||||
|
Add this module as a dependency and use the keys described above. |
||||
|
|
||||
|
Roadmap |
||||
|
======= |
||||
|
|
||||
|
Add support for the following additional keys: |
||||
|
|
||||
|
* ``breaks`` - Used to mark some modules as being incompatible with the |
||||
|
current one. This could be set up to support versioning (e.g. ``'breaks': |
||||
|
['my_module<<0.4.2']``). |
||||
|
* ``demo_if_installed``, ``data_if_installed``, ``qweb_if_installed`` - Dicts |
||||
|
with module names as keys and lists of files as values. Used to load files |
||||
|
only if some other module is installed. |
||||
|
* ``_if_module`` - Used on models to load them only if the appropriate module |
||||
|
is installed. |
||||
|
|
||||
|
Bug Tracker |
||||
|
=========== |
||||
|
|
||||
|
Bugs are tracked on |
||||
|
`GitHub Issues <https://github.com/OCA/server-tools/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 welcome feedback. |
||||
|
|
||||
|
Credits |
||||
|
======= |
||||
|
|
||||
|
Images |
||||
|
------ |
||||
|
|
||||
|
* Odoo Community Association: |
||||
|
`Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_. |
||||
|
|
||||
|
Contributors |
||||
|
------------ |
||||
|
|
||||
|
* Holger Brunn <hbrunn@therp.nl> |
||||
|
* Oleg Bulkin <obulkin@laslabs.com> |
||||
|
|
||||
|
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. |
@ -0,0 +1,5 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Therp BV <http://therp.nl> |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) |
||||
|
|
||||
|
from .hooks import post_load_hook |
@ -0,0 +1,15 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Therp BV <http://therp.nl> |
||||
|
# Copyright 2017 LasLabs Inc. |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) |
||||
|
|
||||
|
{ |
||||
|
'name': 'Module Manifest - Extra Options', |
||||
|
'version': '9.0.1.0.0', |
||||
|
'website': 'https://github.com/OCA/server-tools', |
||||
|
'author': 'Therp BV, LasLabs, Odoo Community Association (OCA)', |
||||
|
'license': 'LGPL-3', |
||||
|
'category': 'Hidden/Dependency', |
||||
|
'summary': 'Adds useful keys to manifest files', |
||||
|
'post_load': 'post_load_hook', |
||||
|
} |
@ -0,0 +1,132 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Therp BV <http://therp.nl> |
||||
|
# Copyright 2017 LasLabs Inc. |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) |
||||
|
|
||||
|
import inspect |
||||
|
from werkzeug.local import Local |
||||
|
from openerp.sql_db import Cursor |
||||
|
from openerp.modules import module |
||||
|
from openerp.modules.graph import Graph |
||||
|
|
||||
|
original = module.load_information_from_description_file |
||||
|
local = Local() |
||||
|
local.rdepends_to_process = {} |
||||
|
|
||||
|
|
||||
|
def load_information_from_description_file(module, mod_path=None): |
||||
|
result = original(module, mod_path=mod_path) |
||||
|
|
||||
|
# add the keys you want to react on here |
||||
|
if result.get('depends_if_installed'): |
||||
|
cr = _get_cr() |
||||
|
if cr: |
||||
|
_handle_depends_if_installed(cr, result) |
||||
|
if result.get('rdepends_if_installed'): |
||||
|
cr = _get_cr() |
||||
|
if cr: |
||||
|
_handle_rdepends_if_installed(cr, result, module) |
||||
|
|
||||
|
# Apply depends specified in other modules as rdepends |
||||
|
extra_depends = local.rdepends_to_process.get(module) |
||||
|
if extra_depends: |
||||
|
result['depends'] += extra_depends |
||||
|
|
||||
|
return result |
||||
|
|
||||
|
|
||||
|
def _handle_depends_if_installed(cr, manifest): |
||||
|
if not manifest.get('depends_if_installed'): |
||||
|
return |
||||
|
|
||||
|
added_depends = manifest.pop('depends_if_installed') |
||||
|
added_depends = _installed_modules(cr, added_depends) |
||||
|
|
||||
|
depends = manifest.setdefault('depends', []) |
||||
|
depends.extend(added_depends) |
||||
|
|
||||
|
|
||||
|
def _handle_rdepends_if_installed(cr, manifest, current_module): |
||||
|
graph = _get_graph() |
||||
|
if not graph: |
||||
|
return |
||||
|
|
||||
|
rdepends = manifest.pop('rdepends_if_installed') |
||||
|
rdepends = _installed_modules(cr, rdepends) |
||||
|
|
||||
|
for rdepend in rdepends: |
||||
|
to_process = local.rdepends_to_process.get(rdepend, set([])) |
||||
|
local.rdepends_to_process[rdepend] = to_process | set([current_module]) |
||||
|
# If rdepend already in graph, reload it so new depend is applied |
||||
|
if graph.get(rdepend): |
||||
|
del graph[rdepend] |
||||
|
graph.add_module(cr, rdepend) |
||||
|
|
||||
|
|
||||
|
def _installed_modules(cr, modules): |
||||
|
if not modules: |
||||
|
return [] |
||||
|
|
||||
|
cr.execute( |
||||
|
'SELECT name FROM ir_module_module ' |
||||
|
'WHERE state IN %s AND name IN %s', |
||||
|
( |
||||
|
tuple(['installed', 'to install', 'to upgrade']), |
||||
|
tuple(modules), |
||||
|
), |
||||
|
) |
||||
|
return [module for module, in cr.fetchall()] |
||||
|
|
||||
|
|
||||
|
def _get_cr(): |
||||
|
cr = None |
||||
|
for frame, filename, lineno, funcname, line, index in inspect.stack(): |
||||
|
# walk up the stack until we've found a cursor |
||||
|
if 'cr' in frame.f_locals and isinstance(frame.f_locals['cr'], Cursor): |
||||
|
cr = frame.f_locals['cr'] |
||||
|
break |
||||
|
return cr |
||||
|
|
||||
|
|
||||
|
def _get_graph(): |
||||
|
graph = None |
||||
|
for frame, filename, lineno, funcname, line, index in inspect.stack(): |
||||
|
# walk up the stack until we've found a graph |
||||
|
if 'graph' in frame.f_locals and isinstance( |
||||
|
frame.f_locals['graph'], Graph |
||||
|
): |
||||
|
graph = frame.f_locals['graph'] |
||||
|
break |
||||
|
return graph |
||||
|
|
||||
|
|
||||
|
def post_load_hook(): |
||||
|
cr = _get_cr() |
||||
|
if not cr: |
||||
|
return |
||||
|
|
||||
|
# do nothing if we're not installed |
||||
|
installed = _installed_modules(cr, ['base_manifest_extension']) |
||||
|
if not installed: |
||||
|
return |
||||
|
|
||||
|
module.load_information_from_description_file =\ |
||||
|
load_information_from_description_file |
||||
|
|
||||
|
# here stuff can become tricky: On the python level, modules |
||||
|
# are not loaded in dependency order. This means that there might |
||||
|
# be modules loaded depending on us before we could patch the function |
||||
|
# above. So we reload the module graph for all modules coming after us |
||||
|
|
||||
|
graph = _get_graph() |
||||
|
if not graph: |
||||
|
return |
||||
|
|
||||
|
this = graph['base_manifest_extension'] |
||||
|
to_reload = [] |
||||
|
for node in graph.itervalues(): |
||||
|
if node.depth > this.depth: |
||||
|
to_reload.append(node.name) |
||||
|
for module_name in to_reload: |
||||
|
del graph[module_name] |
||||
|
graph.add_modules(cr, to_reload) |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
@ -0,0 +1,6 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Therp BV <http://therp.nl> |
||||
|
# Copyright 2017 LasLabs Inc. |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) |
||||
|
|
||||
|
from . import test_hooks |
@ -0,0 +1,185 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# Copyright 2017 Therp BV <http://therp.nl> |
||||
|
# Copyright 2017 LasLabs Inc. |
||||
|
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) |
||||
|
|
||||
|
from mock import Mock, patch |
||||
|
import os |
||||
|
import tempfile |
||||
|
from openerp.modules.module import load_information_from_description_file,\ |
||||
|
get_module_path, MANIFEST |
||||
|
from openerp.tests.common import TransactionCase |
||||
|
from ..hooks import _handle_rdepends_if_installed, _installed_modules |
||||
|
|
||||
|
MOCK_PATH = 'openerp.addons.base_manifest_extension.hooks' |
||||
|
|
||||
|
|
||||
|
class TestHooks(TransactionCase): |
||||
|
def setUp(self): |
||||
|
super(TestHooks, self).setUp() |
||||
|
|
||||
|
self.test_cr = self.env.cr |
||||
|
self.test_rdepends = [ |
||||
|
'base', |
||||
|
'base_manifest_extension', |
||||
|
'not_installed', |
||||
|
] |
||||
|
self.test_manifest = { |
||||
|
'rdepends_if_installed': self.test_rdepends, |
||||
|
'depends': [], |
||||
|
} |
||||
|
self.test_module_name = 'base_manifest_extension' |
||||
|
self.test_call = ( |
||||
|
self.test_cr, |
||||
|
self.test_manifest, |
||||
|
self.test_module_name, |
||||
|
) |
||||
|
|
||||
|
def test_base_manifest_extension(self): |
||||
|
# write a test manifest |
||||
|
module_path = tempfile.mkdtemp(dir=os.path.join( |
||||
|
get_module_path('base_manifest_extension'), 'static' |
||||
|
)) |
||||
|
with open(os.path.join(module_path, MANIFEST), 'w') as manifest: |
||||
|
manifest.write(repr({ |
||||
|
'depends_if_installed': [ |
||||
|
'base_manifest_extension', |
||||
|
'not installed', |
||||
|
], |
||||
|
})) |
||||
|
# parse it |
||||
|
parsed = load_information_from_description_file( |
||||
|
# this name won't really be used, but avoids a warning |
||||
|
'base', mod_path=module_path, |
||||
|
) |
||||
|
self.assertIn('base_manifest_extension', parsed['depends']) |
||||
|
self.assertNotIn('not installed', parsed['depends']) |
||||
|
self.assertNotIn('depends_if_installed', parsed) |
||||
|
|
||||
|
def test_installed_modules_correct_result(self): |
||||
|
"""It should return only installed modules in list""" |
||||
|
result = _installed_modules(self.test_cr, self.test_rdepends) |
||||
|
|
||||
|
expected = self.test_rdepends[:2] |
||||
|
self.assertEqual(result, expected) |
||||
|
|
||||
|
def test_installed_modules_empty_starting_list(self): |
||||
|
"""It should safely handle being passed an empty module list""" |
||||
|
result = _installed_modules(self.test_cr, []) |
||||
|
|
||||
|
self.assertEqual(result, []) |
||||
|
|
||||
|
@patch(MOCK_PATH + '._get_graph') |
||||
|
def test_handle_rdepends_if_installed_graph_call(self, graph_mock): |
||||
|
"""It should call graph helper and return early if graph not found""" |
||||
|
graph_mock.return_value = None |
||||
|
graph_mock.reset_mock() |
||||
|
self.test_cr = Mock() |
||||
|
self.test_cr.reset_mock() |
||||
|
_handle_rdepends_if_installed(*self.test_call) |
||||
|
|
||||
|
graph_mock.assert_called_once() |
||||
|
self.test_cr.assert_not_called() |
||||
|
|
||||
|
@patch(MOCK_PATH + '._get_graph') |
||||
|
def test_handle_rdepends_if_installed_clean_manifest(self, graph_mock): |
||||
|
"""It should remove rdepends key from manifest""" |
||||
|
_handle_rdepends_if_installed(*self.test_call) |
||||
|
|
||||
|
self.assertEqual(self.test_manifest, {'depends': []}) |
||||
|
|
||||
|
@patch(MOCK_PATH + '.local.rdepends_to_process', new_callable=dict) |
||||
|
@patch(MOCK_PATH + '._get_graph') |
||||
|
def test_handle_rdepends_if_installed_list(self, graph_mock, dict_mock): |
||||
|
"""It should correctly add all installed rdepends to processing dict""" |
||||
|
_handle_rdepends_if_installed(*self.test_call) |
||||
|
|
||||
|
expected_result = { |
||||
|
'base': set([self.test_module_name]), |
||||
|
'base_manifest_extension': set([self.test_module_name]), |
||||
|
} |
||||
|
self.assertEqual(dict_mock, expected_result) |
||||
|
|
||||
|
@patch(MOCK_PATH + '.local.rdepends_to_process', new_callable=dict) |
||||
|
@patch(MOCK_PATH + '._get_graph') |
||||
|
def test_handle_rdepends_if_installed_dupes(self, graph_mock, dict_mock): |
||||
|
"""It should correctly handle multiple calls with same rdepends""" |
||||
|
for __ in range(2): |
||||
|
_handle_rdepends_if_installed(*self.test_call) |
||||
|
self.test_manifest['rdepends_if_installed'] = self.test_rdepends |
||||
|
test_module_name_2 = 'test_module_name_2' |
||||
|
_handle_rdepends_if_installed( |
||||
|
self.test_cr, |
||||
|
self.test_manifest, |
||||
|
test_module_name_2, |
||||
|
) |
||||
|
|
||||
|
expected_set = set([self.test_module_name, test_module_name_2]) |
||||
|
expected_result = { |
||||
|
'base': expected_set, |
||||
|
'base_manifest_extension': expected_set, |
||||
|
} |
||||
|
self.assertEqual(dict_mock, expected_result) |
||||
|
|
||||
|
@patch(MOCK_PATH + '._get_graph') |
||||
|
def test_handle_rdepends_if_installed_graph_reload(self, graph_mock): |
||||
|
"""It should reload installed rdepends already in module graph""" |
||||
|
class TestGraph(dict): |
||||
|
pass |
||||
|
|
||||
|
test_graph = TestGraph(base='Test Value') |
||||
|
test_graph.add_module = Mock() |
||||
|
graph_mock.return_value = test_graph |
||||
|
_handle_rdepends_if_installed(*self.test_call) |
||||
|
|
||||
|
self.assertEqual(test_graph, {}) |
||||
|
test_graph.add_module.assert_called_once_with(self.cr, 'base') |
||||
|
|
||||
|
@patch(MOCK_PATH + '._handle_rdepends_if_installed') |
||||
|
@patch(MOCK_PATH + '._get_cr') |
||||
|
@patch(MOCK_PATH + '.original') |
||||
|
def test_load_information_from_description_file_rdepends_key( |
||||
|
self, super_mock, cr_mock, helper_mock |
||||
|
): |
||||
|
"""It should correctly call rdepends helper if key present""" |
||||
|
super_mock.return_value = self.test_manifest |
||||
|
cr_mock.return_value = self.cr |
||||
|
helper_mock.reset_mock() |
||||
|
load_information_from_description_file(self.test_module_name) |
||||
|
|
||||
|
helper_mock.assert_called_once_with(*self.test_call) |
||||
|
|
||||
|
@patch(MOCK_PATH + '._handle_rdepends_if_installed') |
||||
|
@patch(MOCK_PATH + '._get_cr') |
||||
|
@patch(MOCK_PATH + '.original') |
||||
|
def test_load_information_from_description_file_no_rdepends_key( |
||||
|
self, super_mock, cr_mock, helper_mock |
||||
|
): |
||||
|
"""It should not call rdepends helper if key not present""" |
||||
|
del self.test_manifest['rdepends_if_installed'] |
||||
|
super_mock.return_value = self.test_manifest |
||||
|
cr_mock.return_value = self.cr |
||||
|
helper_mock.reset_mock() |
||||
|
load_information_from_description_file(self.test_module_name) |
||||
|
|
||||
|
helper_mock.assert_not_called() |
||||
|
|
||||
|
@patch(MOCK_PATH + '._get_cr') |
||||
|
@patch(MOCK_PATH + '.original') |
||||
|
def test_load_information_from_description_file_rdepends_to_process( |
||||
|
self, super_mock, cr_mock |
||||
|
): |
||||
|
"""It should correctly add pending rdepends to manifest""" |
||||
|
del self.test_manifest['rdepends_if_installed'] |
||||
|
super_mock.return_value = self.test_manifest |
||||
|
cr_mock.return_value = self.cr |
||||
|
test_depends = set(['Test Depend 1', 'Test Depend 2']) |
||||
|
test_rdepend_dict = { |
||||
|
self.test_module_name: test_depends, |
||||
|
'Other Module': set(['Other Depend']), |
||||
|
} |
||||
|
dict_path = MOCK_PATH + '.local.rdepends_to_process' |
||||
|
with patch.dict(dict_path, test_rdepend_dict, clear=True): |
||||
|
load_information_from_description_file(self.test_module_name) |
||||
|
|
||||
|
self.assertEqual(self.test_manifest['depends'], list(test_depends)) |
Write
Preview
Loading…
Cancel
Save
Reference in new issue