18.0 vanilla

This commit is contained in:
Ernad Husremovic 2025-10-03 18:06:50 +02:00
parent d72e748793
commit 0a7ae8db93
337 changed files with 399651 additions and 232598 deletions

View file

@ -6,20 +6,35 @@ import collections.abc
import copy
import functools
import importlib
import importlib.metadata
import logging
import os
import pkg_resources
import re
import sys
import traceback
import warnings
from os.path import join as opj, normpath
import odoo
import odoo.tools as tools
import odoo.release as release
from odoo.tools import pycompat
from odoo.tools.misc import file_path
try:
from packaging.requirements import InvalidRequirement, Requirement
except ImportError:
class InvalidRequirement(Exception):
...
class Requirement:
def __init__(self, pydep):
if not re.fullmatch(r'[\w\-]+', pydep): # check that we have no versions or marker in pydep
msg = f"Package `packaging` is required to parse `{pydep}` external dependency and is not installed"
raise Exception(msg)
self.marker = None
self.specifier = None
self.name = pydep
MANIFEST_NAMES = ('__manifest__.py', '__openerp__.py')
README = ['README.rst', 'README.md', 'README.txt']
@ -32,6 +47,7 @@ _DEFAULT_MANIFEST = {
'author': 'Odoo S.A.',
'auto_install': False,
'category': 'Uncategorized',
'cloc_exclude': [],
'configurator_snippets': {}, # website themes
'countries': [],
'data': [],
@ -62,6 +78,17 @@ _DEFAULT_MANIFEST = {
'website': '',
}
# matches field definitions like
# partner_id: base.ResPartner = fields.Many2one
# partner_id = fields.Many2one[base.ResPartner]
TYPED_FIELD_DEFINITION_RE = re.compile(r'''
\b (?P<field_name>\w+) \s*
(:\s*(?P<field_type>[^ ]*))? \s*
= \s*
fields\.(?P<field_class>Many2one|One2many|Many2many)
(\[(?P<type_param>[^\]]+)\])?
''', re.VERBOSE)
_logger = logging.getLogger(__name__)
@ -103,7 +130,7 @@ def initialize_sys_path():
# hook odoo.addons on addons paths
for ad in tools.config['addons_path'].split(','):
ad = os.path.normcase(os.path.abspath(tools.ustr(ad.strip())))
ad = os.path.normcase(os.path.abspath(ad.strip()))
if ad not in odoo.addons.__path__:
odoo.addons.__path__.append(ad)
@ -116,7 +143,7 @@ def initialize_sys_path():
from odoo import upgrade
legacy_upgrade_path = os.path.join(base_path, 'base', 'maintenance', 'migrations')
for up in (tools.config['upgrade_path'] or legacy_upgrade_path).split(','):
up = os.path.normcase(os.path.abspath(tools.ustr(up.strip())))
up = os.path.normcase(os.path.abspath(up.strip()))
if os.path.isdir(up) and up not in upgrade.__path__:
upgrade.__path__.append(up)
@ -155,39 +182,6 @@ def get_module_path(module, downloaded=False, display_warning=True):
_logger.warning('module %s: module not found', module)
return False
def get_module_filetree(module, dir='.'):
warnings.warn(
"Since 16.0: use os.walk or a recursive glob or something",
DeprecationWarning,
stacklevel=2
)
path = get_module_path(module)
if not path:
return False
dir = os.path.normpath(dir)
if dir == '.':
dir = ''
if dir.startswith('..') or (dir and dir[0] == '/'):
raise Exception('Cannot access file outside the module')
files = odoo.tools.osutil.listdir(path, True)
tree = {}
for f in files:
if not f.startswith(dir):
continue
if dir:
f = f[len(dir)+int(not dir.endswith('/')):]
lst = f.split(os.sep)
current = tree
while len(lst) != 1:
current = current.setdefault(lst.pop(0), {})
current[lst.pop(0)] = None
return tree
def get_resource_path(module, *args):
"""Return the full path of a resource of the given module.
@ -372,11 +366,6 @@ def get_manifest(module, mod_path=None):
def _get_manifest_cached(module, mod_path=None):
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.
@ -401,6 +390,24 @@ def load_openerp_module(module_name):
if info['post_load']:
getattr(sys.modules[qualname], info['post_load'])()
except AttributeError as err:
_logger.critical("Couldn't load module %s", module_name)
trace = traceback.format_exc()
match = TYPED_FIELD_DEFINITION_RE.search(trace)
if match and "most likely due to a circular import" in trace:
field_name = match['field_name']
field_class = match['field_class']
field_type = match['field_type'] or match['type_param']
if "." not in field_type:
field_type = f"{module_name}.{field_type}"
raise AttributeError(
f"{err}\n"
"To avoid circular import for the the comodel use the annotation syntax:\n"
f" {field_name}: {field_type} = fields.{field_class}(...)\n"
"and add at the beggining of the file:\n"
" from __future__ import annotations"
).with_traceback(err.__traceback__) from None
raise
except Exception:
_logger.critical("Couldn't load module %s", module_name)
raise
@ -464,21 +471,31 @@ current_test = False
def check_python_external_dependency(pydep):
try:
pkg_resources.get_distribution(pydep)
except pkg_resources.DistributionNotFound as e:
requirement = Requirement(pydep)
except InvalidRequirement as e:
msg = f"{pydep} is an invalid external dependency specification: {e}"
raise Exception(msg) from e
if requirement.marker and not requirement.marker.evaluate():
_logger.debug(
"Ignored external dependency %s because environment markers do not match",
pydep
)
return
try:
version = importlib.metadata.version(requirement.name)
except importlib.metadata.PackageNotFoundError as e:
try:
# keep compatibility with module name but log a warning instead of info
importlib.import_module(pydep)
_logger.info("python external dependency on '%s' does not appear to be a valid PyPI package. Using a PyPI package name is recommended.", pydep)
_logger.warning("python external dependency on '%s' does not appear o be a valid PyPI package. Using a PyPI package name is recommended.", pydep)
return
except ImportError:
# backward compatibility attempt failed
_logger.warning("DistributionNotFound: %s", e)
raise Exception('Python library not installed: %s' % (pydep,))
except pkg_resources.VersionConflict as e:
_logger.warning("VersionConflict: %s", e)
raise Exception('Python library version conflict: %s' % (pydep,))
except Exception as e:
_logger.warning("get_distribution(%s) failed: %s", pydep, e)
raise Exception('Error finding python library %s' % (pydep,))
pass
msg = f"External dependency {pydep} not installed: {e}"
raise Exception(msg) from e
if requirement.specifier and not requirement.specifier.contains(version):
msg = f"External dependency version mismatch: {pydep} (installed: {version})"
raise Exception(msg)
def check_manifest_dependencies(manifest):