Repository: psf/black

Issue:
setting line-length in pyproject.toml prevents Black from running in vim
**Describe the bug**

If `line-length` is set in `pyproject.toml`, running `black` in Vim fails and throws the following error:

```
'<=' not supported between instances of 'int' and 'str'
```

**To Reproduce**

The minimum example that reproduces the error for me:
```black-bug
├── pyproject.toml
└── test.py
```

The contents of `pyproject.toml` follow the same syntax as is provided by [`pyproject.toml` in the `black` repo](https://github.com/psf/black/blob/6bedb5c58a7d8c25aa9509f8217bc24e9797e90d/pyproject.toml#L9)
```
[tool.black]
line-length = 79
```

The contents of `test.py`:
```
foo = 1
```

Running `black` in Vim on `test.py` throws the error:
```
'<=' not supported between instances of 'int' and 'str'
```

I can successfully run `black` on `test.py` outside of vim:
```
~/.vim/black/bin/black test.py
```
or
```
black test.py
```

If I comment out `line-length` in `pyproject.toml`, the error we see when calling `black` in Vim goes away:
```
[tool.black]
#line-length = 79
```

The problem seems to be how `line-length` is parsed and cast by `black`.

**Expected behavior**
`black` runs without errors, and utilizes the `line-length` value specified in `pyproject.toml`.

**Environment (please complete the following information):**

- Version: `19.10b0`
- OS and Python version: macOS 10.15.5 and Python 3.8.3; also reproduced on Ubuntu 20.04 and Python 3.8.3.

**Does this bug also happen on master?**

> To answer this, you have two options:
> 1. Use the online formatter at https://black.now.sh/?version=master, which will use the latest master branch.

n/a

> 2. Or run _Black_ on your machine:
>    - create a new virtualenv (make sure it's the same Python version);
>    - clone this repository;
>    - run `pip install -e .`;
>    - make sure it's sane by running `python -m unittest`; and
>    - run `black` like you did last time.

I get errors running `python -m unittest` but unsure related to this bug:

```
$ python -m unittest                                                                                                                                   
E....[2020-06-14 14:22:15,341] DEBUG: Using selector: EpollSelector (selector_events.py:59)
.[2020-06-14 14:22:15,342] DEBUG: Using selector: EpollSelector (selector_events.py:59)
.[2020-06-14 14:22:15,680] DEBUG: Using selector: EpollSelector (selector_events.py:59)
.[2020-06-14 14:22:15,682] DEBUG: Using selector: EpollSelector (selector_events.py:59)
[2020-06-14 14:22:15,683] INFO: 16 projects to run Black over (lib.py:311)
[2020-06-14 14:22:15,683] DEBUG: Using 2 parallel workers to run Black (lib.py:316)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on aioexabgp (lib.py:247)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on attrs (lib.py:247)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on bandersnatch (lib.py:247)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on channels (lib.py:247)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on django (lib.py:247)
[2020-06-14 14:22:15,684] INFO: Skipping django as it's disabled via config (lib.py:254)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on flake8-bugbear (lib.py:247)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on hypothesis (lib.py:247)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on pandas (lib.py:247)
[2020-06-14 14:22:15,684] INFO: Skipping pandas as it's disabled via config (lib.py:254)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on poetry (lib.py:247)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on ptr (lib.py:247)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on pyramid (lib.py:247)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on pytest (lib.py:247)
[2020-06-14 14:22:15,684] INFO: Skipping pytest as it's disabled via config (lib.py:254)
[2020-06-14 14:22:15,684] DEBUG: worker 0 working on sqlalchemy (lib.py:247)
[2020-06-14 14:22:15,685] DEBUG: worker 0 working on tox (lib.py:247)
[2020-06-14 14:22:15,685] DEBUG: worker 0 working on virtualenv (lib.py:247)
[2020-06-14 14:22:15,685] DEBUG: worker 0 working on warehouse (lib.py:247)
[2020-06-14 14:22:15,685] DEBUG: project_runner 0 exiting (lib.py:245)
[2020-06-14 14:22:15,685] DEBUG: project_runner 1 exiting (lib.py:245)
[2020-06-14 14:22:15,685] INFO: Analyzing results (lib.py:327)
.
======================================================================
ERROR: tests.test_black (unittest.loader._FailedTest)
----------------------------------------------------------------------
ImportError: Failed to import test module: tests.test_black
Traceback (most recent call last):
  File "/home/username/miniconda3/envs/ml4sts/lib/python3.8/unittest/loader.py", line 436, in _find_test_path
    module = self._get_module_from_name(name)
  File "/home/username/miniconda3/envs/ml4sts/lib/python3.8/unittest/loader.py", line 377, in _get_module_from_name
    __import__(name)
  File "/home/erik/black/tests/test_black.py", line 1766, in <module>
    class BlackDTestCase(AioHTTPTestCase):
NameError: name 'AioHTTPTestCase' is not defined


----------------------------------------------------------------------
Ran 9 tests in 0.348s

FAILED (errors=1)
```


Discussion:
I can fix this, I introduced this regression trying to fix #1458.

Resolving patch:
diff --git a/src/black/__init__.py b/src/black/__init__.py
--- a/src/black/__init__.py
+++ b/src/black/__init__.py
@@ -281,12 +281,7 @@ def parse_pyproject_toml(path_config: str) -> Dict[str, Any]:
     """
     pyproject_toml = toml.load(path_config)
     config = pyproject_toml.get("tool", {}).get("black", {})
-    return {
-        k.replace("--", "").replace("-", "_"): str(v)
-        if not isinstance(v, (list, dict))
-        else v
-        for k, v in config.items()
-    }
+    return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}
 
 
 def read_pyproject_toml(
@@ -311,6 +306,14 @@ def read_pyproject_toml(
 
     if not config:
         return None
+    else:
+        # Sanitize the values to be Click friendly. For more information please see:
+        # https://github.com/psf/black/issues/1458
+        # https://github.com/pallets/click/issues/1567
+        config = {
+            k: str(v) if not isinstance(v, (list, dict)) else v
+            for k, v in config.items()
+        }
 
     target_version = config.get("target_version")
     if target_version is not None and not isinstance(target_version, list):


Test patch:
diff --git a/tests/test.toml b/tests/test.toml
new file mode 100644
--- /dev/null
+++ b/tests/test.toml
@@ -0,0 +1,10 @@
+[tool.black]
+verbose = 1
+--check = "no"
+diff = "y"
+color = true
+line-length = 79
+target-version = ["py36", "py37", "py38"]
+exclude='\.pyi?$'
+include='\.py?$'
+
diff --git a/tests/test_black.py b/tests/test_black.py
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -10,10 +10,11 @@
 import regex as re
 import sys
 from tempfile import TemporaryDirectory
-from typing import Any, BinaryIO, Generator, List, Tuple, Iterator, TypeVar
+from typing import Any, BinaryIO, Dict, Generator, List, Tuple, Iterator, TypeVar
 import unittest
 from unittest.mock import patch, MagicMock
 
+import click
 from click import unstyle
 from click.testing import CliRunner
 
@@ -1762,6 +1763,44 @@ def test_invalid_config_return_code(self) -> None:
         finally:
             tmp_file.unlink()
 
+    def test_parse_pyproject_toml(self) -> None:
+        test_toml_file = THIS_DIR / "test.toml"
+        config = black.parse_pyproject_toml(str(test_toml_file))
+        self.assertEqual(config["verbose"], 1)
+        self.assertEqual(config["check"], "no")
+        self.assertEqual(config["diff"], "y")
+        self.assertEqual(config["color"], True)
+        self.assertEqual(config["line_length"], 79)
+        self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
+        self.assertEqual(config["exclude"], r"\.pyi?$")
+        self.assertEqual(config["include"], r"\.py?$")
+
+    def test_read_pyproject_toml(self) -> None:
+        test_toml_file = THIS_DIR / "test.toml"
+
+        # Fake a click context and parameter so mypy stays happy
+        class FakeContext(click.Context):
+            def __init__(self) -> None:
+                self.default_map: Dict[str, Any] = {}
+
+        class FakeParameter(click.Parameter):
+            def __init__(self) -> None:
+                pass
+
+        fake_ctx = FakeContext()
+        black.read_pyproject_toml(
+            fake_ctx, FakeParameter(), str(test_toml_file),
+        )
+        config = fake_ctx.default_map
+        self.assertEqual(config["verbose"], "1")
+        self.assertEqual(config["check"], "no")
+        self.assertEqual(config["diff"], "y")
+        self.assertEqual(config["color"], "True")
+        self.assertEqual(config["line_length"], "79")
+        self.assertEqual(config["target_version"], ["py36", "py37", "py38"])
+        self.assertEqual(config["exclude"], r"\.pyi?$")
+        self.assertEqual(config["include"], r"\.py?$")
+
 
 class BlackDTestCase(AioHTTPTestCase):
     async def get_application(self) -> web.Application: