Support for top-level user configuration (#1899)
* Added support for top-level user configuration At the user level, a TOML config can be specified in the following locations: * Windows: ~\.black * Unix-like: $XDG_CONFIG_HOME/black (~/.config/black fallback) Instead of changing env vars for the entire black-primer process, they are now changed only for the black subprocess, using a tmpdir.
This commit is contained in:
parent
ed9d58b741
commit
9451c57d1c
14
README.md
14
README.md
@ -293,6 +293,20 @@ parent directories. It stops looking when it finds the file, or a `.git` directo
|
|||||||
If you're formatting standard input, _Black_ will look for configuration starting from
|
If you're formatting standard input, _Black_ will look for configuration starting from
|
||||||
the current working directory.
|
the current working directory.
|
||||||
|
|
||||||
|
You can use a "global" configuration, stored in a specific location in your home
|
||||||
|
directory. This will be used as a fallback configuration, that is, it will be used if
|
||||||
|
and only if _Black_ doesn't find any configuration as mentioned above. Depending on your
|
||||||
|
operating system, this configuration file should be stored as:
|
||||||
|
|
||||||
|
- Windows: `~\.black`
|
||||||
|
- Unix-like (Linux, MacOS, etc.): `$XDG_CONFIG_HOME/black` (`~/.config/black` if the
|
||||||
|
`XDG_CONFIG_HOME` environment variable is not set)
|
||||||
|
|
||||||
|
Note that these are paths to the TOML file itself (meaning that they shouldn't be named
|
||||||
|
as `pyproject.toml`), not directories where you store the configuration. Here, `~`
|
||||||
|
refers to the path to your home directory. On Windows, this will be something like
|
||||||
|
`C:\\Users\UserName`.
|
||||||
|
|
||||||
You can also explicitly specify the path to a particular file that you want with
|
You can also explicitly specify the path to a particular file that you want with
|
||||||
`--config`. In this situation _Black_ will not look for any other file.
|
`--config`. In this situation _Black_ will not look for any other file.
|
||||||
|
|
||||||
|
@ -28,6 +28,20 @@ parent directories. It stops looking when it finds the file, or a `.git` directo
|
|||||||
If you're formatting standard input, _Black_ will look for configuration starting from
|
If you're formatting standard input, _Black_ will look for configuration starting from
|
||||||
the current working directory.
|
the current working directory.
|
||||||
|
|
||||||
|
You can use a "global" configuration, stored in a specific location in your home
|
||||||
|
directory. This will be used as a fallback configuration, that is, it will be used if
|
||||||
|
and only if _Black_ doesn't find any configuration as mentioned above. Depending on your
|
||||||
|
operating system, this configuration file should be stored as:
|
||||||
|
|
||||||
|
- Windows: `~\.black`
|
||||||
|
- Unix-like (Linux, MacOS, etc.): `$XDG_CONFIG_HOME/black` (`~/.config/black` if the
|
||||||
|
`XDG_CONFIG_HOME` environment variable is not set)
|
||||||
|
|
||||||
|
Note that these are paths to the TOML file itself (meaning that they shouldn't be named
|
||||||
|
as `pyproject.toml`), not directories where you store the configuration. Here, `~`
|
||||||
|
refers to the path to your home directory. On Windows, this will be something like
|
||||||
|
`C:\\Users\UserName`.
|
||||||
|
|
||||||
You can also explicitly specify the path to a particular file that you want with
|
You can also explicitly specify the path to a particular file that you want with
|
||||||
`--config`. In this situation _Black_ will not look for any other file.
|
`--config`. In this situation _Black_ will not look for any other file.
|
||||||
|
|
||||||
|
@ -306,7 +306,11 @@ def find_pyproject_toml(path_search_start: Iterable[str]) -> Optional[str]:
|
|||||||
"""Find the absolute filepath to a pyproject.toml if it exists"""
|
"""Find the absolute filepath to a pyproject.toml if it exists"""
|
||||||
path_project_root = find_project_root(path_search_start)
|
path_project_root = find_project_root(path_search_start)
|
||||||
path_pyproject_toml = path_project_root / "pyproject.toml"
|
path_pyproject_toml = path_project_root / "pyproject.toml"
|
||||||
return str(path_pyproject_toml) if path_pyproject_toml.is_file() else None
|
if path_pyproject_toml.is_file():
|
||||||
|
return str(path_pyproject_toml)
|
||||||
|
|
||||||
|
path_user_pyproject_toml = find_user_pyproject_toml()
|
||||||
|
return str(path_user_pyproject_toml) if path_user_pyproject_toml.is_file() else None
|
||||||
|
|
||||||
|
|
||||||
def parse_pyproject_toml(path_config: str) -> Dict[str, Any]:
|
def parse_pyproject_toml(path_config: str) -> Dict[str, Any]:
|
||||||
@ -6248,6 +6252,22 @@ def find_project_root(srcs: Iterable[str]) -> Path:
|
|||||||
return directory
|
return directory
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def find_user_pyproject_toml() -> Path:
|
||||||
|
r"""Return the path to the top-level user configuration for black.
|
||||||
|
|
||||||
|
This looks for ~\.black on Windows and ~/.config/black on Linux and other
|
||||||
|
Unix systems.
|
||||||
|
"""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# Windows
|
||||||
|
user_config_path = Path.home() / ".black"
|
||||||
|
else:
|
||||||
|
config_root = os.environ.get("XDG_CONFIG_HOME", "~/.config")
|
||||||
|
user_config_path = Path(config_root).expanduser() / "black"
|
||||||
|
return user_config_path.resolve()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Report:
|
class Report:
|
||||||
"""Provides a reformatting counter. Can be rendered with `str(report)`."""
|
"""Provides a reformatting counter. Can be rendered with `str(report)`."""
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
from shutil import rmtree, which
|
from shutil import rmtree, which
|
||||||
from subprocess import CalledProcessError
|
from subprocess import CalledProcessError
|
||||||
from sys import version_info
|
from sys import version_info
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any, Callable, Dict, NamedTuple, Optional, Sequence, Tuple
|
from typing import Any, Callable, Dict, NamedTuple, Optional, Sequence, Tuple
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@ -121,28 +122,36 @@ async def black_run(
|
|||||||
cmd.extend(*project_config["cli_arguments"])
|
cmd.extend(*project_config["cli_arguments"])
|
||||||
cmd.extend(["--check", "--diff", "."])
|
cmd.extend(["--check", "--diff", "."])
|
||||||
|
|
||||||
try:
|
with TemporaryDirectory() as tmp_path:
|
||||||
_stdout, _stderr = await _gen_check_output(cmd, cwd=repo_path)
|
# Prevent reading top-level user configs by manipulating envionment variables
|
||||||
except asyncio.TimeoutError:
|
env = {
|
||||||
results.stats["failed"] += 1
|
**os.environ,
|
||||||
LOG.error(f"Running black for {repo_path} timed out ({cmd})")
|
"XDG_CONFIG_HOME": tmp_path, # Unix-like
|
||||||
except CalledProcessError as cpe:
|
"USERPROFILE": tmp_path, # Windows (changes `Path.home()` output)
|
||||||
# TODO: Tune for smarter for higher signal
|
}
|
||||||
# If any other return value than 1 we raise - can disable project in config
|
|
||||||
if cpe.returncode == 1:
|
try:
|
||||||
if not project_config["expect_formatting_changes"]:
|
_stdout, _stderr = await _gen_check_output(cmd, cwd=repo_path, env=env)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
results.stats["failed"] += 1
|
||||||
|
LOG.error(f"Running black for {repo_path} timed out ({cmd})")
|
||||||
|
except CalledProcessError as cpe:
|
||||||
|
# TODO: Tune for smarter for higher signal
|
||||||
|
# If any other return value than 1 we raise - can disable project in config
|
||||||
|
if cpe.returncode == 1:
|
||||||
|
if not project_config["expect_formatting_changes"]:
|
||||||
|
results.stats["failed"] += 1
|
||||||
|
results.failed_projects[repo_path.name] = cpe
|
||||||
|
else:
|
||||||
|
results.stats["success"] += 1
|
||||||
|
return
|
||||||
|
elif cpe.returncode > 1:
|
||||||
results.stats["failed"] += 1
|
results.stats["failed"] += 1
|
||||||
results.failed_projects[repo_path.name] = cpe
|
results.failed_projects[repo_path.name] = cpe
|
||||||
else:
|
return
|
||||||
results.stats["success"] += 1
|
|
||||||
return
|
|
||||||
elif cpe.returncode > 1:
|
|
||||||
results.stats["failed"] += 1
|
|
||||||
results.failed_projects[repo_path.name] = cpe
|
|
||||||
return
|
|
||||||
|
|
||||||
LOG.error(f"Unknown error with {repo_path}")
|
LOG.error(f"Unknown error with {repo_path}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# If we get here and expect formatting changes something is up
|
# If we get here and expect formatting changes something is up
|
||||||
if project_config["expect_formatting_changes"]:
|
if project_config["expect_formatting_changes"]:
|
||||||
|
@ -296,6 +296,7 @@ def test_expression_ff(self) -> None:
|
|||||||
|
|
||||||
def test_expression_diff(self) -> None:
|
def test_expression_diff(self) -> None:
|
||||||
source, _ = read_data("expression.py")
|
source, _ = read_data("expression.py")
|
||||||
|
config = THIS_DIR / "data" / "empty_pyproject.toml"
|
||||||
expected, _ = read_data("expression.diff")
|
expected, _ = read_data("expression.diff")
|
||||||
tmp_file = Path(black.dump_to_file(source))
|
tmp_file = Path(black.dump_to_file(source))
|
||||||
diff_header = re.compile(
|
diff_header = re.compile(
|
||||||
@ -303,7 +304,9 @@ def test_expression_diff(self) -> None:
|
|||||||
r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
|
r"\d\d:\d\d:\d\d\.\d\d\d\d\d\d \+\d\d\d\d"
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
result = BlackRunner().invoke(black.main, ["--diff", str(tmp_file)])
|
result = BlackRunner().invoke(
|
||||||
|
black.main, ["--diff", str(tmp_file), f"--config={config}"]
|
||||||
|
)
|
||||||
self.assertEqual(result.exit_code, 0)
|
self.assertEqual(result.exit_code, 0)
|
||||||
finally:
|
finally:
|
||||||
os.unlink(tmp_file)
|
os.unlink(tmp_file)
|
||||||
@ -320,11 +323,12 @@ def test_expression_diff(self) -> None:
|
|||||||
|
|
||||||
def test_expression_diff_with_color(self) -> None:
|
def test_expression_diff_with_color(self) -> None:
|
||||||
source, _ = read_data("expression.py")
|
source, _ = read_data("expression.py")
|
||||||
|
config = THIS_DIR / "data" / "empty_pyproject.toml"
|
||||||
expected, _ = read_data("expression.diff")
|
expected, _ = read_data("expression.diff")
|
||||||
tmp_file = Path(black.dump_to_file(source))
|
tmp_file = Path(black.dump_to_file(source))
|
||||||
try:
|
try:
|
||||||
result = BlackRunner().invoke(
|
result = BlackRunner().invoke(
|
||||||
black.main, ["--diff", "--color", str(tmp_file)]
|
black.main, ["--diff", "--color", str(tmp_file), f"--config={config}"]
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
os.unlink(tmp_file)
|
os.unlink(tmp_file)
|
||||||
@ -1842,6 +1846,34 @@ def test_find_project_root(self) -> None:
|
|||||||
self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
|
self.assertEqual(black.find_project_root((src_dir,)), src_dir.resolve())
|
||||||
self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
|
self.assertEqual(black.find_project_root((src_python,)), src_dir.resolve())
|
||||||
|
|
||||||
|
@patch("black.find_user_pyproject_toml", black.find_user_pyproject_toml.__wrapped__)
|
||||||
|
def test_find_user_pyproject_toml_linux(self) -> None:
|
||||||
|
if system() == "Windows":
|
||||||
|
return
|
||||||
|
|
||||||
|
# Test if XDG_CONFIG_HOME is checked
|
||||||
|
with TemporaryDirectory() as workspace:
|
||||||
|
tmp_user_config = Path(workspace) / "black"
|
||||||
|
with patch.dict("os.environ", {"XDG_CONFIG_HOME": workspace}):
|
||||||
|
self.assertEqual(
|
||||||
|
black.find_user_pyproject_toml(), tmp_user_config.resolve()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test fallback for XDG_CONFIG_HOME
|
||||||
|
with patch.dict("os.environ"):
|
||||||
|
os.environ.pop("XDG_CONFIG_HOME", None)
|
||||||
|
fallback_user_config = Path("~/.config").expanduser() / "black"
|
||||||
|
self.assertEqual(
|
||||||
|
black.find_user_pyproject_toml(), fallback_user_config.resolve()
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_find_user_pyproject_toml_windows(self) -> None:
|
||||||
|
if system() != "Windows":
|
||||||
|
return
|
||||||
|
|
||||||
|
user_config_path = Path.home() / ".black"
|
||||||
|
self.assertEqual(black.find_user_pyproject_toml(), user_config_path.resolve())
|
||||||
|
|
||||||
def test_bpo_33660_workaround(self) -> None:
|
def test_bpo_33660_workaround(self) -> None:
|
||||||
if system() == "Windows":
|
if system() == "Windows":
|
||||||
return
|
return
|
||||||
|
Loading…
Reference in New Issue
Block a user