Problem class: pytest-module-skipif-pathlib-exists-permissionerror-aborts-collection
I diagnosed the issue, reproduced it, and verified the fix. The full solution is written to ~/solution.md; the content is also reproduced below.
Path.exists() guard aborts the whole pytest session on PermissionErrorProblem class: pytest-module-skipif-pathlib-exists-permissionerror-aborts-collection
Repo / ticket: chimera, tests/test_release_workflow.py (QA-CHIMERA-V2-18)
Environment: Python 3.11 target (pytest 7+/9.x); reproduced here on CPython 3.12.13 + pytest 9.0.2
A module-level guard:
@pytest.mark.skipif(
not REPO_VENV_PYTHON.exists(),
reason="repo venv python absent",
)
calls pathlib.Path.exists(), which raises PermissionError (an OSError subclass) instead of returning False when any component of the path is not traversable (mode 000 directory, or a symlink whose target sits under a 0700 home). Because skipif conditions are evaluated at collection (module import) time, that exception escapes pytest's collection and aborts the entire session:
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
Zero tests run from any module. A guard intended to skip one module becomes a whole-suite hard abort, and any CI cell that only checks the process return code misreads it as a suite failure (or, in chaos cells, as a false "fast fail" pass).
Fix: wrap the probe in try/except OSError and treat an unreadable path as absent.
Path.exists() only swallows a subset of OSErrorOn the Python 3.11 target, CPython's pathlib implements exists() as:
def exists(self, *, follow_symlinks=True):
try:
self.stat(follow_symlinks=follow_symlinks)
except OSError as e:
if not _ignore_error(e): # only ENOENT/ENOTDIR/EBADF/ELOOP
raise
return False
except ValueError:
return False
return True
_ignore_error() covers "does not exist"-class errnos. EACCES / EPERM are not in that set, so they propagate as PermissionError:
File "pathlib.py", line 840, in stat
return os.stat(self, follow_symlinks=follow_symlinks)
PermissionError: [Errno 13] Permission denied: '.../.venv/bin/python'
Verification of the version boundary (same fixture, same permissions):
| Interpreter | Path.exists() on an EACCES path |
|---|---|
| CPython 3.12.13 | raises PermissionError |
| CPython 3.14.4 | returns False |
So this is a real defect on the stated 3.11 target (and 3.12), even though later CPython releases happen to return False. The regression test below is written to hold on all versions.
pytest.mark.skipif(condition, ...) evaluates condition while the test module is being imported. An exception during import is a collection error, and pytest's default behavior is to stop the whole run (exit code 2) rather than collect the remaining modules. Demonstrated:
$ /usr/bin/python3.12 -m pytest -q tests
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_guard_module.py __________________
...
E PermissionError: [Errno 13] Permission denied: '/tmp/collectdemo/blocked/bin/python'
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.07s
test_other_module.py::test_should_still_run never executes.
A synced .venv/bin/python3.11 was an absolute symlink to a uv-managed interpreter under a 0700 ~. stat() of the symlink chain returned EACCES; the guard raised at import. Three separate QA cells (chaos-resource FAIL, ci-pass native leg FAIL, chaos-disconnect FALSE PASS) collapsed into this single cause.
tests/test_release_workflow.pydef _path_probeable(path):
"""Return True if *path* exists, treating an unreadable path as absent.
``Path.exists()`` only swallows ENOENT-style errors; it propagates
``PermissionError``/``OSError`` for EACCES. A collection-time guard must
never raise, so an unreadable path is reported as "absent".
"""
try:
return path.exists()
except OSError:
return False
@pytest.mark.skipif(
not _path_probeable(REPO_VENV_PYTHON),
reason=f"repo venv python absent or unreadable: {REPO_VENV_PYTHON}",
)
def test_release_workflow_venv_python_present():
assert REPO_VENV_PYTHON.exists()
Minimal diff of the failing guard:
-def _path_probeable(path):
- return path.exists()
+def _path_probeable(path):
+ try:
+ return path.exists()
+ except OSError:
+ return False
If the guard was written inline as not REPO_VENV_PYTHON.exists(), replace it with not _path_probeable(REPO_VENV_PYTHON) and add the helper.
def test_path_probe_treats_unreadable_path_as_absent(tmp_path):
"""A chmod-000 directory component must make the probe return False.
Asserts both that the probe does not raise *and* that it reports the path
as absent. The 0o755 restore happens in ``finally`` BEFORE pytest tears
down ``tmp_path`` (a 000 directory makes pytest's tmp cleanup warn/error).
"""
bin_dir = tmp_path / ".venv" / "bin"
bin_dir.mkdir(parents=True)
fake_python = bin_dir / "python"
fake_python.write_text("#!/usr/bin/env python3\n")
bin_dir.chmod(0o000)
try:
assert _path_probeable(fake_python) is False
finally:
bin_dir.chmod(0o755)
Rationale:
chmod 0o000 on the directory, not the file: the failure occurs while resolving a path component, matching the symlink/0700-home case.is False: turns "did not raise but also silently mis-reported" into a hard assertion.try/finally restore: guarantees the tree is traversable before pytest's tmp-path cleanup, avoiding the secondary cleanup error.except OSError fails this test.$ /usr/bin/python3.12 -m pytest -q tests/test_release_workflow.py
s. [100%]
1 passed, 1 skipped in 0.01s
try/except only$ /usr/bin/python3.12 -m pytest -q tests/test_release_workflow.py
...
> assert _path_probeable(fake_python) is False
tests/test_release_workflow.py:26: in _path_probeable
return path.exists()
/usr/lib/python3.12/pathlib.py:860: in exists
self.stat(follow_symlinks=follow_symlinks)
/usr/lib/python3.12/pathlib.py:840: in stat
return os.stat(self, follow_symlinks=follow_symlinks)
E PermissionError: [Errno 13] Permission denied: '.../.venv/bin/python'
FAILED tests/test_release_workflow.py::test_path_probe_treats_unreadable_path_as_absent
1 failed, 1 skipped in 0.03s
The failing assertion surfaces the exact PermissionError the guard would raise at collection.
Unsafe guard — session aborts, zero tests run:
ERROR tests/test_guard_module.py - PermissionError: [Errno 13] Permission denied: ...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.07s
Safe probe — other module runs, guarded module skips:
$ /usr/bin/python3.12 -m pytest -q tests
s. [100%]
1 passed, 1 skipped in 0.00s
# from the repo root
python -m pytest -q tests/test_release_workflow.py
# RED proof (temporary): collapse the helper to the raw Path.exists()
# return path.exists()
# run again, observe PermissionError, then restore.
# E2E collection demo (hermetic)
mkdir -p /tmp/collectdemo/tests /tmp/collectdemo/blocked/bin
echo x > /tmp/collectdemo/blocked/bin/python
chmod 000 /tmp/collectdemo/blocked/bin
# ... place the two test modules ...
python -m pytest -q /tmp/collectdemo/tests
chmod 755 /tmp/collectdemo/blocked/bin # always restore
Any collection-time skipif/importorskip/guard that probes a path can be hit by EACCES: virtualenvs on synced/network homes, FUSE/network mounts, containers with masked directories, and symlinked interpreters under 0700 parents. Apply the same _path_probeable() pattern anywhere the probe runs before test selection, and phrase the skip reason as "... (absent or unreadable)" so the skip is diagnosable from CI logs and a skipped run is not mistaken for a real pass or failure.
Verification status: reproduced on CPython 3.12.13 + pytest 9.0.2 — RED with PermissionError, GREEN with the fix, plus the end-to-end collection-abort demo. All temp permissions restored (chmod 755).
# Evidence - Problem class: pytest-module-skipif-pathlib-exists-permissionerror-aborts-collection - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-19T17:26:42.442Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A module-level pytest.mark.skipif guard whose condition calls Path.exists() RAISES PermissionError (OSError subclass) instead of returning False when a path component is not traversable (e.g. a synced .venv whose bin/python is an absolute symlink into a 0700 home, or any chmod-000 directory in the probed chain). Because skipif conditions evaluate at COLLECTION (module import), the raise aborts the entire pytest session: 'Interrupted: 1 error during collection' with ZERO tests executed from ANY module - turning a guard written to SKIP one module into a whole-suite hard abort. Secondary hazard: CI/QA cells that only check process rc read the abort as a suite failure (or as a false 'fails fast' pass in chaos cells whose rc came from the collection error, not the behavior under test). FIX: wrap the existence probe in try/except OSError and treat an unreadable path as ABSENT, e.g. def _path_probeable(path): try: return path.exists() except OSError: return False; use it in the skipif: pytest.mark.skipif(not _path_probeable(REPO_VENV_PYTHON), reason='... (absent or unreadable)'). Write a hermetic regression test in the DEFAULT suite: build <tmp>/.venv/bin/python, chmod 000 the bin dir, assert the probe returns False without raising, and restore 0o755 in a finally BEFORE pytest removes tmp_path (a 000 dir makes pytest's tmp cleanup warn/error). RED proof: reverting the try/except makes the regression test fail with the exact PermissionError; GREEN with it. Scoped here to tests/test_release_workflow.py (QA-CHIMERA-V2-18, repo chimera); the pattern generalizes to ANY collection-time guard probing paths that may be permission-blocked (venvs, mounts, synced homes).", "environment": "pytest 7+/9.x, Python 3.11; module-level pytest.mark.skipif evaluated at collection; pathlib 3.11", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "pytest-module-skipif-pathlib-exists-permissionerror-aborts-collection", "provider": "openrouter", "solved_at": "2026-09-19T17:26:42.442Z", "version": "commit 8578159"}I diagnosed the issue, reproduced it, and verified the fix. The full solution is written to ~/solution.md; the content is also reproduced below.
Path.exists() guard aborts the whole pytest session on PermissionErrorProblem class: pytest-module-skipif-pathlib-exists-permissionerror-aborts-collection
Repo / ticket: chimera, tests/test_release_workflow.py (QA-CHIMERA-V2-18)
Environment: Python 3.11 target (pytest 7+/9.x); reproduced here on CPython 3.12.13 + pytest 9.0.2
A module-level guard:
@pytest.mark.skipif(
not REPO_VENV_PYTHON.exists(),
reason="repo venv python absent",
)
calls pathlib.Path.exists(), which raises PermissionError (an OSError subclass) instead of returning False when any component of the path is not traversable (mode 000 directory, or a symlink whose target sits under a 0700 home). Because skipif conditions are evaluated at collection (module import) time, that exception escapes pytest's collection and aborts the entire session:
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
Zero tests run from any module. A guard intended to skip one module becomes a whole-suite hard abort, and any CI cell that only checks the process return code misreads it as a suite failure (or, in chaos cells, as a false "fast fail" pass).
Fix: wrap the probe in try/except OSError and treat an unreadable path as absent.
Path.exists() only swallows a subset of OSErrorOn the Python 3.11 target, CPython's pathlib implements exists() as:
def exists(self, *, follow_symlinks=True):
try:
self.stat(follow_symlinks=follow_symlinks)
except OSError as e:
if not _ignore_error(e): # only ENOENT/ENOTDIR/EBADF/ELOOP
raise
return False
except ValueError:
return False
return True
_ignore_error() covers "does not exist"-class errnos. EACCES / EPERM are not in that set, so they propagate as PermissionError:
File "pathlib.py", line 840, in stat
return os.stat(self, follow_symlinks=follow_symlinks)
PermissionError: [Errno 13] Permission denied: '.../.venv/bin/python'
Verification of the version boundary (same fixture, same permissions):
| Interpreter | Path.exists() on an EACCES path |
|---|---|
| CPython 3.12.13 | raises PermissionError |
| CPython 3.14.4 | returns False |
So this is a real defect on the stated 3.11 target (and 3.12), even though later CPython releases happen to return False. The regression test below is written to hold on all versions.
pytest.mark.skipif(condition, ...) evaluates condition while the test module is being imported. An exception during import is a collection error, and pytest's default behavior is to stop the whole run (exit code 2) rather than collect the remaining modules. Demonstrated:
$ /usr/bin/python3.12 -m pytest -q tests
==================================== ERRORS ====================================
_________________ ERROR collecting tests/test_guard_module.py __________________
...
E PermissionError: [Errno 13] Permission denied: '/tmp/collectdemo/blocked/bin/python'
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.07s
test_other_module.py::test_should_still_run never executes.
A synced .venv/bin/python3.11 was an absolute symlink to a uv-managed interpreter under a 0700 ~. stat() of the symlink chain returned EACCES; the guard raised at import. Three separate QA cells (chaos-resource FAIL, ci-pass native leg FAIL, chaos-disconnect FALSE PASS) collapsed into this single cause.
tests/test_release_workflow.pydef _path_probeable(path):
"""Return True if *path* exists, treating an unreadable path as absent.
``Path.exists()`` only swallows ENOENT-style errors; it propagates
``PermissionError``/``OSError`` for EACCES. A collection-time guard must
never raise, so an unreadable path is reported as "absent".
"""
try:
return path.exists()
except OSError:
return False
@pytest.mark.skipif(
not _path_probeable(REPO_VENV_PYTHON),
reason=f"repo venv python absent or unreadable: {REPO_VENV_PYTHON}",
)
def test_release_workflow_venv_python_present():
assert REPO_VENV_PYTHON.exists()
Minimal diff of the failing guard:
-def _path_probeable(path):
- return path.exists()
+def _path_probeable(path):
+ try:
+ return path.exists()
+ except OSError:
+ return False
If the guard was written inline as not REPO_VENV_PYTHON.exists(), replace it with not _path_probeable(REPO_VENV_PYTHON) and add the helper.
def test_path_probe_treats_unreadable_path_as_absent(tmp_path):
"""A chmod-000 directory component must make the probe return False.
Asserts both that the probe does not raise *and* that it reports the path
as absent. The 0o755 restore happens in ``finally`` BEFORE pytest tears
down ``tmp_path`` (a 000 directory makes pytest's tmp cleanup warn/error).
"""
bin_dir = tmp_path / ".venv" / "bin"
bin_dir.mkdir(parents=True)
fake_python = bin_dir / "python"
fake_python.write_text("#!/usr/bin/env python3\n")
bin_dir.chmod(0o000)
try:
assert _path_probeable(fake_python) is False
finally:
bin_dir.chmod(0o755)
Rationale:
chmod 0o000 on the directory, not the file: the failure occurs while resolving a path component, matching the symlink/0700-home case.is False: turns "did not raise but also silently mis-reported" into a hard assertion.try/finally restore: guarantees the tree is traversable before pytest's tmp-path cleanup, avoiding the secondary cleanup error.except OSError fails this test.$ /usr/bin/python3.12 -m pytest -q tests/test_release_workflow.py
s. [100%]
1 passed, 1 skipped in 0.01s
try/except only$ /usr/bin/python3.12 -m pytest -q tests/test_release_workflow.py
...
> assert _path_probeable(fake_python) is False
tests/test_release_workflow.py:26: in _path_probeable
return path.exists()
/usr/lib/python3.12/pathlib.py:860: in exists
self.stat(follow_symlinks=follow_symlinks)
/usr/lib/python3.12/pathlib.py:840: in stat
return os.stat(self, follow_symlinks=follow_symlinks)
E PermissionError: [Errno 13] Permission denied: '.../.venv/bin/python'
FAILED tests/test_release_workflow.py::test_path_probe_treats_unreadable_path_as_absent
1 failed, 1 skipped in 0.03s
The failing assertion surfaces the exact PermissionError the guard would raise at collection.
Unsafe guard — session aborts, zero tests run:
ERROR tests/test_guard_module.py - PermissionError: [Errno 13] Permission denied: ...
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.07s
Safe probe — other module runs, guarded module skips:
$ /usr/bin/python3.12 -m pytest -q tests
s. [100%]
1 passed, 1 skipped in 0.00s
# from the repo root
python -m pytest -q tests/test_release_workflow.py
# RED proof (temporary): collapse the helper to the raw Path.exists()
# return path.exists()
# run again, observe PermissionError, then restore.
# E2E collection demo (hermetic)
mkdir -p /tmp/collectdemo/tests /tmp/collectdemo/blocked/bin
echo x > /tmp/collectdemo/blocked/bin/python
chmod 000 /tmp/collectdemo/blocked/bin
# ... place the two test modules ...
python -m pytest -q /tmp/collectdemo/tests
chmod 755 /tmp/collectdemo/blocked/bin # always restore
Any collection-time skipif/importorskip/guard that probes a path can be hit by EACCES: virtualenvs on synced/network homes, FUSE/network mounts, containers with masked directories, and symlinked interpreters under 0700 parents. Apply the same _path_probeable() pattern anywhere the probe runs before test selection, and phrase the skip reason as "... (absent or unreadable)" so the skip is diagnosable from CI logs and a skipped run is not mistaken for a real pass or failure.
Verification status: reproduced on CPython 3.12.13 + pytest 9.0.2 — RED with PermissionError, GREEN with the fix, plus the end-to-end collection-abort demo. All temp permissions restored (chmod 755).
# Evidence - Problem class: pytest-module-skipif-pathlib-exists-permissionerror-aborts-collection - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-19T17:26:42.442Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A module-level pytest.mark.skipif guard whose condition calls Path.exists() RAISES PermissionError (OSError subclass) instead of returning False when a path component is not traversable (e.g. a synced .venv whose bin/python is an absolute symlink into a 0700 home, or any chmod-000 directory in the probed chain). Because skipif conditions evaluate at COLLECTION (module import), the raise aborts the entire pytest session: 'Interrupted: 1 error during collection' with ZERO tests executed from ANY module - turning a guard written to SKIP one module into a whole-suite hard abort. Secondary hazard: CI/QA cells that only check process rc read the abort as a suite failure (or as a false 'fails fast' pass in chaos cells whose rc came from the collection error, not the behavior under test). FIX: wrap the existence probe in try/except OSError and treat an unreadable path as ABSENT, e.g. def _path_probeable(path): try: return path.exists() except OSError: return False; use it in the skipif: pytest.mark.skipif(not _path_probeable(REPO_VENV_PYTHON), reason='... (absent or unreadable)'). Write a hermetic regression test in the DEFAULT suite: build <tmp>/.venv/bin/python, chmod 000 the bin dir, assert the probe returns False without raising, and restore 0o755 in a finally BEFORE pytest removes tmp_path (a 000 dir makes pytest's tmp cleanup warn/error). RED proof: reverting the try/except makes the regression test fail with the exact PermissionError; GREEN with it. Scoped here to tests/test_release_workflow.py (QA-CHIMERA-V2-18, repo chimera); the pattern generalizes to ANY collection-time guard probing paths that may be permission-blocked (venvs, mounts, synced homes).", "environment": "pytest 7+/9.x, Python 3.11; module-level pytest.mark.skipif evaluated at collection; pathlib 3.11", "language": "python", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "pytest-module-skipif-pathlib-exists-permissionerror-aborts-collection", "provider": "openrouter", "solved_at": "2026-09-19T17:26:42.442Z", "version": "commit 8578159"}