From 9c293870d22d5ea3e0e2869486fa4ee8d5a7fba8 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:52:00 +0200 Subject: [PATCH] Handle multiline options Clean up options expecting lists before using them, as they may contain newlines. Examples: * Enclosing command-line arguments in quotes may introduce newlines in option values: $ codespell -S "A, B, > C, D, E" * INI files may contain multiline values: [codespell] skip = A, B, C, D, E, In all the above cases, the option parsing mechanism keeps the newlines (and spaces). We need to clean up, by splitting on commas and stripping each resulting item from newlines (and spaces). --- codespell_lib/_codespell.py | 36 ++++++++++++++++++---------- codespell_lib/tests/test_basic.py | 40 ++++++++++++++++--------------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py index 2db9692242e..b2b2c3e4912 100644 --- a/codespell_lib/_codespell.py +++ b/codespell_lib/_codespell.py @@ -160,19 +160,15 @@ class QuietLevels: class GlobMatch: - def __init__(self, pattern: Optional[str]) -> None: - self.pattern_list: Optional[List[str]] - if pattern: - # Pattern might be a list of comma-delimited strings - self.pattern_list = ",".join(pattern).split(",") - else: - self.pattern_list = None + def __init__(self, pattern: Optional[List[str]]) -> None: + self.pattern_list: Optional[List[str]] = pattern def match(self, filename: str) -> bool: - if self.pattern_list is None: - return False - - return any(fnmatch.fnmatch(filename, p) for p in self.pattern_list) + return ( + any(fnmatch.fnmatch(filename, p) for p in self.pattern_list) + if self.pattern_list + else False + ) class Misspelling: @@ -1109,6 +1105,22 @@ def parse_file( return bad_count +def flatten_clean_comma_separated_arguments( + arguments: Optional[List[str]], +) -> Optional[List[str]]: + """ + >>> flatten_clean_comma_separated_arguments(["a, b ,\n c, d,", "e"]) + ['a', 'b', 'c', 'd', 'e'] + >>> flatten_clean_comma_separated_arguments([]) + >>> flatten_clean_comma_separated_arguments(None) + """ + return ( + [item.strip() for argument in arguments for item in argument.split(",") if item] + if arguments + else None + ) + + def _script_main() -> int: """Wrap to main() for setuptools.""" return main(*sys.argv[1:]) @@ -1256,7 +1268,7 @@ def main(*args: str) -> int: file_opener = FileOpener(options.hard_encoding_detection, options.quiet_level) - glob_match = GlobMatch(options.skip) + glob_match = GlobMatch(flatten_clean_comma_separated_arguments(options.skip)) try: glob_match.match("/random/path") # does not need a real path except re.error: diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py index 645bb49583b..9a3466ea1fc 100644 --- a/codespell_lib/tests/test_basic.py +++ b/codespell_lib/tests/test_basic.py @@ -573,6 +573,8 @@ def test_ignore( (subdir / "bad.txt").write_text("abandonned") assert cs.main(tmp_path) == 2 assert cs.main("--skip=bad*", tmp_path) == 0 + assert cs.main("--skip=whatever.txt,bad*,whatelse.txt", tmp_path) == 0 + assert cs.main("--skip=whatever.txt,\n bad* ,", tmp_path) == 0 assert cs.main("--skip=*ignoredir*", tmp_path) == 1 assert cs.main("--skip=ignoredir", tmp_path) == 1 assert cs.main("--skip=*ignoredir/bad*", tmp_path) == 1 @@ -1213,7 +1215,7 @@ def test_ill_formed_ini_config_file( assert "ill-formed config file" in stderr -@pytest.mark.parametrize("kind", ["cfg", "toml", "toml_list"]) +@pytest.mark.parametrize("kind", ["cfg", "cfg_multiline", "toml", "toml_list"]) def test_config_toml( tmp_path: Path, capsys: pytest.CaptureFixture[str], @@ -1235,44 +1237,44 @@ def test_config_toml( assert "bad.txt" in stdout assert "abandonned.txt" in stdout - if kind == "cfg": + if kind.startswith("cfg"): conffile = tmp_path / "setup.cfg" args = ("--config", conffile) - conffile.write_text( - """\ + if kind == "cfg": + text = """\ [codespell] skip = bad.txt, whatever.txt count = """ - ) - elif kind == "toml": - assert kind == "toml" + else: + assert kind == "cfg_multiline" + text = """\ +[codespell] +skip = bad.txt, whatever.txt +count = +""" + conffile.write_text(text) + else: if sys.version_info < (3, 11): pytest.importorskip("tomli") tomlfile = tmp_path / "pyproject.toml" args = ("--toml", tomlfile) - tomlfile.write_text( - """\ + if kind == "toml": + text = """\ [tool.codespell] skip = 'bad.txt,whatever.txt' check-filenames = false count = true """ - ) - else: - assert kind == "toml_list" - if sys.version_info < (3, 11): - pytest.importorskip("tomli") - tomlfile = tmp_path / "pyproject.toml" - args = ("--toml", tomlfile) - tomlfile.write_text( - """\ + else: + assert kind == "toml_list" + text = """\ [tool.codespell] skip = ['bad.txt', 'whatever.txt'] check-filenames = false count = true """ - ) + tomlfile.write_text(text) # Should pass when skipping bad.txt or abandonned.txt result = cs.main(d, *args, std=True)