Repository navigation
conftest.py: support importorskip at import, make every other outcome exception a usage error聽#15142
Description
馃 Written by Claude Opus 5.5 via Claude Code for the pytest maintainers; I prompted it, it did the work, I read it.
Problem
A conftest.py is plugin code, not a collected module. Today an outcome exception raised while importing it does something different depending on how pytest was invoked:
| conftest top level | pytest tests |
pytest tests/sub or a node id |
|---|---|---|
pytest.importorskip("missing") |
directory skipped, exit 0 | raw Skipped traceback, exit 1 (#15141) |
pytest.skip("x") (no allow_module_level) |
whole directory skipped, exit 0 (#12371) | raw Skipped traceback, exit 1 |
pytest.fail("x") |
collection error, exit 2 | raw traceback, exit 1 |
raise unittest.SkipTest("x") |
collection error, exit 2 | collection error |
(checked on main @ e84e063)
Why:
PytestPluginManager._importconftestwrapsexcept ExceptionintoConftestImportFailure.OutcomeExceptionderives fromBaseException, soSkipped/Failed/XFailedslip past, whileunittest.SkipTest(anException) is reported as an error.- During collection the conftest happens to be imported inside the
Dircollector'sCallInfo(pytest_make_collect_reportinrunner.py), so an escapedSkippedbecomes a skip of the directory. - Initial conftests (
_set_initial_conftests, for command-line args) have no collector around them, so the same exception crashes pytest. - The
allow_module_levelcheck exists only inpython.importtestmodule, so a plainpytest.skip("x")in a conftest silently skips a whole package.
Real-world usage
Scan of 1648 conftest.py files in 177 popular repos:
- Import-time outcome exceptions in conftests: 10 files in 6 repos (pandas, fsspec, kombu, ibis, salt, gunicorn). All of them are
pytest.importorskip; there was no top-levelpytest.skip/pytest.fail/raise SkipTestin any conftest. Typical:pa = pytest.importorskip("pyarrow")inpandas/tests/arrays/arrow/conftest.py, gating the directory and binding the module for the fixtures below in one line. - Alternatives in use are all worse for this job: conditional
collect_ignore/collect_ignore_glob(14 repos, silent, a try/except per dependency, does not bind the module),pytest_ignore_collect(7 repos, silent), skip markers added frompytest_collection_modifyitems/pytest_runtest_setup(~12 repos, verbose, does not protect the conftest's own imports).
So importorskip in a conftest is a legitimate, useful idiom; everything else is an accident.
Proposal
- Support
pytest.importorskipat conftest import. It skips the conftest's directory, consistently: also when the conftest is an initial conftest (command-line path or node id), which fixes Skipped raised by a conftest of a command-line path crashes pytest instead of skipping聽#15141. A node id below the skipped directory reports the skip instead ofERROR: found no collectors. - Every other outcome exception at conftest import is a usage error naming the conftest:
pytest.skip()(with or withoutallow_module_level),pytest.fail(),pytest.xfail(). This resolves pytest.skip in conftest/__init__ files doesn't work as expected聽#12371.pytest.skip()currently "works" during collection, so it gets aPytestRemovedIn10Warning(keeping today's directory skip) in 9.x and becomes the usage error in 10.fail/xfailalready error during collection and crash on initial conftests, so they become the usage error right away.
--import-mode=importlib: remove the conftest fromsys.moduleswhen its import raises, so a retry never sees a half-executed module (Skipped raised by a conftest of a command-line path crashes pytest instead of skipping聽#15141, point 3).
Implementation note: importorskip raises a plain Skipped today, so it needs to be told apart, e.g. a private Skipped subclass or flag set only by importorskip (including its minversion skip).
Not affected: pytest.exit() (Exit is not an OutcomeException), outcome exceptions raised from conftest hooks or fixtures at runtime (node-scoped, already well-defined, see test_skip_file_by_conftest), and skips in a package __init__.py (a collected module, governed by allow_module_level).
Docs
Document importorskip in conftest.py as the supported way to gate a directory on an optional dependency, and state that no other skip/fail is allowed there.
Supersedes #12371 and #15141 (both closed in favour of this). Background: #4479.
Activity