diff --git a/docs/changelog/3578.bugfix.rst b/docs/changelog/3578.bugfix.rst new file mode 100644 index 0000000000..236ab29210 --- /dev/null +++ b/docs/changelog/3578.bugfix.rst @@ -0,0 +1,2 @@ +Makes the error message more clear when pyproject.toml file cannot be loaded +or is missing expected keys. diff --git a/src/tox/config/source/discover.py b/src/tox/config/source/discover.py index f04b9a7a0c..988c4976da 100644 --- a/src/tox/config/source/discover.py +++ b/src/tox/config/source/discover.py @@ -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 @@ -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) @@ -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="") diff --git a/src/tox/config/source/legacy_toml.py b/src/tox/config/source/legacy_toml.py index db6227cddd..6de39f11fe 100644 --- a/src/tox/config/source/legacy_toml.py +++ b/src/tox/config/source/legacy_toml.py @@ -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+) @@ -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) diff --git a/src/tox/config/source/setup_cfg.py b/src/tox/config/source/setup_cfg.py index d1d316eac9..ffb994afdd 100644 --- a/src/tox/config/source/setup_cfg.py +++ b/src/tox/config/source/setup_cfg.py @@ -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",) diff --git a/src/tox/config/source/toml_pyproject.py b/src/tox/config/source/toml_pyproject.py index 4c03e41ac6..b70b1404b3 100644 --- a/src/tox/config/source/toml_pyproject.py +++ b/src/tox/config/source/toml_pyproject.py @@ -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 @@ -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: diff --git a/src/tox/config/types.py b/src/tox/config/types.py index 42c2c48149..0e9070ad40 100644 --- a/src/tox/config/types.py +++ b/src/tox/config/types.py @@ -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.""" diff --git a/tests/config/source/test_discover.py b/tests/config/source/test_discover.py index 86370e5cec..d86fb750bc 100644 --- a/tests/config/source/test_discover.py +++ b/tests/config/source/test_discover.py @@ -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" ) @@ -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" diff --git a/tests/config/source/test_setup_cfg.py b/tests/config/source/test_setup_cfg.py index 3de4747353..e5a84ec9d4 100644 --- a/tests/config/source/test_setup_cfg.py +++ b/tests/config/source/test_setup_cfg.py @@ -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" diff --git a/tests/session/cmd/test_legacy.py b/tests/session/cmd/test_legacy.py index 0d04dc6a12..08d65fe2d5 100644 --- a/tests/session/cmd/test_legacy.py +++ b/tests/session/cmd/test_legacy.py @@ -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