Skip to content

python: propagate package-level pytestmark from __init__.py - #14917

Closed
aryansk wants to merge 1 commit into
pytest-dev:mainfrom
aryansk:fix/package-pytestmark-14737
Closed

python: propagate package-level pytestmark from __init__.py#14917
aryansk wants to merge 1 commit into
pytest-dev:mainfrom
aryansk:fix/package-pytestmark-14737

Conversation

@aryansk

@aryansk aryansk commented Aug 20, 2026

Copy link
Copy Markdown

Problem

A pytestmark = pytest.mark.skip(...) (or xfail, etc.) defined in a package's __init__.py no longer propagates to tests inside that package. Tests that should be skipped run and fail instead.

Minimal reproduction from #14737:

from pathlib import Path
import pytest
root = Path("/tmp/pytest_pkg_skip_test")
package = root / "skippedpkg"
package.mkdir(parents=True)
(package / "__init__.py").write_text("import pytest\npytestmark = pytest.mark.skip(reason='package is disabled')\n")
(package / "test_skipped.py").write_text("def test_should_be_skipped():\n    assert False\n")
pytest.main(["-q", str(package)])  # expected 1 skipped, got 1 failed

This is a consequence of #6197. Before #6197, Package was a Module and its __init__.py was loaded via PyobjMixin.obj which populated own_markers via get_unpacked_marks. #6197 made Package a Directory to fix incorrect discovery of non-test __init__.py files (#6194) and removed that marking path, without replacement.

Fixes #14737.

Change

In src/_pytest/python.py Package.collect, import the package's __init__.py via importtestmodule (already used in Package.setup for setup_module), extract marks with get_unpacked_marks, and extend self.own_markers / self.keywords before yielding children. Children inherit via Node.iter_markers_with_node.

  • Do it in collect (not setup) so marks are visible during collection and before pytest_runtest_setup (the skipping plugin is tryfirst, while Package.setup runs after).
  • Snapshot and restore sys.modules / sys.path when no marks are applied to avoid polluting unrelated imports (e.g. src/nope in test_does_not_put_src_on_path). When marks are applied we keep the import so Package.setup does not need to re-execute __init__.py.
  • Guard against double-collect with _pytestmark_applied.

No new collection of __init__.py as a test file, so #6194 is not reintroduced.

Why this approach

  • Restores the pre-Fix incorrect discovery of non-test __init__.py files. #6197 expectation (package-level marks propagate) without re-collecting __init__.py.
  • Mirrors how PyobjMixin and FunctionDefinition handle pytestmark (via get_unpacked_marksown_markers / keywords).
  • setup is too late for skip/xfail evaluation; collect is the earliest point where the Package node exists and its children will inherit.

Testing

Reproduction after fix:

$ pytest -q /tmp/pytest_pkg_skip_test/skippedpkg -v
skippedpkg/test_skipped.py s [100%]
1 skipped in 0.01s   # was 1 failed

Nested package (outer skip propagates to subpackage): 2 skipped — pass.
xfail in __init__.py: 1 xfailed — pass.
Empty package: 1 passed — pass.
src/nope pollution case: 1 passed — pass (no ImportError regression).
Pre-existing regression test_skip_package scenario (root __init__.py with pytestmark = skip): 2 skipped — pass.

Repository checks:

$ pytest testing/test_mark.py -q
118 passed, 1 xfailed

$ pytest testing/python/collect.py -q
84 passed

$ pytest testing/test_collection.py -q
113 passed, 3 skipped, 1 xfailed  # test_does_not_put_src_on_path now passes

$ pytest testing/test_skipping.py -q
95 passed

Documentation and release impact

  • No documentation impact
  • Changelog entry needed (will add changelog/14737.bugfix.rst if requested)
  • No migration note

Review notes

  • Known limitations: __init__.py is imported to read marks; if it has side effects, it will be executed once during collection for packages with marks (kept), twice (collect + setup) for packages without marks that are deleted/restored. For pure-mark files this is harmless.
  • Follow-up: if maintainers prefer a different layer (e.g. pytest_collection_modifyitems), happy to move the logic there.
  • Security/licensing: MIT, no new dependencies.

Package collectors lost pytestmark propagation when Package stopped
being a Module/File in pytest-dev#6197 (fix for pytest-dev#6194).  The previous fix
removed the Module-style own_markers handling without replacing it,
so a pytestmark = pytest.mark.skip(...) in a package __init__.py
no longer applied to tests in that package.

Restore the behaviour without re-collecting __init__.py as a test
file: in Package.collect, import the package's __init__.py via
importtestmodule, extract marks with get_unpacked_marks, and
extend own_markers / keywords before yielding children.  Children
inherit via Node.iter_markers_with_node.

Do this in collect (not setup) so marks are visible before
pytest_runtest_setup (skipping is tryfirst).  Snapshot and restore
sys.modules / sys.path when no marks are found to avoid polluting
unrelated imports (test_does_not_put_src_on_path).

Fixes pytest-dev#14737
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Package-level pytestmark in __init__.py no longer propagates to tests

2 participants