From 7623ef83bf2e05a7d9926e2d5cd591105b614fa3 Mon Sep 17 00:00:00 2001 From: Solaris-star <820622658@qq.com> Date: Tue, 21 Jul 2026 11:26:15 +0800 Subject: [PATCH 1/2] fix: propagate package-level pytestmark from __init__.py After Package stopped being a Module/File, pytestmark defined in a package __init__.py was no longer applied to contained tests, so package-level skip/xfail markers were ignored. Load __init__.py during Package.collect and extend own_markers/keywords from get_unpacked_marks, matching Module/PyCollector behaviour. Fixes #14737 --- src/_pytest/python.py | 12 ++++++++++++ testing/test_mark.py | 17 +++++++++++++++++ 2 files changed, 29 insertions(+) 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..42c178a22cc 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1377,3 +1377,20 @@ 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\n" + "pytestmark = 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) From 10fbfb870fb45e19e699c24c498c045dccc794b7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 21 Jul 2026 03:26:40 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- testing/test_mark.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/testing/test_mark.py b/testing/test_mark.py index 42c178a22cc..b53a2a8a462 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1380,16 +1380,14 @@ def test_something(): def test_package_level_pytestmark_propagates(pytester: Pytester) -> None: - """pytestmark in package __init__.py must apply to tests in the package (#14737).""" + """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\n" - "pytestmark = pytest.mark.skip(reason=\"package is disabled\")\n", + '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", + "def test_should_be_skipped():\n assert False\n", encoding="utf-8", ) result = pytester.runpytest("-q")