Skip to content

Commit

Permalink
Support pytest>=8: monkey patching pytest resolving odoo python test …
Browse files Browse the repository at this point in the history
…module

...name ensuring proper odoo.addons namespace
  • Loading branch information
petrus-v committed Oct 6, 2024
1 parent c87b3f0 commit afa9cd9
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 103 deletions.
5 changes: 0 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
110 changes: 19 additions & 91 deletions pytest_odoo.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
# -*- coding: utf-8 -*-
# Copyright 2016 Camptocamp SA
# Copyright 2015 Odoo
# @author Pierre Verkest <[email protected]>
# 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):
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
59 changes: 53 additions & 6 deletions tests/test_pytest_odoo.py
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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"
)

0 comments on commit afa9cd9

Please sign in to comment.