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

@ -1,68 +0,0 @@
from typing import Optional
import astroid
from pylint import interfaces, checkers
try:
from pylint.checkers.utils import only_required_for_messages
except ImportError:
from pylint.checkers.utils import check_messages as only_required_for_messages
class OdooBaseChecker(checkers.BaseChecker):
__implements__ = interfaces.IAstroidChecker
name = 'odoo'
msgs = {
'E8504': (
'The Markup constructor called with a non-constant argument',
'non-const-markup',
'',
)
}
@only_required_for_messages('non-const-markup')
def visit_call(self, node):
if (isinstance(node.func, astroid.Name) and
node.func.name == "Markup" and
not self._is_constant(node.args[0])):
self.add_message('non-const-markup', node=node, col_offset=len(node.as_string().split('\\n')))
elif (isinstance(node.func, astroid.Attribute) and
node.func.attrname == "Markup" and
not self._is_constant(node.args[0])):
self.add_message('non-const-markup', node=node, col_offset=len(node.as_string().split('\\n')))
def _is_constant(self, node: Optional[astroid.node_classes.NodeNG]) -> bool:
if isinstance(node, astroid.Const) or node is None:
return True
elif isinstance(node, astroid.JoinedStr):
return all(map(self._is_constant, node.values))
elif isinstance(node, astroid.FormattedValue):
return self._is_constant(node.value)
elif isinstance(node, astroid.Name):
_, assignments = node.lookup(node.name)
return all(map(self._is_constant, assignments))
elif isinstance(node, astroid.AssignName):
return self._is_constant(node.parent)
elif isinstance(node, astroid.Assign):
return self._is_constant(node.value)
elif (isinstance(node, astroid.Call) and
isinstance(node.func, astroid.Attribute) and
node.func.attrname in ["format", "join"]):
return (self._is_constant(node.func.expr) and
all(map(self._is_constant, node.args)) and
all(map(self._is_constant, node.keywords)))
elif isinstance(node, astroid.Keyword):
return self._is_constant(node.value)
elif isinstance(node, (astroid.List, astroid.Set, astroid.Tuple)):
return all(map(self._is_constant, node.elts))
elif isinstance(node, astroid.Dict):
return all(map(self._is_constant, node.values))
elif isinstance(node, astroid.BinOp):
return self._is_constant(node.left) and self._is_constant(node.right)
elif isinstance(node, astroid.IfExp):
return self._is_constant(node.body) and self._is_constant(node.orelse)
return False
def register(linter):
linter.register_checker(OdooBaseChecker(linter))

File diff suppressed because it is too large Load diff

View file

