diff --git a/src/_pytest/python.py b/src/_pytest/python.py index eb481393ba1..cfc76a73b94 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -693,6 +693,18 @@ def setup(self) -> None: self.addfinalizer(func) def collect(self) -> Iterable[nodes.Item | nodes.Collector]: + # Apply package-level pytestmark from __init__.py so markers propagate + # to contained tests (regression from Package no longer being a Module). + try: + init_mod = importtestmodule(self.path / "__init__.py", self.config) + except nodes.Collector.CollectError: + init_mod = None + if init_mod is not None and not getattr(self, "_package_marks_applied", False): + marks = get_unpacked_marks(init_mod) + self.own_markers.extend(marks) + self.keywords.update((mark.name, mark) for mark in marks) + self._package_marks_applied = True # type: ignore[attr-defined] + # Always collect __init__.py first. def sort_key(entry: os.DirEntry[str]) -> object: return (entry.name != "__init__.py", entry.name) diff --git a/testing/test_mark.py b/testing/test_mark.py index 253cda94503..b53a2a8a462 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1377,3 +1377,18 @@ def test_something(): # The module is buggy (__getattr__ returns None for all attributes), # so no tests are collected, but pytest should NOT crash with a TypeError. assert result.ret != ExitCode.INTERNAL_ERROR + + +def test_package_level_pytestmark_propagates(pytester: Pytester) -> None: + """Pytestmark in package __init__.py must apply to tests in the package (#14737).""" + pkg = pytester.mkpydir("skippedpkg") + pkg.joinpath("__init__.py").write_text( + 'import pytest\npytestmark = pytest.mark.skip(reason="package is disabled")\n', + encoding="utf-8", + ) + pkg.joinpath("test_skipped.py").write_text( + "def test_should_be_skipped():\n assert False\n", + encoding="utf-8", + ) + result = pytester.runpytest("-q") + result.assert_outcomes(skipped=1)