Skip to content

Commit

Permalink
Package: do not derive from Module
Browse files Browse the repository at this point in the history
- Add `_pytest.python.PyFile`, used by `Module` and `Package`

- Add test from afaabdd, verifying fix for pytest-dev#7758
  • Loading branch information
blueyed committed Dec 1, 2021
1 parent 1a575bc commit 50db419
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 53 deletions.
112 changes: 59 additions & 53 deletions src/_pytest/python.py
Expand Up @@ -58,6 +58,8 @@
from _pytest.warning_types import PytestUnhandledCoroutineWarning

if TYPE_CHECKING:
from typing import Set

from _pytest._io import TerminalWriter


Expand Down Expand Up @@ -452,12 +454,60 @@ def _genfunctions(self, name, funcobj):
)


class Module(nodes.File, PyCollector):
""" Collector for test classes and functions. """

class PyFile(nodes.File):
def _getobj(self):
return self._importtestmodule()

def _importtestmodule(self) -> ModuleType:
# we assume we are only called once per module
importmode = self.config.getoption("--import-mode")
fspath = self.fspath
try:
mod = fspath.pyimport(ensuresyspath=importmode) # type: ModuleType
except SyntaxError:
raise self.CollectError(ExceptionInfo.from_current().getrepr(style="short"))
except fspath.ImportMismatchError as e:
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
)
except ImportError:
exc_info = ExceptionInfo.from_current()
if self.config.getoption("verbose") < 2:
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
exc_repr = (
exc_info.getrepr(style="short")
if exc_info.traceback
else exc_info.exconly()
)
formatted_tb = str(exc_repr)
raise self.CollectError(
"ImportError while importing test module '{fspath}'.\n"
"Hint: make sure your test modules/packages have valid Python names.\n"
"Traceback:\n"
"{traceback}".format(fspath=fspath, traceback=formatted_tb)
)
except _pytest.runner.Skipped as e:
if e.allow_module_level:
raise
raise self.CollectError(
"Using pytest.skip outside of a test is not allowed. "
"To decorate a test function, use the @pytest.mark.skip "
"or @pytest.mark.skipif decorators instead, and to skip a "
"module use `pytestmark = pytest.mark.{skip,skipif}."
)
self.config.pluginmanager.consider_module(mod)
return mod


class Module(PyFile, PyCollector):
"""Collector for test classes and functions in a module."""

def collect(self):
self._inject_setup_module_fixture()
self._inject_setup_function_fixture()
Expand Down Expand Up @@ -520,54 +570,10 @@ def xunit_setup_function_fixture(request):

self.obj.__pytest_setup_function = xunit_setup_function_fixture

def _importtestmodule(self) -> ModuleType:
# we assume we are only called once per module
importmode = self.config.getoption("--import-mode")
fspath = self.fspath
try:
mod = fspath.pyimport(ensuresyspath=importmode) # type: ModuleType
except SyntaxError:
raise self.CollectError(ExceptionInfo.from_current().getrepr(style="short"))
except fspath.ImportMismatchError as e:
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
)
except ImportError:
exc_info = ExceptionInfo.from_current()
if self.config.getoption("verbose") < 2:
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
exc_repr = (
exc_info.getrepr(style="short")
if exc_info.traceback
else exc_info.exconly()
)
formatted_tb = str(exc_repr)
raise self.CollectError(
"ImportError while importing test module '{fspath}'.\n"
"Hint: make sure your test modules/packages have valid Python names.\n"
"Traceback:\n"
"{traceback}".format(fspath=fspath, traceback=formatted_tb)
)
except _pytest.runner.Skipped as e:
if e.allow_module_level:
raise
raise self.CollectError(
"Using pytest.skip outside of a test is not allowed. "
"To decorate a test function, use the @pytest.mark.skip "
"or @pytest.mark.skipif decorators instead, and to skip a "
"module use `pytestmark = pytest.mark.{skip,skipif}."
)
self.config.pluginmanager.consider_module(mod)
return mod

class Package(PyFile, PyCollector):
"""Collector for modules in a package."""

class Package(Module):
def __init__(
self,
fspath: py.path.local,
Expand All @@ -586,7 +592,7 @@ def __init__(

self.name = fspath.dirname

def setup(self):
def setup(self) -> None:
# not using fixtures to call setup_module here because autouse fixtures
# from packages are not called automatically (#4085)
setup_module = _get_first_non_fixture_func(
Expand All @@ -608,14 +614,14 @@ def gethookproxy(self, fspath: py.path.local):
def isinitpath(self, path):
return path in self.session._initialpaths

def collect(self):
def collect(self) -> Iterable[Module]:
this_path = self.fspath.dirpath()
init_module = this_path.join("__init__.py")
if init_module.check(file=1) and path_matches_patterns(
init_module, self.config.getini("python_files")
):
yield Module.from_parent(self, fspath=init_module)
pkg_prefixes = set()
pkg_prefixes = set() # type: Set[str]
for path in this_path.visit(rec=self._recurse, bf=True, sort=True):
# We will visit our own __init__.py file, in which case we skip it.
is_file = path.isfile()
Expand All @@ -636,7 +642,7 @@ def collect(self):
# Broken symlink or invalid/missing file.
continue
elif path.join("__init__.py").check(file=1):
pkg_prefixes.add(path)
pkg_prefixes.add(str(path))


def _call_with_optional_argument(func, arg):
Expand Down
30 changes: 30 additions & 0 deletions testing/test_cacheprovider.py
Expand Up @@ -1037,6 +1037,36 @@ def test_pass(): pass
)
assert result.ret == 0

def test_packages(self, testdir: "Testdir") -> None:
"""Regression test for #7758.
The particular issue here was that Package nodes were included in the
filtering, being themselves Modules for the __init__.py, even if they
had failed Modules in them.
The tests includes a test in an __init__.py file just to make sure the
fix doesn't somehow regress that, it is not critical for the issue.
"""
testdir.makepyfile(
**{
"__init__.py": "",
"a/__init__.py": "def test_a_init(): assert False",
"a/test_one.py": "def test_1(): assert False",
"b/__init__.py": "",
"b/test_two.py": "def test_2(): assert False",
},
)
testdir.makeini(
"""
[pytest]
python_files = *.py
"""
)
result = testdir.runpytest()
result.assert_outcomes(failed=3)
result = testdir.runpytest("--lf")
result.assert_outcomes(failed=3)


class TestNewFirst:
def test_newfirst_usecase(self, testdir):
Expand Down

0 comments on commit 50db419

Please sign in to comment.