From 9bf8db876217797aabd670a1abb2d296f155a890 Mon Sep 17 00:00:00 2001 From: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:37:28 -0700 Subject: [PATCH] Read only the codespell TOML table Passing every child of [tool] to ConfigParser made unrelated arrays of tables crash option parsing. Extract only [tool.codespell], reject a non-table codespell section through the existing config error path, and cover both cases with regression tests. Fixes #3969 --- codespell_lib/_codespell.py | 10 +++++---- codespell_lib/tests/test_basic.py | 35 ++++++++++++++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py index 8ebb19de43..994c5c4a66 100644 --- a/codespell_lib/_codespell.py +++ b/codespell_lib/_codespell.py @@ -712,10 +712,12 @@ def convert_arg_line_to_args(self, arg_line: str) -> list[str]: if tomllib is not None: for toml_file in toml_files: with open(toml_file, "rb") as f: - data = tomllib.load(f).get("tool", {}) - if "codespell" in data: - data["codespell"] = _toml_to_parseconfig(data["codespell"]) - config.read_dict(data) + data = tomllib.load(f).get("tool", {}).get("codespell") + if data is not None: + if not isinstance(data, dict): + msg = f"{toml_file}: [tool.codespell] must be a table" + raise configparser.Error(msg) + config.read_dict({"codespell": _toml_to_parseconfig(data)}) # Collect which config files are going to be used used_cfg_files = [] diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py index d1c66a18a5..930f09f14a 100644 --- a/codespell_lib/tests/test_basic.py +++ b/codespell_lib/tests/test_basic.py @@ -1428,7 +1428,10 @@ def test_ill_formed_ini_config_file( assert "ill-formed config file" in stderr -@pytest.mark.parametrize("kind", ["cfg", "cfg_multiline", "toml", "toml_list"]) +@pytest.mark.parametrize( + "kind", + ["cfg", "cfg_multiline", "toml", "toml_list", "toml_sibling_array"], +) def test_config_toml( tmp_path: Path, capsys: pytest.CaptureFixture[str], @@ -1482,13 +1485,23 @@ def test_config_toml( check-filenames = false count = true """ - else: - assert kind == "toml_list" + elif kind == "toml_list": text = """\ [tool.codespell] skip = ['bad.txt', 'whatever.txt'] check-filenames = false count = true +""" + else: + assert kind == "toml_sibling_array" + text = """\ +[tool.codespell] +skip = 'bad.txt,whatever.txt' +check-filenames = false +count = true + +[[tool.dynamic-metadata]] +provider = 'scikit_build_core.metadata.version' """ tomlfile.write_text(text) @@ -1514,6 +1527,22 @@ def test_config_toml( assert "abandonned.txt" not in stdout +def test_config_toml_codespell_array( + tmp_path: Path, + capsys: pytest.CaptureFixture[str], +) -> None: + if sys.version_info < (3, 11): + pytest.importorskip("tomli") + tomlfile = tmp_path / "pyproject.toml" + tomlfile.write_text("[[tool.codespell]]\nskip = 'bad.txt'\n") + + result = cs.main("--toml", tomlfile, std=True) + assert isinstance(result, tuple) + code, _, stderr = result + assert code == EX_CONFIG + assert "[tool.codespell] must be a table" in stderr + + @contextlib.contextmanager def FakeStdin(text: str) -> Generator[None, None, None]: oldin = sys.stdin