@ -3,13 +3,13 @@
The module :mod:`odoo.tests.form` provides an implementation of a client form
view for server-side unit tests.
"""
from __future__ import annotations
import ast
import collections
import itertools
import logging
import time
from datetime import datetime, date
from dateutil.relativedelta import relativedelta
from lxml import etree
@ -96,15 +96,13 @@ class Form:
the view in "creation" mode from default values, while a
singleton will put it in "edit" mode and only load the
view's data.
:type record: odoo.models.Model
:param view: the id, xmlid or actual view object to use for onchanges and
view constraints. If none is provided, simply loads the
default view for the model.
:type view: int | str | odoo.model.Model
.. versionadded:: 12.0
"""
def __init__(self, record, view=None):
def __init__(self, record: BaseModel, view: None | int | str | BaseModel = None) -> None:
assert isinstance(record, BaseModel)
assert len(record) <= 1
@ -123,7 +121,7 @@ class Form:
views = record.get_views([(view_id, 'form')])
object.__setattr__(self, '_models_info', views['models'])
# self._models_info = {model_name: {field_name: field_info}}
# self._models_info = {model_name: {fields: {field_name: field_info}}}
tree = etree.fromstring(views['views']['form']['arch'])
view = self._process_view(tree, record)
object.__setattr__(self, '_view', view)
@ -143,6 +141,31 @@ class Form:
else:
self._init_from_defaults()
@classmethod
def from_action(cls, env: odoo.api.Environment, action: dict) -> Form:
assert action['type'] == 'ir.actions.act_window', \
f"only window actions are valid, got {action['type']}"
# ensure the first-requested view is a form view
if views := action.get('views'):
assert views[0][1] == 'form', \
f"the actions dict should have a form as first view, got {views[0][1]}"
view_id = views[0][0]
else:
view_mode = action.get('view_mode', '')
if not view_mode.startswith('form'):
raise ValueError(f"The actions dict should have a form first view mode, got {view_mode}")
view_id = action.get('view_id')
if view_id and ',' in view_mode:
raise ValueError(f"A `view_id` is only valid if the action has a single `view_mode`, got {view_mode}")
context = action.get('context', {})
if isinstance(context, str):
context = ast.literal_eval(context)
record = env[action['res_model']]\
.with_context(context)\
.browse(action.get('res_id'))
return cls(record, view_id)
def _process_view(self, tree, model, level=2):
""" Post-processes to augment the view_get with:
* an id field (may not be present if not in the view but needed)
@ -160,7 +183,7 @@ class Form:
field_name = node.get('name')
# add field_info into fields
field_info = self._models_info.get(model._name, {}).get(field_name) or {'type': None}
field_info = self._models_info.get(model._name, {}).get("fields", {}).get(field_name) or {'type': None}
fields[field_name] = field_info
fields_spec[field_name] = field_spec = {}
@ -251,7 +274,7 @@ class Form:
views = {
view.tag: view for view in node.xpath('./*[descendant::field]')
}
for view_type in ['tree', 'form']:
for view_type in ['list', 'form']:
if view_type in views:
continue
if field_info['invisible'] == 'True':
@ -263,14 +286,17 @@ class Form:
subnode = etree.fromstring(subviews['views'][view_type]['arch'])
views[view_type] = subnode
node.append(subnode)
for model_name, fields in subviews['models'].items():
self._models_info.setdefault(model_name, {}).update(fields)
for model_name, value in subviews['models'].items():
model_info = self._models_info.setdefault(model_name, {})
if "fields" not in model_info:
model_info["fields"] = {}
model_info["fields"].update(value["fields"])
# pick the first editable subview
view_type = next(
vtype for vtype in node.get('mode', 'tree').split(',') if vtype != 'form'
vtype for vtype in node.get('mode', 'list').split(',') if vtype != 'form'
)
if not (view_type == 'tree' and views['tree'].get('editable')):
if not (view_type == 'list' and views['list'].get('editable')):
view_type = 'form'
# don't recursively process o2ms in o2ms

View file

@ -1,11 +1,11 @@
import importlib
import importlib.util
import inspect
import itertools
import logging
import sys
import threading
import unittest
from pathlib import Path
from unittest import case
from .. import tools
from .tag_selector import TagsSelector
@ -13,9 +13,41 @@ from .suite import OdooSuite
from .result import OdooTestResult
_logger = logging.getLogger(__name__)
def get_module_test_cases(module):
"""Return a suite of all test cases contained in the given module"""
for obj in module.__dict__.values():
if not isinstance(obj, type):
continue
if not issubclass(obj, case.TestCase):
continue
if obj.__module__ != module.__name__:
continue
test_case_class = obj
test_cases = test_case_class.__dict__.items()
if getattr(test_case_class, 'allow_inherited_tests_method', False):
# keep iherited method for specific classes.
# This is likely to be removed once a better solution is found
test_cases = inspect.getmembers(test_case_class, callable)
else:
# sort test case to keep the initial behaviour.
# This is likely to be removed in the future
test_cases = sorted(test_cases, key=lambda pair: pair[0])
for method_name, method in test_cases:
if not callable(method):
continue
if not method_name.startswith('test'):
continue
yield test_case_class(method_name)
def get_test_modules(module):
""" Return a list of module for the addons potentially containing tests to
feed unittest.TestLoader.loadTestsFromModule() """
feed get_module_test_cases() """
results = _get_tests_modules(importlib.util.find_spec(f'odoo.addons.{module}'))
results += list(_get_upgrade_test_modules(module))
@ -70,13 +102,13 @@ def make_suite(module_names, position='at_install'):
t
for module_name in module_names
for m in get_test_modules(module_name)
for t in unwrap_suite(unittest.TestLoader().loadTestsFromModule(m))
for t in get_module_test_cases(m)
if position_tag.check(t) and config_tags.check(t)
)
return OdooSuite(sorted(tests, key=lambda t: t.test_sequence))
def run_suite(suite, module_name=None, global_report=None):
def run_suite(suite, global_report=None):
# avoid dependency hell
from ..modules import module
module.current_test = True
@ -88,29 +120,3 @@ def run_suite(suite, module_name=None, global_report=None):
threading.current_thread().testing = False
module.current_test = False
return results
def unwrap_suite(test):
"""
Attempts to unpack testsuites (holding suites or cases) in order to
generate a single stream of terminals (either test cases or customized
test suites). These can then be checked for run/skip attributes
individually.
An alternative would be to use a variant of @unittest.skipIf with a state
flag of some sort e.g. @unittest.skipIf(common.runstate != 'at_install'),
but then things become weird with post_install as tests should *not* run
by default there
"""
if isinstance(test, unittest.TestCase):
yield test
return
subtests = list(test)
## custom test suite (no test cases)
#if not len(subtests):
# yield test
# return
for item in itertools.chain.from_iterable(unwrap_suite(t) for t in subtests):
yield item

