Repository: jendrikseipp/vulture

Issue:
Switch to tomli/tomllib for toml parsing
Due to https://github.com/uiri/toml/issues/270, vulture won't run for projects with a `pyproject.toml` that contain an array with mixed types. According to https://github.com/python-poetry/poetry/issues/7094#issuecomment-1328127456, this is not an issue in the `tomllib` in Python 3.11+ or its backport (https://github.com/hukkin/tomli).

It looks like this should be a quick swap. I'll send a PR along when I have a moment.

Comments:

Patch:
diff --git a/.gitignore b/.gitignore
index df19119..0ec272f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ vulture.egg-info/
 .pytest_cache/
 .tox/
 .venv/
+.vscode/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c793814..660b0d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+# next (unreleased)
+* Switch to tomllib/tomli to support heterogeneous arrays (Sebastian Csar, #340).
+
 # 2.10 (2023-10-06)
 
 * Drop support for Python 3.7 (Jendrik Seipp, #323).
diff --git a/setup.py b/setup.py
index 3914be9..4854941 100644
--- a/setup.py
+++ b/setup.py
@@ -47,7 +47,7 @@ setuptools.setup(
         "Programming Language :: Python :: Implementation :: PyPy",
         "Topic :: Software Development :: Quality Assurance",
     ],
-    install_requires=["toml"],
+    install_requires=["tomli >= 1.1.0; python_version < '3.11'"],
     entry_points={"console_scripts": ["vulture = vulture.core:main"]},
     python_requires=">=3.8",
     packages=setuptools.find_packages(exclude=["tests"]),
diff --git a/vulture/config.py b/vulture/config.py
index 4aa0d2d..4e193fe 100644
--- a/vulture/config.py
+++ b/vulture/config.py
@@ -5,7 +5,10 @@ command-line arguments or the pyproject.toml file.
 import argparse
 import pathlib
 
-import toml
+try:
+    import tomllib
+except ModuleNotFoundError:
+    import tomli as tomllib
 
 from .version import __version__
 
@@ -76,7 +79,7 @@ def _parse_toml(infile):
         verbose = true
         paths = ["path1", "path2"]
     """
-    data = toml.load(infile)
+    data = tomllib.load(infile)
     settings = data.get("tool", {}).get("vulture", {})
     _check_input_config(settings)
     return settings
@@ -194,7 +197,7 @@ def make_config(argv=None, tomlfile=None):
     else:
         toml_path = pathlib.Path("pyproject.toml").resolve()
         if toml_path.is_file():
-            with open(toml_path) as fconfig:
+            with open(toml_path, "rb") as fconfig:
                 config = _parse_toml(fconfig)
             detected_toml_path = str(toml_path)
         else:


Test patch:
diff --git a/tests/test_config.py b/tests/test_config.py
index 494433d..04ce725 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -2,7 +2,7 @@
 Unit tests for config file and CLI argument parsing.
 """
 
-from io import StringIO
+from io import BytesIO
 from textwrap import dedent
 
 import pytest
@@ -17,6 +17,13 @@ from vulture.config import (
 )
 
 
+def get_toml_bytes(toml_str: str) -> BytesIO:
+    """
+    Wrap a string in BytesIO to play the role of the incoming config stream.
+    """
+    return BytesIO(bytes(toml_str, "utf-8"))
+
+
 def test_cli_args():
     """
     Ensure that CLI arguments are converted to a config object.
@@ -62,9 +69,48 @@ def test_toml_config():
         sort_by_size=True,
         verbose=True,
     )
-    data = StringIO(
+    data = get_toml_bytes(
+        dedent(
+            """\
+        [tool.vulture]
+        exclude = ["file*.py", "dir/"]
+        ignore_decorators = ["deco1", "deco2"]
+        ignore_names = ["name1", "name2"]
+        make_whitelist = true
+        min_confidence = 10
+        sort_by_size = true
+        verbose = true
+        paths = ["path1", "path2"]
+        """
+        )
+    )
+    result = _parse_toml(data)
+    assert isinstance(result, dict)
+    assert result == expected
+
+
+def test_toml_config_with_heterogenous_array():
+    """
+    Ensure parsing of TOML files results in a valid config object, even if some
+    other part of the file contains an array of mixed types.
+    """
+    expected = dict(
+        paths=["path1", "path2"],
+        exclude=["file*.py", "dir/"],
+        ignore_decorators=["deco1", "deco2"],
+        ignore_names=["name1", "name2"],
+        make_whitelist=True,
+        min_confidence=10,
+        sort_by_size=True,
+        verbose=True,
+    )
+    data = get_toml_bytes(
         dedent(
             """\
+        [tool.foo]
+        # comment for good measure
+        problem_array = [{a = 1}, [2,3,4], "foo"]
+
         [tool.vulture]
         exclude = ["file*.py", "dir/"]
         ignore_decorators = ["deco1", "deco2"]
@@ -87,7 +133,7 @@ def test_config_merging():
     If we have both CLI args and a ``pyproject.toml`` file, the CLI args should
     have precedence.
     """
-    toml = StringIO(
+    toml = get_toml_bytes(
         dedent(
             """\
         [tool.vulture]
@@ -131,7 +177,7 @@ def test_config_merging_missing():
     If we have set a boolean value in the TOML file, but not on the CLI, we
     want the TOML value to be taken.
     """
-    toml = StringIO(
+    toml = get_toml_bytes(
         dedent(
             """\
         [tool.vulture]
@@ -153,7 +199,7 @@ def test_config_merging_toml_paths_only():
     If we have paths in the TOML but not on the CLI, the TOML paths should be
     used.
     """
-    toml = StringIO(
+    toml = get_toml_bytes(
         dedent(
             """\
         [tool.vulture]