Skip to content

fix: provide clear messaging about config file loading #3578

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/3578.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Makes the error message more clear when pyproject.toml file cannot be loaded
or is missing expected keys.
24 changes: 18 additions & 6 deletions src/tox/config/source/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path
from typing import TYPE_CHECKING

from tox.config.types import MissingRequiredConfigKeyError
from tox.report import HandledError

from .legacy_toml import LegacyToml
Expand Down Expand Up @@ -59,21 +60,32 @@ def _locate_source() -> Source | None:
for base in chain([folder], folder.parents):
for src_type in SOURCE_TYPES:
candidate: Path = base / src_type.FILENAME
try:
return src_type(candidate)
except ValueError:
pass
if candidate.exists():
try:
return src_type(candidate)
except MissingRequiredConfigKeyError as exc:
msg = f"{src_type.__name__} skipped loading {candidate.resolve()} due to {exc}"
logging.info(msg)
except ValueError as exc:
msg = f"{src_type.__name__} failed loading {candidate.resolve()} due to {exc}"
raise HandledError(msg) from exc
return None


def _load_exact_source(config_file: Path) -> Source:
# if the filename matches to the letter some config file name do not fallback to other source types
if not config_file.exists():
msg = f"config file {config_file} does not exist"
raise HandledError(msg)
exact_match = [s for s in SOURCE_TYPES if config_file.name == s.FILENAME] # pragma: no cover
for src_type in exact_match or SOURCE_TYPES: # pragma: no branch
try:
return src_type(config_file)
except ValueError: # noqa: PERF203
except MissingRequiredConfigKeyError: # noqa: PERF203
pass
except ValueError as exc:
msg = f"{src_type.__name__} failed loading {config_file.resolve()} due to {exc}"
raise HandledError(msg) from exc
msg = f"could not recognize config file {config_file}"
raise HandledError(msg)

Expand All @@ -88,7 +100,7 @@ def _create_default_source(root_dir: Path | None) -> Source:
else: # if not set use where we find pyproject.toml in the tree or cwd
empty = root_dir
names = " or ".join({i.FILENAME: None for i in SOURCE_TYPES})
logging.warning("No %s found, assuming empty tox.ini at %s", names, empty)
logging.warning("No loadable %s found, assuming empty tox.ini at %s", names, empty)
return ToxIni(empty / "tox.ini", content="")


Expand Down
5 changes: 4 additions & 1 deletion src/tox/config/source/legacy_toml.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import sys

from tox.config.types import MissingRequiredConfigKeyError

if sys.version_info >= (3, 11): # pragma: no cover (py311+)
import tomllib
else: # pragma: no cover (py311+)
Expand All @@ -27,7 +29,8 @@ def __init__(self, path: Path) -> None:
try:
content = toml_content["tool"]["tox"]["legacy_tox_ini"]
except KeyError as exc:
raise ValueError(path) from exc
msg = f"`tool.tox.legacy_tox_ini` missing from {path}"
raise MissingRequiredConfigKeyError(msg) from exc
super().__init__(path, content=content)


Expand Down
3 changes: 2 additions & 1 deletion src/tox/config/source/setup_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class SetupCfg(IniSource):
def __init__(self, path: Path) -> None:
super().__init__(path)
if not self._parser.has_section(self.CORE_SECTION.key):
raise ValueError
msg = f"section {self.CORE_SECTION.key} not found"
raise ValueError(msg)


__all__ = ("SetupCfg",)
3 changes: 2 additions & 1 deletion src/tox/config/source/toml_pyproject.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from tox.config.loader.section import Section
from tox.config.loader.toml import TomlLoader
from tox.config.types import MissingRequiredConfigKeyError
from tox.report import HandledError

from .api import Source
Expand Down Expand Up @@ -81,7 +82,7 @@ def __init__(self, path: Path) -> None:
our_content = our_content[key]
self._our_content = our_content
except KeyError as exc:
raise ValueError(path) from exc
raise MissingRequiredConfigKeyError(path) from exc
super().__init__(path)

def get_core_section(self) -> Section:
Expand Down
7 changes: 7 additions & 0 deletions src/tox/config/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ class CircularChainError(ValueError):
"""circular chain in config"""


class MissingRequiredConfigKeyError(ValueError):
"""missing required config key

Used by the two toml loaders in order to identify if config keys are present.
"""


class Command: # noqa: PLW1641
"""A command to execute."""

Expand Down
6 changes: 3 additions & 3 deletions tests/config/source/test_discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@

def out_no_src(path: Path) -> str:
return (
f"ROOT: No tox.ini or setup.cfg or pyproject.toml or tox.toml found, assuming empty tox.ini at {path}\n"
f"default environments:\npy -> [no description]\n"
f"ROOT: No loadable tox.ini or setup.cfg or pyproject.toml or tox.toml found, assuming empty tox.ini at {path}"
f"\ndefault environments:\npy -> [no description]\n"
)


Expand Down Expand Up @@ -47,4 +47,4 @@ def test_bad_src_content(tox_project: ToxProjectCreator, tmp_path: Path) -> None

outcome = project.run("l", "-c", str(tmp_path / "setup.cfg"))
outcome.assert_failed()
assert outcome.out == f"ROOT: HandledError| could not recognize config file {tmp_path / 'setup.cfg'}\n"
assert outcome.out == f"ROOT: HandledError| config file {tmp_path / 'setup.cfg'} does not exist\n"
2 changes: 1 addition & 1 deletion tests/config/source/test_setup_cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ def test_bad_conf_setup_cfg(tox_project: ToxProjectCreator) -> None:
filename = str(project.path / "setup.cfg")
outcome = project.run("l", "-c", filename)
outcome.assert_failed()
assert outcome.out == f"ROOT: HandledError| could not recognize config file {filename}\n"
assert outcome.out == f"ROOT: HandledError| SetupCfg failed loading {filename} due to section tox:tox not found\n"
3 changes: 2 additions & 1 deletion tests/session/cmd/test_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ def test_legacy_list_env_with_no_tox_file(tox_project: ToxProjectCreator) -> Non
outcome = project.run("le", "-l")
outcome.assert_success()
out = (
f"ROOT: No tox.ini or setup.cfg or pyproject.toml or tox.toml found, assuming empty tox.ini at {project.path}\n"
"ROOT: No loadable tox.ini or setup.cfg or pyproject.toml or tox.toml found, assuming empty tox.ini at "
f"{project.path}\n"
)
assert not outcome.err
assert outcome.out == out
Expand Down