From bd7dcaefd3591882073683f912e3cd665406bb62 Mon Sep 17 00:00:00 2001 From: Valter Silva Date: Sat, 27 Jun 2026 14:25:05 +0800 Subject: [PATCH] Add --cache-dir command-line option Add a --cache-dir option to set the directory where Black reads and writes its cache. It takes precedence over the BLACK_CACHE_DIR environment variable, mirroring how --workers relates to BLACK_NUM_WORKERS. This helps when an environment variable can't be set for the invocation, such as running Black as a pre-commit hook in a monorepo where the cache should be written outside the project root. Closes #5174 --- CHANGES.md | 2 + .../file_collection_and_discovery.md | 22 ++++++++--- docs/usage_and_configuration/the_basics.md | 9 +++++ src/black/__init__.py | 25 ++++++++++++- src/black/cache.py | 19 ++++++---- src/black/resources/black.schema.json | 4 ++ tests/test_black.py | 37 +++++++++++++++++++ 7 files changed, 104 insertions(+), 14 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 8cd6700c831..bb2a014dfe4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -58,6 +58,8 @@ `srcs` are given) is now resolved before the `lru_cache` key is computed, so each directory gets the correct `pyproject.toml` (#5152) - Add validation for --line-ranges values (#5107) +- Add a `--cache-dir` command-line option to set the cache directory, taking precedence + over the `BLACK_CACHE_DIR` environment variable (#5201) ### Packaging diff --git a/docs/usage_and_configuration/file_collection_and_discovery.md b/docs/usage_and_configuration/file_collection_and_discovery.md index 6d17f4d5413..4aac5fc5070 100644 --- a/docs/usage_and_configuration/file_collection_and_discovery.md +++ b/docs/usage_and_configuration/file_collection_and_discovery.md @@ -22,12 +22,22 @@ run. The file is non-portable. The standard location on common operating systems `file-mode` is an int flag that determines whether the file was formatted as 3.6+ only, as .pyi, and whether string normalization was omitted. -To override the location of these files on all systems, set the environment variable -`BLACK_CACHE_DIR` to the preferred location. Alternatively on macOS and Linux, set -`XDG_CACHE_HOME` to your preferred location. For example, if you want to put the cache -in the directory you're running _Black_ from, set `BLACK_CACHE_DIR=.cache/black`. -_Black_ will then write the above files to `.cache/black`. Note that `BLACK_CACHE_DIR` -will take precedence over `XDG_CACHE_HOME` if both are set. +To override the location of these files on all systems, pass the `--cache-dir` option or +set the environment variable `BLACK_CACHE_DIR` to the preferred location. Alternatively +on macOS and Linux, set `XDG_CACHE_HOME` to your preferred location. For example, if you +want to put the cache in the directory you're running _Black_ from, set +`BLACK_CACHE_DIR=.cache/black`. _Black_ will then write the above files to +`.cache/black`. When more than one of these is set, the highest-precedence option wins: +`--cache-dir` takes precedence over `BLACK_CACHE_DIR`, which in turn takes precedence +over `XDG_CACHE_HOME`. + +The `--cache-dir` option is useful when you cannot set an environment variable for the +invocation, such as when running _Black_ as a [pre-commit](https://pre-commit.com/) hook +in a monorepo and you want the cache written outside the project root: + +```console +$ black --cache-dir build/black-cache . +``` ### Disabling the cache with --no-cache diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 4e34f70a3f5..ddab42e52d7 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -237,6 +237,15 @@ nor write to the cache. This is helpful for reproducing formatting results from run, debugging cache-related issues, or ensuring CI executes a fresh formatting analysis every time. +#### `--cache-dir` + +Set the directory where _Black_ reads and writes its cache. This can also be specified +via the `BLACK_CACHE_DIR` environment variable; `--cache-dir` takes precedence over the +environment variable. Defaults to the platform-specific user cache directory. This is +useful when you cannot set an environment variable for the invocation, for example when +running _Black_ as a pre-commit hook and you want the cache written outside the project +root. + #### `--color` / `--no-color` Show (or do not show) colored diff. Only applies when `--diff` is given. diff --git a/src/black/__init__.py b/src/black/__init__.py index e4a0edda879..bd0674ea4f3 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -26,8 +26,9 @@ from pathspec import GitIgnoreSpec from pathspec.patterns.gitignore import GitIgnorePatternError +import black.cache from _black_version import version as __version__ -from black.cache import Cache +from black.cache import Cache, get_cache_dir from black.comments import normalize_fmt_off from black.const import ( DEFAULT_EXCLUDES, @@ -540,6 +541,21 @@ def validate_regex( " included files." ), ) +@click.option( + "--cache-dir", + type=click.Path( + file_okay=False, + dir_okay=True, + writable=True, + path_type=str, + ), + default=None, + help=( + "The directory where Black should read and write its cache. This takes" + " precedence over the BLACK_CACHE_DIR environment variable. Defaults to the" + " platform-specific user cache directory." + ), +) @click.pass_context def main( ctx: click.Context, @@ -572,6 +588,7 @@ def main( src: tuple[str, ...], config: str | None, no_cache: bool, + cache_dir: str | None, ) -> None: """The uncompromising code formatter.""" ctx.ensure_object(dict) @@ -697,6 +714,12 @@ def main( report = Report(check=check, diff=diff, quiet=quiet, verbose=verbose) + if cache_dir is not None: + # Redirect the on-disk cache for this run. This takes precedence over the + # BLACK_CACHE_DIR environment variable. Reassigning the module-level constant + # is what the cache read/write helpers consult at runtime. + black.cache.CACHE_DIR = get_cache_dir(override=Path(cache_dir)) + if code is not None: reformat_code( content=code, diff --git a/src/black/cache.py b/src/black/cache.py index ef9d99a7b90..fd011c05934 100644 --- a/src/black/cache.py +++ b/src/black/cache.py @@ -8,7 +8,7 @@ from collections.abc import Iterable from dataclasses import dataclass, field from pathlib import Path -from typing import NamedTuple +from typing import NamedTuple, Optional from platformdirs import user_cache_dir @@ -28,19 +28,24 @@ class FileData(NamedTuple): hash: str -def get_cache_dir() -> Path: +def get_cache_dir(override: Optional[Path] = None) -> Path: """Get the cache directory used by black. - Users can customize this directory on all systems using `BLACK_CACHE_DIR` - environment variable. By default, the cache directory is the user cache directory - under the black application. + The directory is resolved with the following precedence (highest first): + + 1. An explicit `override` (e.g. the `--cache-dir` command-line option). + 2. The `BLACK_CACHE_DIR` environment variable. + 3. The platform-specific user cache directory. This result is immediately set to a constant `black.cache.CACHE_DIR` as to avoid repeated calls. """ # NOTE: Function mostly exists as a clean way to test getting the cache directory. - default_cache_dir = user_cache_dir("black") - cache_dir = Path(os.environ.get("BLACK_CACHE_DIR", default_cache_dir)) + if override is not None: + cache_dir = override + else: + default_cache_dir = user_cache_dir("black") + cache_dir = Path(os.environ.get("BLACK_CACHE_DIR", default_cache_dir)) cache_dir = cache_dir / __version__ return cache_dir diff --git a/src/black/resources/black.schema.json b/src/black/resources/black.schema.json index f467f1a6879..9aaab3a81b6 100644 --- a/src/black/resources/black.schema.json +++ b/src/black/resources/black.schema.json @@ -156,6 +156,10 @@ "type": "boolean", "description": "Skip reading and writing the cache, forcing Black to reformat all included files.", "default": false + }, + "cache-dir": { + "type": "string", + "description": "The directory where Black should read and write its cache. This takes precedence over the BLACK_CACHE_DIR environment variable. Defaults to the platform-specific user cache directory." } } } diff --git a/tests/test_black.py b/tests/test_black.py index c958acd86ba..8c638375b7c 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -2211,6 +2211,43 @@ def test_get_cache_dir( monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2)) assert get_cache_dir().parent == workspace2 + def test_get_cache_dir_override_takes_precedence( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + ) -> None: + env_dir = tmp_path / "from-env" + cli_dir = tmp_path / "from-cli" + + # An explicit override wins over BLACK_CACHE_DIR. + monkeypatch.setenv("BLACK_CACHE_DIR", str(env_dir)) + assert get_cache_dir(override=cli_dir).parent == cli_dir + + # Without an override we fall back to the env var. + assert get_cache_dir().parent == env_dir + + def test_cache_dir_cli_argument(self, tmp_path: Path) -> None: + # The `cache_dir()` fixture patches black.cache.CACHE_DIR to `default_loc` + # (and restores it afterwards). Passing --cache-dir must take precedence over + # that already-established location, proving the option wins at runtime. + with cache_dir() as default_loc: + src = (tmp_path / "test.py").resolve() + src.write_text("print('hello')", encoding="utf-8") + cli_loc = tmp_path / "custom-cache" + + invokeBlack([str(src), "--cache-dir", str(cli_loc)]) + + cache_file = ( + cli_loc + / black.__version__ + / f"cache.{DEFAULT_MODE.get_cache_key()}.pickle" + ) + assert cache_file.exists() + # Nothing should have been written to the default cache location. + assert not ( + default_loc / f"cache.{DEFAULT_MODE.get_cache_key()}.pickle" + ).exists() + def test_cache_file_length(self) -> None: cases = [ DEFAULT_MODE,