Skip to content
Merged
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 @@ -27,6 +27,8 @@

<!-- Changes to how Black can be configured -->

- Add `no_cache` option to control caching behavior. (#4803)

### Packaging

<!-- Changes to how Black is packaged, such as dependency requirements -->
Expand Down
11 changes: 11 additions & 0 deletions docs/usage_and_configuration/file_collection_and_discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ 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.

### Disabling the cache with --no-cache

If you need Black to always perform a fresh analysis and not consult or update the
on-disk cache, use the `--no-cache` flag. When provided, Black will neither read from
nor write to the per-user cache. This is useful for debugging, for CI runs where you
want a deterministic fresh run, or when you suspect cache corruption.

Example:

python -m black --no-cache .

## .gitignore

If `--exclude` is not set, _Black_ will automatically ignore files and directories in
Expand Down
12 changes: 12 additions & 0 deletions docs/usage_and_configuration/the_basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,18 @@ All done! ✨ 🍰 ✨
1 file would be reformatted.
```

#### `--no-cache`

Do not consult or update Black's per-user cache during this run. When `--no-cache` is
specified, Black will perform fresh analysis for all files and will neither read from
nor write to the cache. This is helpful for reproducing formatting results from a clean
run, debugging cache-related issues, or ensuring CI executes a fresh formatting analysis
every time.

Example:

python -m black --no-cache .

#### `--color` / `--no-color`

Show (or do not show) colored diff. Only applies when `--diff` is given.
Expand Down
24 changes: 20 additions & 4 deletions src/black/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,14 @@ def validate_regex(
callback=read_pyproject_toml,
help="Read configuration options from a configuration file.",
)
@click.option(
"--no-cache",
is_flag=True,
help=(
"Skip reading and writing the cache, forcing Black to reformat all"
" included files."
),
)
@click.pass_context
def main( # noqa: C901
ctx: click.Context,
Expand Down Expand Up @@ -536,6 +544,7 @@ def main( # noqa: C901
workers: Optional[int],
src: tuple[str, ...],
config: Optional[str],
no_cache: bool,
) -> None:
"""The uncompromising code formatter."""
ctx.ensure_object(dict)
Expand Down Expand Up @@ -695,6 +704,7 @@ def main( # noqa: C901
mode=mode,
report=report,
lines=lines,
no_cache=no_cache,
)
else:
from black.concurrency import reformat_many
Expand All @@ -709,6 +719,7 @@ def main( # noqa: C901
mode=mode,
report=report,
workers=workers,
no_cache=no_cache,
)

if verbose or not quiet:
Expand Down Expand Up @@ -859,6 +870,7 @@ def reformat_one(
report: "Report",
*,
lines: Collection[tuple[int, int]] = (),
no_cache: bool = False,
) -> None:
"""Reformat a single file under `src` without spawning child processes.

Expand Down Expand Up @@ -888,16 +900,20 @@ def reformat_one(
):
changed = Changed.YES
else:
cache = Cache.read(mode)
if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
cache = None if no_cache else Cache.read(mode)
if cache is not None and write_back not in (
WriteBack.DIFF,
WriteBack.COLOR_DIFF,
):
if not cache.is_changed(src):
changed = Changed.CACHED
if changed is not Changed.CACHED and format_file_in_place(
src, fast=fast, write_back=write_back, mode=mode, lines=lines
):
changed = Changed.YES
if (write_back is WriteBack.YES and changed is not Changed.CACHED) or (
write_back is WriteBack.CHECK and changed is Changed.NO
if cache is not None and (
(write_back is WriteBack.YES and changed is not Changed.CACHED)
or (write_back is WriteBack.CHECK and changed is Changed.NO)
):
cache.write([src])
report.done(src, changed)
Expand Down
12 changes: 9 additions & 3 deletions src/black/concurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def reformat_many(
mode: Mode,
report: Report,
workers: Optional[int],
no_cache: bool = False,
) -> None:
"""Reformat multiple files using a ProcessPoolExecutor."""
maybe_install_uvloop()
Expand Down Expand Up @@ -115,6 +116,7 @@ def reformat_many(
report=report,
loop=loop,
executor=executor,
no_cache=no_cache,
)
)
finally:
Expand All @@ -134,6 +136,7 @@ async def schedule_formatting(
report: "Report",
loop: asyncio.AbstractEventLoop,
executor: "Executor",
no_cache: bool = False,
) -> None:
"""Run formatting of `sources` in parallel using the provided `executor`.

Expand All @@ -142,8 +145,11 @@ async def schedule_formatting(
`write_back`, `fast`, and `mode` options are passed to
:func:`format_file_in_place`.
"""
cache = Cache.read(mode)
if write_back not in (WriteBack.DIFF, WriteBack.COLOR_DIFF):
cache = None if no_cache else Cache.read(mode)
if cache is not None and write_back not in (
WriteBack.DIFF,
WriteBack.COLOR_DIFF,
):
sources, cached = cache.filtered_cached(sources)
for src in sorted(cached):
report.done(src, Changed.CACHED)
Expand Down Expand Up @@ -194,5 +200,5 @@ async def schedule_formatting(
report.done(src, changed)
if cancelled:
await asyncio.gather(*cancelled, return_exceptions=True)
if sources_to_cache:
if sources_to_cache and not no_cache and cache is not None:
cache.write(sources_to_cache)
5 changes: 5 additions & 0 deletions src/black/resources/black.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@
"type": "boolean",
"description": "Emit messages about files that were not changed or were ignored due to exclusion patterns. If Black is using a configuration file, a message detailing which one it is using will be emitted.",
"default": false
},
"no-cache": {
"type": "boolean",
"description": "Skip reading and writing the cache, forcing Black to reformat all included files.",
"default": false
}
}
}
47 changes: 47 additions & 0 deletions tests/test_black.py
Original file line number Diff line number Diff line change
Expand Up @@ -2233,6 +2233,53 @@ def test_no_cache_when_stdin(self) -> None:
cache_file = get_cache_file(mode)
assert not cache_file.exists()

def test_no_cache_flag_prevents_writes(self) -> None:
"""--no-cache should neither read nor write the cache"""
mode = DEFAULT_MODE
with cache_dir() as workspace:
src = (workspace / "test.py").resolve()
src.write_text("print('hello')", encoding="utf-8")
cache = black.Cache.read(mode)
# Pre-populate cache so the file is considered cached
cache.write([src])
with (
patch.object(black.Cache, "read") as read_cache,
patch.object(black.Cache, "write") as write_cache,
):
# Pass --no-cache; it should neither read nor write
invokeBlack([str(src), "--no-cache"])
read_cache.assert_not_called()
write_cache.assert_not_called()

def test_no_cache_with_multiple_files(self) -> None:
"""Formatting multiple files with --no-cache should not read or write cache
and should format files normally."""
mode = DEFAULT_MODE
with (cache_dir() as workspace,):
one = (workspace / "one.py").resolve()
one.write_text("print('hello')", encoding="utf-8")
two = (workspace / "two.py").resolve()
two.write_text("print('hello')", encoding="utf-8")

# Pre-populate cache for `one` so it would normally be skipped
cache = black.Cache.read(mode)
cache.write([one])

with (
patch.object(black.Cache, "read") as read_cache,
patch.object(black.Cache, "write") as write_cache,
):
# Run Black over the directory with --no-cache
invokeBlack([str(workspace), "--no-cache"])

# Cache should not be consulted or updated
read_cache.assert_not_called()
write_cache.assert_not_called()

# Both files should have been formatted (double quotes + newline)
assert one.read_text(encoding="utf-8") == 'print("hello")\n'
assert two.read_text(encoding="utf-8") == 'print("hello")\n'

def test_read_cache_no_cachefile(self) -> None:
mode = DEFAULT_MODE
with cache_dir():
Expand Down