View file

@ -10,6 +10,7 @@ sys.path.append(os.path.abspath(os.path.join(__file__,'../../../')))
import odoo
from odoo.tools import config, topological_sort, unique
from odoo.modules.registry import Registry
from odoo.netsvc import init_logger
from odoo.tests import standalone_tests
import odoo.tests.loader
@ -23,11 +24,12 @@ BLACKLIST = {
IGNORE = ('hw_', 'theme_', 'l10n_', 'test_')
INSTALL_BLACKLIST = {
'payment_alipay', 'payment_ogone', 'payment_payulatam', 'payment_payumoney',
'payment_alipay', 'payment_payulatam', 'payment_payumoney',
} # deprecated modules (cannot be installed manually through button_install anymore)
def install(db_name, module_id, module_name):
with odoo.registry(db_name).cursor() as cr:
with Registry(db_name).cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
module = env['ir.module.module'].browse(module_id)
module.button_immediate_install()
@ -35,7 +37,7 @@ def install(db_name, module_id, module_name):
def uninstall(db_name, module_id, module_name):
with odoo.registry(db_name).cursor() as cr:
with Registry(db_name).cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
module = env['ir.module.module'].browse(module_id)
module.button_immediate_uninstall()
@ -124,7 +126,7 @@ class StandaloneAction(argparse.Action):
def test_cycle(args):
""" Test full install/uninstall/reinstall cycle for all modules """
with odoo.registry(args.database).cursor() as cr:
with Registry(args.database).cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
def valid(module):
@ -159,7 +161,7 @@ def test_cycle(args):
def test_uninstall(args):
""" Tries to uninstall/reinstall one ore more modules"""
for module_name in args.uninstall.split(','):
with odoo.registry(args.database).cursor() as cr:
with Registry(args.database).cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
module = env['ir.module.module'].search([('name', '=', module_name)])
module_id, module_state = module.id, module.state
@ -178,7 +180,7 @@ def test_standalone(args):
""" Tries to launch standalone scripts tagged with @post_testing """
odoo.service.db._check_faketime_mode(args.database) # noqa: SLF001
# load the registry once for script discovery
registry = odoo.registry(args.database)
registry = Registry(args.database)
for module_name in registry._init_modules:
# import tests for loaded modules
odoo.tests.loader.get_test_modules(module_name)
@ -192,7 +194,7 @@ def test_standalone(args):
start_time = time.time()
for index, func in enumerate(funcs, start=1):
with odoo.registry(args.database).cursor() as cr:
with Registry(args.database).cursor() as cr:
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
_logger.info("Executing standalone script: %s (%d / %d)",
func.__name__, index, len(funcs))

View file

@ -1,18 +0,0 @@
from os import environ, path
import subprocess
import sys
#This test is meant to be standalone, correct usage : python test_security.py file1 file2 file3 ...
if __name__ == '__main__':
HERE = path.dirname(__file__)
if 'PYTHONPATH' not in environ:
environ['PYTHONPATH'] = HERE
else:
environ['PYTHONPATH'] += ':' + HERE
command = ['pylint', '--rcfile=/dev/null', '--disable=all', '--output-format', 'json', '--enable=non-const-markup', '--reports=n', '--load-plugins=_odoo_checker_markup', *sys.argv[1:]]
proc = subprocess.run(command, env=environ, check=True)