diff --git a/CHANGES.md b/CHANGES.md index cb71e3f4ec4..65eba6ce673 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -27,6 +27,8 @@ +- Add `no_cache` option to control caching behavior. (#4803) + ### Packaging diff --git a/docs/usage_and_configuration/file_collection_and_discovery.md b/docs/usage_and_configuration/file_collection_and_discovery.md index de1d5e6c11e..d62b0cf6379 100644 --- a/docs/usage_and_configuration/file_collection_and_discovery.md +++ b/docs/usage_and_configuration/file_collection_and_discovery.md @@ -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 diff --git a/docs/usage_and_configuration/the_basics.md b/docs/usage_and_configuration/the_basics.md index 0c032261796..b7a33f727c9 100644 --- a/docs/usage_and_configuration/the_basics.md +++ b/docs/usage_and_configuration/the_basics.md @@ -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. diff --git a/src/black/__init__.py b/src/black/__init__.py index c88aa4ef6c7..079e95cf386 100644 --- a/src/black/__init__.py +++ b/src/black/__init__.py @@ -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, @@ -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) @@ -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 @@ -709,6 +719,7 @@ def main( # noqa: C901 mode=mode, report=report, workers=workers, + no_cache=no_cache, ) if verbose or not quiet: @@ -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. @@ -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) diff --git a/src/black/concurrency.py b/src/black/concurrency.py index f6a2b8a93be..53a61456b63 100644 --- a/src/black/concurrency.py +++ b/src/black/concurrency.py @@ -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() @@ -115,6 +116,7 @@ def reformat_many( report=report, loop=loop, executor=executor, + no_cache=no_cache, ) ) finally: @@ -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`. @@ -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) @@ -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) diff --git a/src/black/resources/black.schema.json b/src/black/resources/black.schema.json index ff517b5bbca..bbb3cdfbb84 100644 --- a/src/black/resources/black.schema.json +++ b/src/black/resources/black.schema.json @@ -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 } } } diff --git a/tests/test_black.py b/tests/test_black.py index 36ee7d9e1b9..291dc01421e 100644 --- a/tests/test_black.py +++ b/tests/test_black.py @@ -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():