From dcfae1224147cc1e5234ec92a8c07c0c7675d18b Mon Sep 17 00:00:00 2001 From: Anshul Singh <72524975+ekanshul@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:55:17 +0000 Subject: [PATCH] Warn when both pyproject.toml and .codespellrc set codespell options .codespellrc already overrides pyproject.toml, but the TOML file was omitted from the used-config list so the conflict was silent. Report both files and warn that the INI file wins. Fixes #3999 --- README.rst | 4 +++- codespell_lib/_codespell.py | 19 +++++++++++---- codespell_lib/tests/test_basic.py | 40 +++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index c9edc65d73..184a0e8506 100644 --- a/README.rst +++ b/README.rst @@ -240,7 +240,9 @@ If several config files are present, they are read in the following order: If a codespell configuration is supplied in several of these files, the configuration from the most recently read file overwrites previously -specified configurations. Any options specified in the command line will +specified configurations. A warning is printed when both ``pyproject.toml`` +and ``.codespellrc`` contain codespell settings, since only the latter is +used. Any options specified in the command line will *override* options from the config files. Values in a config file entry cannot start with a ``-`` character, so if diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py index 5d943a8fed..291d281a46 100644 --- a/codespell_lib/_codespell.py +++ b/codespell_lib/_codespell.py @@ -692,6 +692,7 @@ def convert_arg_line_to_args(self, arg_line: str) -> list[str]: # Read toml before other config files. toml_files = [] + used_toml_files = [] tomllib_raise_error = False if os.path.isfile("pyproject.toml"): toml_files.append("pyproject.toml") @@ -721,17 +722,20 @@ def convert_arg_line_to_args(self, arg_line: str) -> list[str]: msg = f"{toml_file}: [tool.codespell] must be a table" raise configparser.Error(msg) config.read_dict({"codespell": _toml_to_parseconfig(data)}) + if toml_file not in used_toml_files: + used_toml_files.append(toml_file) # Collect which config files are going to be used - used_cfg_files = [] + used_ini_files = [] for cfg_file in cfg_files: _cfg = configparser.ConfigParser() _cfg.read(cfg_file) if _cfg.has_section("codespell"): - used_cfg_files.append(cfg_file) + used_ini_files.append(cfg_file) - # Use config files - config.read(used_cfg_files) + # Use INI config files (TOML was already applied above) + config.read(used_ini_files) + used_cfg_files = used_toml_files + used_ini_files if config.has_section("codespell"): # Build a "fake" argv list using option name and value. cfg_args = [] @@ -1411,6 +1415,13 @@ def main(*args: str) -> int: # Report used config files if not options.quiet_level & QuietLevels.CONFIG_FILES: + used_basenames = {os.path.basename(cfg_file) for cfg_file in used_cfg_files} + if "pyproject.toml" in used_basenames and ".codespellrc" in used_basenames: + print( + "WARNING: both pyproject.toml and .codespellrc contain " + "codespell settings; .codespellrc takes precedence", + file=sys.stderr, + ) if len(used_cfg_files) > 0: print("Used config files:") for ifile, cfg_file in enumerate(used_cfg_files, start=1): diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py index 1adb7f7639..796e869c08 100644 --- a/codespell_lib/tests/test_basic.py +++ b/codespell_lib/tests/test_basic.py @@ -1621,6 +1621,46 @@ def test_config_toml_codespell_array( assert "[tool.codespell] must be a table" in stderr +def test_warn_both_pyproject_and_codespellrc( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + if sys.version_info < (3, 11): + pytest.importorskip("tomli") + (tmp_path / "pyproject.toml").write_text("[tool.codespell]\nquiet-level = 2\n") + (tmp_path / ".codespellrc").write_text("[codespell]\nquiet-level = 2\n") + (tmp_path / "ok.txt").write_text("ok\n") + + warning = ( + "WARNING: both pyproject.toml and .codespellrc contain " + "codespell settings; .codespellrc takes precedence" + ) + cwd = Path.cwd() + try: + os.chdir(tmp_path) + result = cs.main(std=True) + finally: + os.chdir(cwd) + assert isinstance(result, tuple) + code, stdout, stderr = result + assert code == 0 + assert warning in stderr + assert "Used config files:" in stdout + assert "pyproject.toml" in stdout + assert ".codespellrc" in stdout + + try: + os.chdir(tmp_path) + result = cs.main("--quiet-level=32", std=True) + finally: + os.chdir(cwd) + assert isinstance(result, tuple) + code, stdout, stderr = result + assert code == 0 + assert warning not in stderr + assert "Used config files:" not in stdout + + @contextlib.contextmanager def FakeStdin(text: str) -> Generator[None, None, None]: oldin = sys.stdin