Repository: psf/black

Issue:
Custom cache directory to be thread/process safe
**Is your feature request related to a problem? Please describe.**

The cache files are not thread/process safe. I've checked multiple projects simultaneously in different processes but get permission errors when one process is writing to the cache while another is reading from it. On linux the fix is pretty easy: set a different temp directory for each process using the ``XDG_CACHE_HOME`` environment variable but there is no equivalent for Windows (there's somewhat an equivalent but you need a rather [specific setup](https://github.com/platformdirs/platformdirs/blob/main/src/platformdirs/windows.py#L157-L165) to use the environment variable).

**Describe the solution you'd like**

The solution that will work for my use case (and I think is the least amount of changes) is to create a custom environment variable that overrides using platformdirs to get the cache directory. Think this could look like:
```python
CACHE_DIR = Path(os.environ.get("BLACK_CACHE_DIR", user_cache_dir("black", version=__version__)))
if not CACHE_DIR.exists():
    raise RuntimeError(f"{CACHE_DIR} does not exist")
```

**Describe alternatives you've considered**

1. Add the command line option to turn off cacheing (as this ticket as #248 asked for) (guess is this  more work)
2. Add command line option to set the cache directory (guess is this is more work but not a lot)
3. Make the cache dir thread/process safe (guess is this is more work)


Discussion:

Resolving patch:
diff --git a/src/black/cache.py b/src/black/cache.py
--- a/src/black/cache.py
+++ b/src/black/cache.py
@@ -20,7 +20,23 @@
 Cache = Dict[str, CacheInfo]
 
 
-CACHE_DIR = Path(user_cache_dir("black", version=__version__))
+def get_cache_dir() -> 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.
+
+    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", version=__version__)
+    cache_dir = Path(os.environ.get("BLACK_CACHE_DIR", default_cache_dir))
+    return cache_dir
+
+
+CACHE_DIR = get_cache_dir()
 
 
 def read_cache(mode: Mode) -> Cache:


Test patch:
diff --git a/tests/test_black.py b/tests/test_black.py
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -40,7 +40,7 @@
 import black.files
 from black import Feature, TargetVersion
 from black import re_compile_maybe_verbose as compile_pattern
-from black.cache import get_cache_file
+from black.cache import get_cache_dir, get_cache_file
 from black.debug import DebugVisitor
 from black.output import color_diff, diff
 from black.report import Report
@@ -1601,6 +1601,33 @@ def test_equivalency_ast_parse_failure_includes_error(self) -> None:
 
 
 class TestCaching:
+    def test_get_cache_dir(
+        self,
+        tmp_path: Path,
+        monkeypatch: pytest.MonkeyPatch,
+    ) -> None:
+        # Create multiple cache directories
+        workspace1 = tmp_path / "ws1"
+        workspace1.mkdir()
+        workspace2 = tmp_path / "ws2"
+        workspace2.mkdir()
+
+        # Force user_cache_dir to use the temporary directory for easier assertions
+        patch_user_cache_dir = patch(
+            target="black.cache.user_cache_dir",
+            autospec=True,
+            return_value=str(workspace1),
+        )
+
+        # If BLACK_CACHE_DIR is not set, use user_cache_dir
+        monkeypatch.delenv("BLACK_CACHE_DIR", raising=False)
+        with patch_user_cache_dir:
+            assert get_cache_dir() == workspace1
+
+        # If it is set, use the path provided in the env var.
+        monkeypatch.setenv("BLACK_CACHE_DIR", str(workspace2))
+        assert get_cache_dir() == workspace2
+
     def test_cache_broken_file(self) -> None:
         mode = DEFAULT_MODE
         with cache_dir() as workspace: