[REF] core: remove http.addons_manifest

The http.addons_manifest is a map {module: manifest_dict} that is
populated upon the first http request. This map is basically a module
manifest cache with an extra `addons_path` key, the path of the module
on the file-system. This cache is eagerly populated upon the first http
request, the map is empty in non-http contextes (e.g. cron) which have
been a source of bugs (e.g. 50c8eb1).

A manifest cache is necessary because reading and parsing python files
from the file-system is not that cheap but there is no reason that cache
is located in `odoo.http`. A thin cache layer now wraps
`load_information_from_description_file()`/`load_manifest()` and is
lazily populated.

The `http.addons_manifest` have been removed. The extra `addons_path`
key is now present in the "normal" manifest. The `read_manifest()` was
hardly used so it has been deprecated. The only way to retrieve a
manifest is now `load_information_from_description_file()` which was
renamed `load_manifest()` (no cache) and `get_manifest()` (cache).

Side note about performances, the cache is necessary. Addons manifest
are read-only and reading + parsing python files from the file system is
not a cheap operation. Running the e-commerce tour
`@website_sale.test_04_admin_website_sale_tour` without cache on
`load_manifest()` requires 68,29 secs to complete on my laptop,
exceeding the default 1-minute time frame allowed in tests. Using a
cache the time is down to 36,53 secs. The performance impact is huge.

