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.

Discussion:
sacsar: Note: While the `tox` style check passes, `flake8` finds errors when run via `pre-commit` (including in `vulture/core.py`, which I didn't touch). I believe this is `flake8` and `black` fighting, but I didn't go too far down the rabbit hole and ended up using `--no-verify`. 
sacsar: > Note: While the `tox` style check passes, `flake8` finds errors when run via `pre-commit` (including in `vulture/core.py`, which I didn't touch). I believe this is `flake8` and `black` fighting, but I didn't go too far down the rabbit hole and ended up using `--no-verify`.

It turns out this isn't a conflict with `black`, it's a change in `flake8` between Python versions. When I run `tox` from a 3.12 virtualenv, the style check fails with:
```
tests/test_utils.py:146:32: E272 multiple spaces before keyword
vulture/config.py:119:69: E231 missing whitespace after ','
vulture/config.py:120:25: E231 missing whitespace after ','
vulture/config.py:120:30: E231 missing whitespace after ','
vulture/config.py:120:42: E231 missing whitespace after ','
vulture/config.py:130:59: E231 missing whitespace after ','
vulture/config.py:138:71: E231 missing whitespace after ','
vulture/core.py:158:41: E231 missing whitespace after ':'
vulture/core.py:172:50: E231 missing whitespace after ':'
```
When run from a 3.8 virtualenv, flake8 is happy. I was getting failures only from pre-commit because I had installed `pre-commit` from the 3.12 virtualenv and then switched virtualenvs.

It turns out that if you use `pyenv` and have pre-commit (or other utilities) installed with a Python it manages, it turns into a pain in the butt, so I ended up installing `pre-commit` via `pipx` with my system python (3.11.5). It turns out there is a [bug in a recent version of flake8](https://stackoverflow.com/questions/77401175/how-to-make-flake8-ignore-syntax-within-strings) that was fixed in 6.1.0.

tl;dr @jendrikseipp I think the fix is just to bump flake8 in `tox.ini`. Do you want a separate PR in the name of a clean changelog or should I just throw it in here?
jendrikseipp: Thanks for the changes and for looking into the flake8 issue! I'll merge this as is to have a clean history. Would be great if you could send another PR for the flake8 issue.

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]