Skip to content

Commit

Permalink
feat(config): Support --config in pyproject.toml
Browse files Browse the repository at this point in the history
You can now specify `config` key in pyproject.toml which does the same
job as the CLI option `--config`. The path of config should be relative
to the CWD it is running from. If the file is not found or is invalid
i.e. it is a folder rather a TOML file it would raise
`click.exceptions.FileError`, and if the file can't be parsed it would
raise `tomli.TOMLDecodeError`. All black config keys in the custom
config should be registered under `[black]` and they would overwrite the
config specified in pyproject but not the one specified by `--config`
flag while running black with the CLI.

Closes psf#1826
  • Loading branch information
Shivansh-007 committed Oct 7, 2021
1 parent 3b2a7d1 commit f6e1aad
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 20 deletions.
12 changes: 8 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
.venv
.coverage
.coverage.*
_build
.DS_Store
.vscode
docs/_static/pypi.svg
.tox
__pycache__
black.egg-info
build/
Expand All @@ -16,6 +13,13 @@ src/_black_version.py
.eggs
.dmypy.json
*.swp
.hypothesis/
venv/
.ipynb_checkpoints/

# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.hypothesis/
.pytest_cache/
58 changes: 42 additions & 16 deletions src/black/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@
from black.concurrency import cancel, shutdown, maybe_install_uvloop
from black.output import dump_to_file, ipynb_diff, diff, color_diff, out, err
from black.report import Report, Changed, NothingChanged
from black.files import find_project_root, find_pyproject_toml, parse_pyproject_toml
from black.files import (
find_project_root,
find_pyproject_toml,
parse_pyproject_toml,
parse_black_config_toml,
)
from black.files import gen_python_files, get_gitignore, normalize_path_maybe_ignore
from black.files import wrap_stream_for_windows
from black.parsing import InvalidInput # noqa F401
Expand Down Expand Up @@ -106,7 +111,10 @@ def read_pyproject_toml(
Returns the path to a successfully found and read configuration file, None
otherwise.
"""
value_is_pyproject_config = False

if not value:
value_is_pyproject_config = True
value = find_pyproject_toml(ctx.params.get("src", ()))
if value is None:
return None
Expand All @@ -120,26 +128,44 @@ 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 ctx.default_map:
config.update(ctx.default_map)

black_config = config.get("config")

if black_config:
black_config_path = Path(black_config).resolve()

try:
custom_black_config = parse_black_config_toml(str(black_config_path))
except (OSError, ValueError) as e:
raise click.FileError(
filename=str(black_config_path),
hint=f"Error reading configuration file: {e}",
) from None
else:
# Do overwrite the clashing keys of pyproject.toml but don't
# overwrite them for `--config` passed through the CLI.
if value_is_pyproject_config:
config.update(custom_black_config)
else:
custom_black_config.update(config)
config = custom_black_config.copy()

# 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
default_map: Dict[str, Any] = {
k: str(v) if not isinstance(v, (list, dict)) else v for k, v in config.items()
}

target_version = default_map.get("target_version")
if target_version is not None and not isinstance(target_version, list):
raise click.BadOptionUsage(
"target-version", "Config key target-version must be a list"
)

default_map: Dict[str, Any] = {}
if ctx.default_map:
default_map.update(ctx.default_map)
default_map.update(config)

ctx.default_map = default_map
return value

Expand Down
11 changes: 11 additions & 0 deletions src/black/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ def parse_pyproject_toml(path_config: str) -> Dict[str, Any]:
return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}


def parse_black_config_toml(path_config: str) -> Dict[str, Any]:
"""Parse the custom black config provided in pyproject.toml.
If parsing fails, will raise a tomli.TOMLDecodeError
"""
with open(path_config, encoding="utf8") as f:
pyproject_toml = tomli.load(f) # type: ignore # due to deprecated API usage
config = pyproject_toml.get("black", {})
return {k.replace("--", "").replace("-", "_"): v for k, v in config.items()}


@lru_cache()
def find_user_pyproject_toml() -> Path:
r"""Return the path to the top-level user configuration for black.
Expand Down
17 changes: 17 additions & 0 deletions tests/invalid_test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[tool.black]
config = "tests/bazqux.toml"
verbose = 1
--check = "no"
diff = "y"
color = true
line-length = 79
target-version = ["py36", "py37", "py38"]
exclude='\.pyi?$'
include='\.py?$'

[v1.0.0-syntax]
# This shouldn't break Black.
contributors = [
"Foo Bar <foo@example.com>",
{ name = "Baz Qux", email = "bazqux@example.com", url = "https://example.com/bazqux" }
]
1 change: 1 addition & 0 deletions tests/test.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[tool.black]
config = "tests/test_black.toml"
verbose = 1
--check = "no"
diff = "y"
Expand Down
14 changes: 14 additions & 0 deletions tests/test_black.py
Original file line number Diff line number Diff line change
Expand Up @@ -1277,20 +1277,34 @@ def test_parse_pyproject_toml(self) -> None:
self.assertEqual(config["exclude"], r"\.pyi?$")
self.assertEqual(config["include"], r"\.py?$")

def test_parse_black_config(self) -> None:
test_black_toml_file = THIS_DIR / "test_black.toml"
config = black.parse_black_config_toml(str(test_black_toml_file))
self.assertEqual(config["line_length"], 88)

def test_read_pyproject_toml(self) -> None:
test_toml_file = THIS_DIR / "test.toml"
fake_ctx = FakeContext()
black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))
config = fake_ctx.default_map
self.assertEqual(config["config"], "tests/test_black.toml")
self.assertEqual(config["verbose"], "1")
self.assertEqual(config["check"], "no")
self.assertEqual(config["diff"], "y")
self.assertEqual(config["color"], "True")
# Test that pyproject.toml specified config won't overwrite the config
# passed through `--config`.
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_invalid_black_config(self) -> None:
test_toml_file = THIS_DIR / "invalid_test.toml"
fake_ctx = FakeContext()
with self.assertRaises(click.exceptions.FileError):
black.read_pyproject_toml(fake_ctx, FakeParameter(), str(test_toml_file))

def test_find_project_root(self) -> None:
with TemporaryDirectory() as workspace:
root = Path(workspace)
Expand Down
2 changes: 2 additions & 0 deletions tests/test_black.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[black]
line-length = 88

0 comments on commit f6e1aad

Please sign in to comment.