Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
22 changes: 16 additions & 6 deletions docs/usage_and_configuration/file_collection_and_discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 9 additions & 0 deletions docs/usage_and_configuration/the_basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 24 additions & 1 deletion src/black/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Nit) If this is the only place where it's called with the override, why can't you run Path(cache_dir) / __version__ inline?


if code is not None:
reformat_code(
content=code,
Expand Down
19 changes: 12 additions & 7 deletions src/black/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
4 changes: 4 additions & 0 deletions src/black/resources/black.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
37 changes: 37 additions & 0 deletions tests/test_black.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading