Repository: codespell-project/codespell

Issue:
PEP 518 Support pyproject.toml for configuration
It would be nice if this project supported reading configuration from `pyproject.toml` files.

INI files read by `configparser` don't have a well defined syntax, and PEP 518 is supported by a lot of other Python linting tools.

---
https://www.python.org/dev/peps/pep-0518/#tool-table


Discussion:

Resolving patch:
diff --git a/codespell_lib/_codespell.py b/codespell_lib/_codespell.py
--- a/codespell_lib/_codespell.py
+++ b/codespell_lib/_codespell.py
@@ -392,7 +392,8 @@ def parse_options(args):
                         help='print LINES of surrounding context')
     parser.add_argument('--config', type=str,
                         help='path to config file.')
-
+    parser.add_argument('--toml', type=str,
+                        help='path to a pyproject.toml file.')
     parser.add_argument('files', nargs='*',
                         help='files or directories to check')
 
@@ -404,6 +405,18 @@ def parse_options(args):
     if options.config:
         cfg_files.append(options.config)
     config = configparser.ConfigParser()
+
+    # Read toml before other config files.
+    if options.toml:
+        try:
+            import tomli
+        except Exception as exc:
+            raise ImportError(
+                f'tomli is required to read pyproject.toml but could not be '
+                f'imported, got: {exc}') from None
+        with open(options.toml, 'rb') as f:
+            data = tomli.load(f).get('tool', {})
+        config.read_dict(data)
     config.read(cfg_files)
 
     if config.has_section('codespell'):
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -43,7 +43,7 @@
                        'Operating System :: Unix',
                        'Operating System :: MacOS'],
           platforms='any',
-          python_requires='>=3.6',
+          python_requires='>=3.7',
           packages=[
               'codespell_lib',
               'codespell_lib.tests',
@@ -58,9 +58,12 @@
                   'codespell = codespell_lib:_script_main'
               ],
           },
+          # TODO: toml will need to be updated when 3.11 comes out as it's a
+          # CPython module there
           extras_require={
               "dev": ["check-manifest", "flake8", "pytest", "pytest-cov",
-                      "pytest-dependency"],
+                      "pytest-dependency", "tomli"],
               "hard-encoding-detection": ["chardet"],
+              "toml": ["tomli"],
           }
           )


Test patch:
diff --git a/codespell_lib/tests/test_basic.py b/codespell_lib/tests/test_basic.py
--- a/codespell_lib/tests/test_basic.py
+++ b/codespell_lib/tests/test_basic.py
@@ -791,35 +791,45 @@ def test_uri_regex_def():
         assert not uri_regex.findall(boilerplate % uri), uri
 
 
-def test_config(tmpdir, capsys):
-    """
-    Tests loading options from a config file.
-    """
-    d = str(tmpdir)
-
-    # Create sample files.
-    with open(op.join(d, 'bad.txt'), 'w') as f:
+@pytest.mark.parametrize('kind', ('toml', 'cfg'))
+def test_config_toml(tmp_path, capsys, kind):
+    """Test loading options from a config file or toml."""
+    d = tmp_path / 'files'
+    d.mkdir()
+    with open(d / 'bad.txt', 'w') as f:
         f.write('abandonned donn\n')
-    with open(op.join(d, 'good.txt'), 'w') as f:
+    with open(d / 'good.txt', 'w') as f:
         f.write("good")
 
-    # Create a config file.
-    conffile = op.join(d, 'config.cfg')
-    with open(conffile, 'w') as f:
-        f.write(
-            '[codespell]\n'
-            'skip = bad.txt\n'
-            'count = \n'
-        )
-
     # Should fail when checking both.
-    code, stdout, _ = cs.main(d, count=True, std=True)
+    code, stdout, _ = cs.main(str(d), count=True, std=True)
     # Code in this case is not exit code, but count of misspellings.
     assert code == 2
     assert 'bad.txt' in stdout
 
+    if kind == 'cfg':
+        conffile = str(tmp_path / 'config.cfg')
+        args = ('--config', conffile)
+        with open(conffile, 'w') as f:
+            f.write("""\
+[codespell]
+skip = bad.txt, whatever.txt
+count =
+""")
+    else:
+        assert kind == 'toml'
+        pytest.importorskip('tomli')
+        tomlfile = str(tmp_path / 'pyproject.toml')
+        args = ('--toml', tomlfile)
+        with open(tomlfile, 'w') as f:
+            f.write("""\
+[tool.codespell]
+skip = 'bad.txt,whatever.txt'
+count = false
+""")
+
     # Should pass when skipping bad.txt
-    code, stdout, _ = cs.main('--config', conffile, d, count=True, std=True)
+    code, stdout, _ = cs.main(str(d), *args, count=True, std=True)
     assert code == 0
     assert 'bad.txt' not in stdout