Skip to content

Commit cc93509

Browse files
Handle unreadable configuration files (#4035)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent c3927c6 commit cc93509

3 files changed

Lines changed: 44 additions & 3 deletions

File tree

docs/changelog/4031.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Report a handled configuration error instead of leaking a traceback when the selected configuration file exists but
2+
cannot be read - by :user:`SirHegel`.

src/tox/config/source/discover.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ def discover_source(config_file: Path | None, root_dir: Path | None) -> Source:
5050
break
5151
except MissingRequiredConfigKeyError:
5252
continue
53-
except ValueError as exc:
53+
except (ValueError, OSError) as exc:
5454
msg = f"{src_type.__name__} failed loading {candidate.resolve()} due to {exc}"
5555
raise HandledError(msg) from exc
5656
if src is None:
@@ -72,7 +72,7 @@ def _locate_source() -> Source | None:
7272
except MissingRequiredConfigKeyError as exc:
7373
msg = f"{src_type.__name__} skipped loading {candidate.resolve()} due to {exc}"
7474
logging.info(msg)
75-
except ValueError as exc:
75+
except (ValueError, OSError) as exc:
7676
msg = f"{src_type.__name__} failed loading {candidate.resolve()} due to {exc}"
7777
raise HandledError(msg) from exc
7878
return None
@@ -89,7 +89,7 @@ def _load_exact_source(config_file: Path) -> Source:
8989
return src_type(config_file)
9090
except MissingRequiredConfigKeyError: # ruff:ignore[try-except-in-loop]
9191
pass
92-
except ValueError as exc:
92+
except (ValueError, OSError) as exc:
9393
msg = f"{src_type.__name__} failed loading {config_file.resolve()} due to {exc}"
9494
raise HandledError(msg) from exc
9595
msg = f"could not recognize config file {config_file}"

tests/config/source/test_discover.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
from __future__ import annotations
22

3+
import os
4+
import sys
35
from typing import TYPE_CHECKING
46

7+
import pytest
8+
9+
from tox.config.source.discover import discover_source
10+
from tox.report import HandledError
11+
512
if TYPE_CHECKING:
613
from pathlib import Path
714

815
from tox.pytest import ToxProjectCreator
916

1017

18+
# Root ignores the permission bits, and Windows does not model them this way,
19+
# so the file stays readable and there is nothing to assert.
20+
unreadable_files_possible = pytest.mark.skipif(
21+
sys.platform == "win32" or os.getuid() == 0,
22+
reason="cannot make a file unreadable as root or on Windows",
23+
)
24+
25+
1126
def out_no_src(path: Path) -> str:
1227
return (
1328
f"ROOT: No loadable tox.ini or setup.cfg or pyproject.toml or tox.toml found, assuming empty tox.ini at {path}"
@@ -50,6 +65,30 @@ def test_bad_src_content(tox_project: ToxProjectCreator, tmp_path: Path) -> None
5065
assert outcome.out == f"ROOT: HandledError| config file {tmp_path / 'setup.cfg'} does not exist\n"
5166

5267

68+
@unreadable_files_possible
69+
@pytest.mark.parametrize("named", [True, False], ids=["explicit-path", "discovery"])
70+
def test_unreadable_config_raises_handled_error(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, named: bool) -> None:
71+
"""Reading the file raises an OSError subclass, not ValueError.
72+
73+
Without handling it, discovery lets PermissionError escape and the CLI prints a traceback instead of the one-line
74+
error used for malformed files.
75+
76+
This exercises discover_source directly: the tox_project fixture converts exceptions on its own, so it cannot tell
77+
the two cases apart.
78+
79+
"""
80+
config = tmp_path / "tox.ini"
81+
config.write_text("[tox]\nenv_list = py\n")
82+
config.chmod(0o000)
83+
monkeypatch.chdir(tmp_path)
84+
85+
try:
86+
with pytest.raises(HandledError, match="failed loading"):
87+
discover_source(config if named else None, None)
88+
finally:
89+
config.chmod(0o644)
90+
91+
5392
def test_malformed_config_does_not_prevent_help(tox_project: ToxProjectCreator) -> None:
5493
project = tox_project({"tox.toml": "deps =\n mypy\n"})
5594
outcome = project.run("--help")

0 commit comments

Comments
 (0)