Odoo18-Base/odoo/addons/test_lint/tests/test_override_signatures.py
2025-01-06 10:57:38 +07:00

165 lines
6.6 KiB
Python

import collections
import inspect
import itertools
import odoo
from odoo.modules.registry import Registry
from odoo.tests.common import get_db_name, tagged
from .lint_case import LintCase
POSITIONAL_ONLY = inspect.Parameter.POSITIONAL_ONLY
POSITIONAL_OR_KEYWORD = inspect.Parameter.POSITIONAL_OR_KEYWORD
VAR_POSITIONAL = inspect.Parameter.VAR_POSITIONAL
KEYWORD_ONLY = inspect.Parameter.KEYWORD_ONLY
VAR_KEYWORD = inspect.Parameter.VAR_KEYWORD
EMPTY = inspect.Parameter.empty
failure_message = """\
Invalid override of {model}.{method} in {child_module}, {message}.
Original definition in {parent_module}:
def {method}{original_signature}
Incompatible definition in {child_module}:
def {method}{override_signature}"""
methods_to_sanitize = {
method_name
for method_name in dir(odoo.models.BaseModel)
if not method_name.startswith('_')
} - {
# Not yet sanitized...
'write', 'create', 'default_get', 'init'
}
class HitMiss:
def __init__(self):
self.hit = 0
self.miss = 0
@property
def ratio(self):
return self.hit / (self.hit + self.miss)
counter = collections.defaultdict(HitMiss)
def get_odoo_module_name(python_module_name):
if python_module_name.startswith('odoo.addons.'):
return python_module_name.split('.')[2]
if python_module_name == 'odoo.models':
return 'odoo'
return python_module_name
def assert_valid_override(parent_signature, child_signature):
pparams = parent_signature.parameters
cparams = child_signature.parameters
# parent and child have exact same signature
if pparams == cparams:
return
parent_is_annotated = any(p.annotation is not inspect._empty for p in pparams.values())
child_is_annotated = any(p.annotation is not inspect._empty for p in cparams.values())
# don't check annotations when one of the two methods is not annotated
if parent_is_annotated != child_is_annotated:
pparams = {name: param.replace(annotation=EMPTY) for name, param in pparams.items()}
cparams = {name: param.replace(annotation=EMPTY) for name, param in cparams.items()}
parent_is_annotated = child_is_annotated = False
# parent and child have exact same signature, modulo annotations
if pparams == cparams:
return
# parent has *args/**kwargs: child can define new custom args/kwargs
parent_has_varargs = any(pp.kind == VAR_POSITIONAL for pp in pparams.values())
parent_has_varkwargs = any(pp.kind == VAR_KEYWORD for pp in pparams.values())
# child has *args/**kwargs: all unknown args/kargs are delegated
child_has_varargs = any(cp.kind == VAR_POSITIONAL for cp in cparams.values())
child_has_varkwargs = any(cp.kind == VAR_KEYWORD for cp in cparams.values())
# check positionals
pposparams = [(pp_name, pp) for pp_name, pp in pparams.items()
if pp.kind in (POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD)]
cposparams = [(cp_name, cp) for cp_name, cp in cparams.items()
if cp.kind in (POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD)]
if len(cposparams) < len(pposparams):
assert child_has_varargs, "missing positional parameters"
assert cposparams == pposparams[:len(cposparams)], "wrong positional parameters"
elif len(cposparams) > len(pposparams):
assert parent_has_varargs, "too many positional parameters"
assert cposparams[:len(pposparams)] == pposparams, "wrong positional parameters"
else:
assert cposparams == pposparams, "wrong positional parameters"
# check keywords
pkwparams = {(pp_name, pp) for pp_name, pp in pparams.items()
if pp.kind in (POSITIONAL_OR_KEYWORD, KEYWORD_ONLY)}
ckwparams = {(cp_name, cp) for cp_name, cp in cparams.items()
if cp.kind in (POSITIONAL_OR_KEYWORD, KEYWORD_ONLY)}
if ckwparams < pkwparams:
assert child_has_varkwargs, "missing keyword parameters"
elif ckwparams > pkwparams:
assert parent_has_varkwargs, "too many keyword parameters"
elif ckwparams != pkwparams:
assert child_has_varkwargs and parent_has_varkwargs, "wrong keyword parameters"
@tagged('-at_install', 'post_install')
class TestLintOverrideSignatures(LintCase):
def test_lint_override_signature(self):
self.failureException = TypeError
registry = Registry(get_db_name())
for model_name, model_cls in registry.items():
for method_name, _ in inspect.getmembers(model_cls, inspect.isroutine):
if method_name not in methods_to_sanitize:
continue
# Find the original function definition
reverse_mro = reversed(model_cls.mro()[1:-1])
for parent_class in reverse_mro:
method = getattr(parent_class, method_name, None)
if callable(method):
break
parent_module = get_odoo_module_name(parent_class.__module__)
original_signature = inspect.signature(method)
# Assert that all child classes correctly override the method
for child_class in reverse_mro:
if method_name not in child_class.__dict__:
continue
override = getattr(child_class, method_name)
child_module = get_odoo_module_name(child_class.__module__)
override_signature = inspect.signature(override)
with self.subTest(module=child_module, model=model_name, method=method_name):
try:
assert_valid_override(original_signature, override_signature)
counter[method_name].hit += 1
except AssertionError as exc:
counter[method_name].miss += 1
msg = failure_message.format(
message=exc.args[0],
model=model_name,
method=method_name,
child_module=child_module,
parent_module=parent_module,
original_signature=original_signature,
override_signature=override_signature,
)
raise TypeError(msg) from None
#with open('/tmp/odoo-override-signatures.md', 'w') as f:
# f.write('method|hit|miss|ratio\n')
# f.write('------|---|----|-----\n')
# for method_name, hm in sorted(counter.items(), key=lambda x: x[1].miss):
# f.write(f'{method_name}|{hm.hit}|{hm.miss}|{hm.ratio:.3f}\n')