Part-of: odoo/odoo#79977
This commit is contained in:
Julien Castiaux
2021-12-14 12:39:54 +00:00
parent 75012e62e9
commit 2e29a93503
18 changed files with 267 additions and 174 deletions
@@ -12,7 +12,7 @@ from os.path import join as opj
import odoo
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.modules import load_information_from_description_file
from odoo.modules.module import load_manifest
from odoo.tools import convert_file, exception_to_unicode
_logger = logging.getLogger(__name__)
@@ -37,7 +37,7 @@ class IrModule(models.Model):
known_mods_names = {m.name: m for m in known_mods}
installed_mods = [m.name for m in known_mods if m.state == 'installed']
terp = load_information_from_description_file(module, mod_path=path)
terp = load_manifest(module, mod_path=path)
if not terp:
return False
values = self.get_values_from_terp(terp)
-1
View File
@@ -266,7 +266,6 @@ def load_iot_handlers():
if spec:
module = util.module_from_spec(spec)
spec.loader.exec_module(module)
http.addons_manifest = {}
http.root = http.Root()
def odoo_restart(delay):
+6 -7
View File
@@ -32,7 +32,7 @@ import odoo
import odoo.modules.registry
from odoo.api import call_kw
from odoo.addons.base.models.qweb import QWeb
from odoo.modules import get_resource_path, module
from odoo.modules import get_resource_path, module, get_manifest
from odoo.tools import html_escape, pycompat, ustr, apply_inheritance_specs, lazy_property, float_repr, osutil
from odoo.tools.mimetypes import guess_mimetype
from odoo.tools.translate import _
@@ -331,7 +331,7 @@ def fix_view_modes(action):
def _local_web_translations(trans_file):
messages = []
try:
with open(trans_file) as t_file:
with file_open(trans_file, filter_ext=('.po')) as t_file:
po = babel.messages.pofile.read_po(t_file)
except Exception:
return
@@ -962,11 +962,10 @@ class WebClient(http.Controller):
translations_per_module = {}
for addon_name in mods:
manifest = http.addons_manifest.get(addon_name)
if manifest and manifest.get('bootstrap'):
addons_path = http.addons_manifest[addon_name]['addons_path']
f_name = os.path.join(addons_path, addon_name, "i18n", lang + ".po")
if not os.path.exists(f_name):
manifest = get_manifest(addon_name)
if manifest and manifest['bootstrap']:
f_name = get_resource_path(addon_name, 'i18n', f'{lang}.po')
if not f_name:
continue
translations_per_module[addon_name] = {'messages': _local_web_translations(f_name)}
+6 -8
View File
@@ -6,7 +6,7 @@ import time
import odoo
import odoo.tests
from odoo.modules.module import read_manifest
from odoo.modules.module import get_manifest
from odoo.tools import mute_logger
_logger = logging.getLogger(__name__)
@@ -15,14 +15,12 @@ _logger = logging.getLogger(__name__)
class TestAssetsGenerateTimeCommon(odoo.tests.TransactionCase):
def generate_bundles(self):
bundles = set()
installed_module_names = self.env['ir.module.module'].search([('state', '=', 'installed')]).mapped('name')
for addon_path in odoo.addons.__path__:
for addon in installed_module_names:
manifest = read_manifest(addon_path, addon) or {}
assets = manifest.get('assets')
if assets:
bundles |= set(assets.keys())
bundles = {
key
for module in installed_module_names
for key in get_manifest(module)['assets']
}
for bundle in bundles:
with mute_logger('odoo.addons.base.models.assetsbundle'):
+4 -2
View File
@@ -18,6 +18,7 @@ from odoo.addons.http_routing.models.ir_http import slug, unslug
from odoo.addons.web_editor.tools import get_video_url_data
from odoo.exceptions import UserError
from odoo.modules.module import get_resource_path
from odoo.tools import file_open
from odoo.tools.mimetypes import guess_mimetype
from odoo.tools.image import image_data_uri, base64_to_image
from odoo.addons.base.models.assetsbundle import AssetsBundle
@@ -56,8 +57,9 @@ class Web_Editor(http.Controller):
# Make sure we have at least size=1
size = max(1, min(size, 512))
# Initialize font
addons_path = http.addons_manifest['web']['addons_path']
font_obj = ImageFont.truetype(addons_path + font, size)
if font.startswith('/'):
font = font[1:]
font_obj = ImageFont.truetype(file_open(font, 'rb'), size)
# if received character is not a number, keep old behaviour (icon is character)
icon = chr(int(icon)) if icon.isdigit() else icon
+6 -6
View File
@@ -23,7 +23,7 @@ from odoo.addons.portal.controllers.portal import pager
from odoo.addons.iap.tools import iap_tools
from odoo.exceptions import UserError, AccessError
from odoo.http import request
from odoo.modules.module import get_resource_path
from odoo.modules.module import get_resource_path, get_manifest
from odoo.osv.expression import AND, OR, FALSE_DOMAIN, get_unaccent_wrapper
from odoo.tools.translate import _
from odoo.tools import escape_psql, pycompat
@@ -296,10 +296,10 @@ class Website(models.Model):
@api.model
def get_theme_snippet_lists(self, theme_name):
default_snippet_lists = http.addons_manifest['theme_default'].get('snippet_lists', {})
theme_snippet_lists = http.addons_manifest[theme_name].get('snippet_lists', {})
snippet_lists = {**default_snippet_lists, **theme_snippet_lists}
return snippet_lists
return {
**get_manifest('theme_default')['snippet_lists'],
**get_manifest(theme_name).get('snippet_lists', {}),
}
def configurator_set_menu_links(self, menu_company, module_data):
menus = self.env['website.menu'].search([('url', 'in', list(module_data.keys())), ('website_id', '=', self.id)])
@@ -339,7 +339,7 @@ class Website(models.Model):
def configurator_recommended_themes(self, industry_id, palette):
domain = [('name', '=like', 'theme%'), ('name', 'not in', ['theme_default', 'theme_common'])]
client_themes = request.env['ir.module.module'].search(domain).mapped('name')
client_themes_img = dict([(t, http.addons_manifest[t].get('images_preview_theme', {})) for t in client_themes])
client_themes_img = {t: get_manifest(t).get('images_preview_theme', {}) for t in client_themes}
themes_suggested = self._website_api_rpc(
'/api/website/2/configurator/recommended_themes/%s' % industry_id,
{'client_themes': client_themes_img}
+6 -17
View File
@@ -1,17 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import os
from glob import glob
from logging import getLogger
from werkzeug import urls
import odoo
import odoo.modules.module # get_manifest, don't from-import it
from odoo import api, fields, models, tools
from odoo.tools import misc
from odoo import tools
from odoo import api, fields, http, models
from odoo.http import root
_logger = getLogger(__name__)
@@ -140,10 +137,6 @@ class IrAsset(models.Model):
if bundle in seen:
raise Exception("Circular assets bundle declaration: %s" % " > ".join(seen + [bundle]))
if not root._loaded:
root.load_addons()
root._loaded = True
manifest_cache = http.addons_manifest
exts = []
if js:
exts += SCRIPT_EXTENSIONS
@@ -163,7 +156,7 @@ class IrAsset(models.Model):
accordingly.
It is nested inside `_get_asset_paths` since we need the current
list of addons, extensions, asset_paths and manifest_cache.
list of addons, extensions and asset_paths.
:param directive: string
:param target: string or None or False
@@ -209,11 +202,7 @@ class IrAsset(models.Model):
# 2. Process all addons' manifests.
for addon in self._topological_sort(tuple(addons)):
manifest = manifest_cache.get(addon)
if not manifest:
continue
manifest_assets = manifest.get('assets', {})
for command in manifest_assets.get(bundle, []):
for command in odoo.modules.module.get_manifest(addon)['assets'].get(bundle, ()):
directive, target, path_def = self._process_command(command)
process_path(directive, target, path_def)
@@ -269,7 +258,7 @@ class IrAsset(models.Model):
IrModule = self.env['ir.module.module']
def mapper(addon):
manif = http.addons_manifest.get(addon, {})
manif = odoo.modules.module.get_manifest(addon)
from_terp = IrModule.get_values_from_terp(manif)
from_terp['name'] = addon
from_terp['depends'] = manif.get('depends', ['base'])
@@ -314,7 +303,7 @@ class IrAsset(models.Model):
path_url = fs2web(path_def)
path_parts = [part for part in path_url.split('/') if part]
addon = path_parts[0]
addon_manifest = http.addons_manifest.get(addon)
addon_manifest = odoo.modules.module.get_manifest(addon)
safe_path = True
if addon_manifest:
+1 -1
View File
@@ -14,7 +14,7 @@ from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
BASE_VERSION = odoo.modules.load_information_from_description_file('base')['version']
BASE_VERSION = odoo.modules.get_manifest('base')['version']
MAX_FAIL_TIME = timedelta(hours=5) # chosen with a fair roll of the dice
+1 -1
View File
@@ -168,7 +168,7 @@ class Module(models.Model):
@classmethod
def get_module_info(cls, name):
try:
return modules.load_information_from_description_file(name)
return modules.get_manifest(name)
except Exception:
_logger.debug('Error when trying to fetch information for module %s', name, exc_info=True)
return {}
+1
View File
@@ -27,6 +27,7 @@ from . import test_mail
from . import test_menu
from . import test_mimetypes
from . import test_misc
from . import test_module
from . import test_orm
from . import test_ormcache
from . import test_osv
+85
View File
@@ -0,0 +1,85 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import os.path
import tempfile
from os.path import join as opj
from unittest.mock import patch
import odoo.addons
from odoo.modules.module import load_manifest
from odoo.release import major_version
from odoo.tests.common import BaseCase
class TestModuleManifest(BaseCase):
@classmethod
def setUpClass(cls):
cls._tmp_dir = tempfile.TemporaryDirectory(prefix='odoo-test-addons-')
cls.addClassCleanup(cls._tmp_dir.cleanup)
cls.addons_path = cls._tmp_dir.name
patcher = patch.object(odoo.addons, '__path__', [cls.addons_path])
patcher.start()
cls.addClassCleanup(patcher.stop)
def setUp(self):
self.module_root = tempfile.mkdtemp(prefix='odoo-test-module-', dir=self.addons_path)
self.module_name = os.path.basename(self.module_root)
def test_default_manifest(self):
with open(opj(self.module_root, '__manifest__.py'), 'w') as file:
file.write(str({'name': f'Temp {self.module_name}', 'license': 'MIT'}))
with self.assertNoLogs('odoo.modules.module', 'WARNING'):
manifest = load_manifest(self.module_name)
self.maxDiff = None
self.assertDictEqual(manifest, {
'addons_path': self.addons_path,
'application': False,
'assets': {},
'author': 'Odoo S.A.',
'auto_install': False,
'bootstrap': False,
'category': 'Uncategorized',
'data': [],
'demo': [],
'demo_xml': [],
'depends': [],
'description': '',
'external_dependencies': [],
'icon': '/base/static/description/icon.png',
'init_xml': [],
'installable': True,
'images': [],
'images_preview_theme': {},
'license': 'MIT',
'live_test_url': '',
'name': f'Temp {self.module_name}',
'post_init_hook': '',
'post_load': None,
'pre_init_hook': '',
'sequence': 100,
'snippet_lists': {},
'summary': '',
'test': [],
'update_xml': [],
'uninstall_hook': '',
'version': f'{major_version}.1.0',
'web': False,
'website': '',
})
def test_missing_manifest(self):
with self.assertLogs('odoo.modules.module', 'DEBUG') as capture:
manifest = load_manifest(self.module_name)
self.assertEqual(manifest, {})
self.assertIn("no manifest file found", capture.output[0])
def test_missing_license(self):
with open(opj(self.module_root, '__manifest__.py'), 'w') as file:
file.write(str({'name': f'Temp {self.module_name}'}))
with self.assertLogs('odoo.modules.module', 'WARNING') as capture:
manifest = load_manifest(self.module_name)
self.assertEqual(manifest['license'], 'LGPL-3')
self.assertIn("Missing `license` key", capture.output[0])
@@ -6,18 +6,19 @@ from lxml import etree
import os
import time
from unittest import skip
from unittest.mock import patch
from unittest.mock import Mock, patch
import textwrap
import pathlib
import lxml
import base64
import odoo
from odoo import api, http
from odoo.addons import __path__ as ADDONS_PATH
from odoo.addons.base.models.assetsbundle import AssetsBundle
from odoo.addons.base.models.ir_asset import AssetPaths
from odoo.addons.base.models.ir_attachment import IrAttachment
from odoo.modules.module import get_resource_path, read_manifest
from odoo.modules.module import get_resource_path, get_manifest
from odoo.tests import HttpCase, tagged
from odoo.tests.common import TransactionCase
from odoo.addons.base.models.qweb import QWebException
@@ -80,33 +81,19 @@ class TestAddonPaths(TransactionCase):
class AddonManifestPatched(TransactionCase):
test_assetsbundle_manifest = None
for path in ADDONS_PATH:
manifest = read_manifest(path, 'test_assetsbundle')
if manifest:
manifest['addons_path'] = path
test_assetsbundle_manifest = manifest
break
def tearDown(self):
super().tearDown()
self.env.registry._init_modules = self.__genuine_registry_modules
http.addons_manifest = self.__genuine_addons_manifest
def setUp(self):
super().setUp()
self.__genuine_registry_modules = self.env.registry._init_modules
self.env.registry._init_modules = func.lazy(lambda: set(self.installed_modules))
self.__genuine_addons_manifest = http.addons_manifest
http.addons_manifest = func.lazy(lambda: self.manifests)
self.installed_modules = ['base', 'test_assetsbundle']
self.installed_modules = {'base', 'test_assetsbundle'}
self.manifests = {
'test_assetsbundle': self.test_assetsbundle_manifest,
'base': get_manifest('base'),
'web': get_manifest('web'),
'test_assetsbundle': get_manifest('test_assetsbundle'),
}
self.patch(self.env.registry, '_init_modules', self.installed_modules)
self.patch(odoo.modules.module, 'get_manifest', Mock(side_effect=lambda module: self.manifests.get(module, {})))
class FileTouchable(AddonManifestPatched):
def setUp(self):
@@ -1205,7 +1192,7 @@ class TestAssetsManifest(AddonManifestPatched):
)
def test_14_other_module(self):
self.installed_modules.append('test_other')
self.installed_modules.add('test_other')
self.manifests['test_other'] = {
'name': 'test_other',
'depends': ['test_assetsbundle'],
@@ -1229,7 +1216,7 @@ class TestAssetsManifest(AddonManifestPatched):
)
def test_15_other_module_append(self):
self.installed_modules.append('test_other')
self.installed_modules.add('test_other')
self.manifests['test_other'] = {
'name': 'test_other',
'depends': ['test_assetsbundle'],
@@ -1256,7 +1243,7 @@ class TestAssetsManifest(AddonManifestPatched):
)
def test_16_other_module_prepend(self):
self.installed_modules.append('test_other')
self.installed_modules.add('test_other')
self.manifests['test_other'] = {
'name': 'test_other',
'depends': ['test_assetsbundle'],
@@ -1283,7 +1270,7 @@ class TestAssetsManifest(AddonManifestPatched):
)
def test_17_other_module_replace(self):
self.installed_modules.append('test_other')
self.installed_modules.add('test_other')
self.manifests['test_other'] = {
'name': 'test_other',
'depends': ['test_assetsbundle'],
@@ -1307,7 +1294,7 @@ class TestAssetsManifest(AddonManifestPatched):
)
def test_17_other_module_remove(self):
self.installed_modules.append('test_other')
self.installed_modules.add('test_other')
self.manifests['test_other'] = {
'name': 'test_other',
'depends': ['test_assetsbundle'],
@@ -1332,7 +1319,7 @@ class TestAssetsManifest(AddonManifestPatched):
)
def test_18_other_module_external(self):
self.installed_modules.append('test_other')
self.installed_modules.add('test_other')
self.manifests['test_other'] = {
'name': 'test_other',
'depends': ['test_assetsbundle'],
@@ -1431,7 +1418,7 @@ class TestAssetsManifest(AddonManifestPatched):
def test_21_js_before_css(self):
'''Non existing target node: ignore the manifest line'''
self.installed_modules.append('test_other')
self.installed_modules.add('test_other')
self.manifests['test_other'] = {
'name': 'test_other',
'depends': ['test_assetsbundle'],
@@ -1461,7 +1448,7 @@ class TestAssetsManifest(AddonManifestPatched):
)
def test_22_js_before_js(self):
self.installed_modules.append('test_other')
self.installed_modules.add('test_other')
self.manifests['test_other'] = {
'name': 'test_other',
'depends': ['test_assetsbundle'],
@@ -1495,7 +1482,7 @@ class TestAssetsManifest(AddonManifestPatched):
def test_23_js_after_css(self):
'''Non existing target node: ignore the manifest line'''
self.installed_modules.append('test_other')
self.installed_modules.add('test_other')
self.manifests['test_other'] = {
'name': 'test_other',
'depends': ['test_assetsbundle'],
@@ -1525,7 +1512,7 @@ class TestAssetsManifest(AddonManifestPatched):
)
def test_24_js_after_js(self):
self.installed_modules.append('test_other')
self.installed_modules.add('test_other')
self.manifests['test_other'] = {
'name': 'test_other',
'depends': ['test_assetsbundle'],
@@ -1785,7 +1772,7 @@ class TestAssetsManifest(AddonManifestPatched):
'path': '/test_assetsbundle/%s' % path_to_dummy,
})
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=self.installed_modules, xml=False)
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=list(self.installed_modules), xml=False)
self.assertFalse(files)
def test_33(self):
@@ -1834,7 +1821,7 @@ class TestAssetsManifest(AddonManifestPatched):
'bundle': 'test_assetsbundle.irassetsec',
'path': '/test_assetsbundle/data/ir_asset.xml',
})
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=self.installed_modules, xml=False)
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=list(self.installed_modules), xml=False)
self.assertFalse(files)
def test_36(self):
@@ -1843,7 +1830,7 @@ class TestAssetsManifest(AddonManifestPatched):
'bundle': 'test_assetsbundle.irassetsec',
'path': '/test_assetsbundle/static/accessible.xml',
})
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=self.installed_modules, xml=False)
files = self.env['ir.asset']._get_asset_paths('test_assetsbundle.irassetsec', addons=list(self.installed_modules), xml=False)
self.assertEqual(len(files), 1)
self.assertTrue('test_assetsbundle/static/accessible.xml' in files[0][0])
+9 -17
View File
@@ -56,7 +56,7 @@ from .tools import ustr, consteq, frozendict, pycompat, unique, date_utils
from .tools.mimetypes import guess_mimetype
from .tools.misc import str2bool
from .tools._vendor import sessions
from .modules.module import read_manifest
from .modules.module import get_manifest
_logger = logging.getLogger(__name__)
rpc_request = logging.getLogger(__name__ + '.rpc.request')
@@ -860,7 +860,6 @@ more details.
#----------------------------------------------------------
# Controller and route registration
#----------------------------------------------------------
addons_manifest = {}
controllers_per_module = collections.defaultdict(list)
class ControllerType(type):
@@ -1316,22 +1315,15 @@ class Root(object):
controllers and configure them. """
# TODO should we move this to ir.http so that only configured modules are served ?
statics = {}
manifests = addons_manifest
for addons_path in odoo.addons.__path__:
for module in sorted(os.listdir(str(addons_path))):
if module not in manifests:
# Deal with the manifest first
mod_path = opj(addons_path, module)
manifest = read_manifest(addons_path, module)
if not manifest or (not manifest.get('installable', True) and 'assets' not in manifest):
continue
manifest['addons_path'] = addons_path
manifests[module] = manifest
# Then deal with the statics
path_static = opj(addons_path, module, 'static')
if os.path.isdir(path_static):
_logger.debug("Loading %s", module)
statics['/%s/static' % module] = path_static
for module in os.listdir(addons_path):
manifest = get_manifest(module)
static_path = opj(addons_path, module, 'static')
if (manifest
and manifest['installable']
and manifest['assets']
and os.path.isdir(static_path)):
statics[f'/{module}/static'] = static_path
if statics:
_logger.info("HTTP Configuring static files")
+1 -1
View File
@@ -19,6 +19,6 @@ from odoo.modules.module import (
get_resource_path,
check_resource_path,
initialize_sys_path,
load_information_from_description_file,
get_manifest,
load_openerp_module,
)
+1 -1
View File
@@ -37,7 +37,7 @@ def initialize(cr):
continue
# This will raise an exception if no/unreadable descriptor file.
info = odoo.modules.load_information_from_description_file(i)
info = odoo.modules.get_manifest(i)
if not info:
continue
+1 -4
View File
@@ -58,10 +58,7 @@ class Graph(dict):
packages = []
len_graph = len(self)
for module in module_list:
# This will raise an exception if no/unreadable descriptor file.
# NOTE The call to load_information_from_description_file is already
# done by db.initialize, so it is possible to not do it again here.
info = odoo.modules.module.load_information_from_description_file(module)
info = odoo.modules.module.get_manifest(module)
if info and info['installable']:
packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version
elif module != 'studio_customization':
+101 -70
View File
@@ -3,6 +3,8 @@
import ast
import collections.abc
import copy
import functools
import importlib
import logging
import os
@@ -10,7 +12,7 @@ import pkg_resources
import re
import sys
import warnings
from os.path import join as opj
from os.path import join as opj, realpath
import odoo
import odoo.tools as tools
@@ -20,6 +22,42 @@ from odoo.tools import pycompat
MANIFEST_NAMES = ('__manifest__.py', '__openerp__.py')
README = ['README.rst', 'README.md', 'README.txt']
_DEFAULT_MANIFEST = {
#addons_path: f'/path/to/the/addons/path/of/{module}', # automatic
'application': False,
'bootstrap': False, # web
'assets': {},
'author': 'Odoo S.A.',
'auto_install': False,
'category': 'Uncategorized',
'data': [],
'demo': [],
'demo_xml': [],
'depends': [],
'description': '',
'external_dependencies': [],
#icon: f'/{module}/static/description/icon.png', # automatic
'init_xml': [],
'installable': True,
'images': [], # website
'images_preview_theme': {}, # website themes
#license, mandatory
'live_test_url': '', # website themes
#name, mandatory
'post_init_hook': '',
'post_load': None,
'pre_init_hook': '',
'sequence': 100,
'snippet_lists': {}, # website themes
'summary': '',
'test': [],
'update_xml': [],
'uninstall_hook': '',
'version': '1.0',
'web': False,
'website': '',
}
_logger = logging.getLogger(__name__)
# addons path as a list
@@ -276,14 +314,6 @@ def module_manifest(path):
if os.path.isfile(opj(path, manifest_name)):
return opj(path, manifest_name)
def read_manifest(addons_path, module):
mod_path = opj(addons_path, module)
manifest_path = module_manifest(mod_path)
if manifest_path:
with tools.file_open(manifest_path, 'r') as fd:
manifest_data = fd.read()
return ast.literal_eval(manifest_data)
def get_module_root(path):
"""
Get closest module's root beginning from path
@@ -311,74 +341,75 @@ def get_module_root(path):
path = new_path
return path
def load_information_from_description_file(module, mod_path=None):
"""
:param module: The name of the module (sale, purchase, ...)
:param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...)
"""
def load_manifest(module, mod_path=None):
""" Load the module manifest from the file system. """
if not mod_path:
mod_path = get_module_path(module, downloaded=True)
manifest_file = module_manifest(mod_path)
if manifest_file:
# default values for descriptor
info = {
'application': False,
'author': 'Odoo S.A.',
'auto_install': False,
'category': 'Uncategorized',
'depends': [],
'description': '',
'icon': get_module_icon(module),
'installable': True,
'post_load': None,
'version': '1.0',
'web': False,
'sequence': 100,
'summary': '',
'website': '',
}
info.update(zip(
'depends data demo test init_xml update_xml demo_xml'.split(),
iter(list, None)))
f = tools.file_open(manifest_file, mode='rb')
try:
info.update(ast.literal_eval(pycompat.to_text(f.read())))
finally:
f.close()
if not manifest_file:
_logger.debug('module %s: no manifest file found %s', module, MANIFEST_NAMES)
return {}
if not info.get('description'):
readme_path = [opj(mod_path, x) for x in README
if os.path.isfile(opj(mod_path, x))]
if readme_path:
with tools.file_open(readme_path[0]) as fd:
info['description'] = fd.read()
manifest = copy.deepcopy(_DEFAULT_MANIFEST)
manifest['icon'] = get_module_icon(module)
if not info.get('license'):
info['license'] = 'LGPL-3'
_logger.warning("Missing `license` key in manifest for '%s', defaulting to LGPL-3", module)
with tools.file_open(manifest_file, mode='r') as f:
manifest.update(ast.literal_eval(f.read()))
# auto_install is either `False` (by default) in which case the module
# is opt-in, either a list of dependencies in which case the module is
# automatically installed if all dependencies are (special case: [] to
# always install the module), either `True` to auto-install the module
# in case all dependencies declared in `depends` are installed.
if isinstance(info['auto_install'], collections.abc.Iterable):
info['auto_install'] = set(info['auto_install'])
non_dependencies = info['auto_install'].difference(info['depends'])
assert not non_dependencies,\
"auto_install triggers must be dependencies, found " \
"non-dependencies [%s] for module %s" % (
', '.join(non_dependencies), module
)
elif info['auto_install']:
info['auto_install'] = set(info['depends'])
if not manifest['description']:
readme_path = [opj(mod_path, x) for x in README
if os.path.isfile(opj(mod_path, x))]
if readme_path:
with tools.file_open(readme_path[0]) as fd:
manifest['description'] = fd.read()
info['version'] = adapt_version(info['version'])
return info
if not manifest.get('license'):
manifest['license'] = 'LGPL-3'
_logger.warning("Missing `license` key in manifest for %r, defaulting to LGPL-3", module)
_logger.debug('module %s: no manifest file found %s', module, MANIFEST_NAMES)
return {}
# auto_install is either `False` (by default) in which case the module
# is opt-in, either a list of dependencies in which case the module is
# automatically installed if all dependencies are (special case: [] to
# always install the module), either `True` to auto-install the module
# in case all dependencies declared in `depends` are installed.
if isinstance(manifest['auto_install'], collections.abc.Iterable):
manifest['auto_install'] = set(manifest['auto_install'])
non_dependencies = manifest['auto_install'].difference(manifest['depends'])
assert not non_dependencies,\
"auto_install triggers must be dependencies, found " \
"non-dependencies [%s] for module %s" % (
', '.join(non_dependencies), module
)
elif manifest['auto_install']:
manifest['auto_install'] = set(manifest['depends'])
manifest['version'] = adapt_version(manifest['version'])
manifest['addons_path'] = realpath(opj(mod_path, os.pardir))
return manifest
@functools.lru_cache(maxsize=None)
def get_manifest(module, mod_path=None):
"""
Get the module manifest.
:param str module: The name of the module (sale, purchase, ...).
:param Optional[str] mod_path: The optional path to the module on
the file-system. If not set, it is determined by scanning the
addons-paths.
:returns: The module manifest as a dict or an empty dict
when the manifest was not found.
:rtype: dict
"""
return load_manifest(module, mod_path)
def load_information_from_description_file(module, mod_path=None):
warnings.warn(
'load_information_from_description_file() is a deprecated '
'alias to get_manifest()', DeprecationWarning, stacklevel=2)
return get_manifest(module, mod_path)
def load_openerp_module(module_name):
""" Load an OpenERP module, if not already loaded.
@@ -398,7 +429,7 @@ def load_openerp_module(module_name):
# Call the module's post-load hook. This can done before any model or
# data has been initialized. This is ok as the post-load hook is for
# server-wide (instead of registry-specific) functionalities.
info = load_information_from_description_file(module_name)
info = get_manifest(module_name)
if info['post_load']:
getattr(sys.modules['odoo.addons.' + module_name], info['post_load'])()
@@ -440,7 +471,7 @@ def get_modules_with_version():
res = dict.fromkeys(modules, adapt_version('1.0'))
for module in modules:
try:
info = load_information_from_description_file(module)
info = get_manifest(module)
res[module] = info['version']
except Exception:
continue
+13
View File
@@ -455,6 +455,19 @@ class BaseCase(unittest.TestCase, metaclass=MetaCase):
else:
return self._assertRaises(exception, **kwargs)
if sys.version_info < (3, 10):
# simplified backport of assertNoLogs()
@contextmanager
def assertNoLogs(self, logger: str, level: str):
# assertLogs ensures there is at least one log record when
# exiting the context manager. We insert one dummy record just
# so we pass that silly test while still capturing the logs.
with self.assertLogs(logger, level) as capture:
logging.getLogger(logger).log(getattr(logging, level), "Dummy log record")
yield
if len(capture.output) > 1:
raise self.failureException(f"Unexpected logs found: {capture.output[1:]}")
@contextmanager
def assertQueries(self, expected, flush=True):
""" Check the queries made by the current cursor. ``expected`` is a list