From 2be34154c10fe8705e9d02db5f4219045f1c4f74 Mon Sep 17 00:00:00 2001 From: Pierre Verkest Date: Thu, 3 Oct 2024 16:49:03 +0200 Subject: [PATCH] Support pytest8: mokey pathing odoo test python modue using odoo.addons namespace --- README.rst | 5 -- pytest_odoo.py | 110 +++++++------------------------------- setup.py | 2 +- tests/test_pytest_odoo.py | 59 +++++++++++++++++--- 4 files changed, 73 insertions(+), 103 deletions(-) diff --git a/README.rst b/README.rst index 67b52bc..f5ce2dd 100644 --- a/README.rst +++ b/README.rst @@ -51,8 +51,3 @@ You can use the ``ODOO_RC`` environment variable using an odoo configuration fil export ODOO_RC=/path/to/odoo/config.cfg pytest ... - -Known issues ------------- - -Currently not compatible with pytest >= 8.0.0 diff --git a/pytest_odoo.py b/pytest_odoo.py index 4bffeca..41479ea 100644 --- a/pytest_odoo.py +++ b/pytest_odoo.py @@ -1,24 +1,22 @@ -# -*- coding: utf-8 -*- # Copyright 2016 Camptocamp SA # Copyright 2015 Odoo +# @author Pierre Verkest # License AGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html) import ast import os import signal -import sys import threading from pathlib import Path from typing import Optional import _pytest -import _pytest._py.error as error import _pytest.python +import pytest + import odoo import odoo.tests -import pytest -from _pytest._code.code import ExceptionInfo def pytest_addoption(parser): @@ -88,7 +86,7 @@ def pytest_cmdline_main(config): raise Exception( "please provide a database name in the Odoo configuration file" ) - + monkey_patch_resolve_pkg_root_and_module_name() odoo.service.server.start(preload=[], stop=True) # odoo.service.server.start() modifies the SIGINT signal by its own # one which in fact prevents us to stop anthem with Ctrl-c. @@ -139,101 +137,31 @@ def enable_odoo_test_flag(): yield odoo.tools.config['test_enable'] = False +def monkey_patch_resolve_pkg_root_and_module_name(): + original_resolve_pkg_root_and_module_name = _pytest.pathlib.resolve_pkg_root_and_module_name -# Original code of xmo-odoo: -# https://github.com/odoo-dev/odoo/commit/95a131b7f4eebc6e2c623f936283153d62f9e70f -class OdooTestModule(_pytest.python.Module): - """ Should only be invoked for paths inside Odoo addons - """ - - def _importtestmodule(self): - # copy/paste/modified from original: removed sys.path injection & - # added Odoo module prefixing so import within modules is correct - try: - pypkgpath = self.fspath.pypkgpath() - pkgroot = pypkgpath.dirpath() - sep = self.fspath.sep - names = self.fspath.new(ext="").relto(pkgroot).split(sep) - if names[-1] == "__init__": - names.pop() - modname = ".".join(names) - # for modules in odoo/addons, since there is a __init__ the - # module name is already fully qualified (maybe?) - if (not modname.startswith('odoo.addons.') - and modname != 'odoo.addons' - and modname != 'odoo'): - modname = 'odoo.addons.' + modname - - __import__(modname) - mod = sys.modules[modname] - if self.fspath.basename == "__init__.py": - # we don't check anything as we might - # we in a namespace package ... too icky to check - return mod - modfile = mod.__file__ - if modfile[-4:] in ('.pyc', '.pyo'): - modfile = modfile[:-1] - elif modfile.endswith('$py.class'): - modfile = modfile[:-9] + '.py' - if modfile.endswith(os.path.sep + "__init__.py"): - if self.fspath.basename != "__init__.py": - modfile = modfile[:-12] - try: - issame = self.fspath.samefile(modfile) - except error.ENOENT: - issame = False - if not issame: - raise self.fspath.ImportMismatchError(modname, modfile, self) - except SyntaxError as e: - raise self.CollectError( - ExceptionInfo.from_current().getrepr(style="short") - ) from e - except self.fspath.ImportMismatchError: - e = sys.exc_info()[1] - raise self.CollectError( - "import file mismatch:\n" - "imported module %r has this __file__ attribute:\n" - " %s\n" - "which is not the same as the test file we want to collect:\n" - " %s\n" - "HINT: remove __pycache__ / .pyc files and/or use a " - "unique basename for your test file modules" % e.args - ) - self.config.pluginmanager.consider_module(mod) - return mod - - def __repr__(self): - return "" % (getattr(self, "name", None), ) - - -class OdooTestPackage(_pytest.python.Package, OdooTestModule): - """Package with odoo module lookup. - - Any python module inside the package will be imported with - the prefix `odoo.addons`. + def resolve_pkg_root_and_module_name( + path: Path, *, consider_namespace_packages: bool = False + ) -> "tuple[Path, str]": + pkg_root, module_name = original_resolve_pkg_root_and_module_name( + path, consider_namespace_packages=consider_namespace_packages + ) - This class is used to prevent loading odoo modules in duplicate, - which happens if a module is loaded with and without the prefix. - """ + if not module_name.startswith("odoo.addons"): + manifest = _find_manifest_path(path) + if manifest and manifest.parent.name == module_name.split(".",1)[0]: + module_name = "odoo.addons." + module_name + return pkg_root, module_name - def __repr__(self): - return "" % (getattr(self, "name", None), ) - -def pytest_pycollect_makemodule(module_path, path, parent): - if not _find_manifest_path(module_path): - return None - if path.basename == "__init__.py": - return OdooTestPackage.from_parent(parent, path=Path(path)) - else: - return OdooTestModule.from_parent(parent, path=Path(path)) + _pytest.pathlib.resolve_pkg_root_and_module_name= resolve_pkg_root_and_module_name def _find_manifest_path(collection_path: Path) -> Path: """Try to locate an Odoo manifest file in the collection path.""" # check if collection_path is an addon directory path = collection_path - for _ in range(0, 5): + for _ in range(5): if (path.parent / "__manifest__.py").is_file(): break path = path.parent diff --git a/setup.py b/setup.py index 71a44b9..f06300b 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ include_package_data=True, platforms='any', install_requires=[ - 'pytest>=7.2.0,<8.0.0', + "pytest>=8" ], setup_requires=[ 'setuptools_scm', diff --git a/tests/test_pytest_odoo.py b/tests/test_pytest_odoo.py index eefcac7..2a89100 100644 --- a/tests/test_pytest_odoo.py +++ b/tests/test_pytest_odoo.py @@ -1,21 +1,39 @@ -from unittest import TestCase import tempfile from contextlib import contextmanager -from pytest_odoo import _find_manifest_path from pathlib import Path +from unittest import TestCase + +from _pytest import pathlib as pytest_pathlib +from pytest_odoo import ( + _find_manifest_path, + monkey_patch_resolve_pkg_root_and_module_name, +) + class TestPytestOdoo(TestCase): @contextmanager - def fake_module(self): + def fake_module(self, with_manifest=True, using_addons_namespace=False): directory = tempfile.TemporaryDirectory() try: module_path = Path(directory.name) - manifest_path = module_path / "__manifest__.py" - manifest_path.touch() + files = [] + if using_addons_namespace: + files.append(module_path / "odoo" / "__init__.py") + files.append(module_path / "odoo" / "addons" / "__init__.py") + module_path = module_path / "odoo" / "addons" / "my_module" + module_path.mkdir(parents=True, exist_ok=True) + manifest_path = None + if with_manifest: + manifest_path = module_path / "__manifest__.py" + files.append(manifest_path) test_path = module_path / "tests" / "test_module.py" test_path.parent.mkdir(parents=True, exist_ok=True) - test_path.touch() + files.append(test_path) + files.append(module_path / "__init__.py") + files.append(module_path / "tests" / "__init__.py") + for file_path in files: + file_path.touch() yield (module_path, manifest_path, test_path,) finally: directory.cleanup() @@ -37,3 +55,32 @@ def test_find_manifest_path_from_brother(self): test = module_path / "test_something.py" test.touch() self.assertEqual(_find_manifest_path(test), manifest_path) + + def test_resolve_pkg_root_and_module_name(self): + monkey_patch_resolve_pkg_root_and_module_name() + with self.fake_module() as (module_path, _, test_path): + pkg_root, module_name = pytest_pathlib.resolve_pkg_root_and_module_name(test_path) + self.assertEqual( + module_name, + f"odoo.addons.{module_path.name}.tests.test_module" + ) + + def test_resolve_pkg_root_and_module_name_not_odoo_module(self): + monkey_patch_resolve_pkg_root_and_module_name() + + with self.fake_module(with_manifest=False) as (module_path, _, test_path): + pkg_root, module_name = pytest_pathlib.resolve_pkg_root_and_module_name(test_path) + self.assertEqual( + module_name, + f"{module_path.name}.tests.test_module" + ) + + def test_resolve_pkg_root_and_module_name_namespace_ok(self): + monkey_patch_resolve_pkg_root_and_module_name() + + with self.fake_module(with_manifest=True, using_addons_namespace=True) as (module_path, _, test_path): + pkg_root, module_name = pytest_pathlib.resolve_pkg_root_and_module_name(test_path) + self.assertEqual( + module_name, + "odoo.addons.my_module.tests.test_module" + )