Repository: camptocamp/pytest-odoo (language: python)

Issue:
[FIX] _find_manifest_path with directory less than 5 deep if no manifest found
This happens if the test module is less than 5 directories deep from root.


Discussion / hints:
hi @simahawk do you mind to activate the workflow in order to make sure test are running properly ?

I'm wondering why this project require maintainer approval to execute such test, do you worries about abuses or a missed configuration?

Fix patch:
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/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',


Test patch:
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 <pierre@verkest.fr>
 # 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 "<Module %r>" % (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 "<Package %r>" % (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/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"
+            )