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
50 changes: 50 additions & 0 deletions docs/platforms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,56 @@ Key behaviors:
which syncs across machines in a Windows domain
- **OPINION**: ``user_cache_dir`` appends ``\Cache``, ``user_log_dir`` appends ``\Logs``

Environment variable overrides
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Unlike Linux/macOS where ``XDG_*`` variables are a platform standard, Windows has no built-in
convention for overriding folder locations at the application level. To fill this gap,
``platformdirs`` checks ``PLATFORMDIRS_*`` environment variables before querying the Shell Folder
APIs. This is useful when large data (ML models, package caches) should live on a different drive
without changing the system-wide ``APPDATA`` / ``LOCALAPPDATA`` variables that other applications
rely on.

The override variable name is ``PLATFORMDIRS_`` followed by the CSIDL suffix:

.. list-table::
:widths: 40 60
:header-rows: 1

* - Environment variable
- Overrides
* - ``PLATFORMDIRS_APPDATA``
- Roaming user data (``AppData\Roaming``)
* - ``PLATFORMDIRS_LOCAL_APPDATA``
- Local user data, config, cache, state (``AppData\Local``)
* - ``PLATFORMDIRS_COMMON_APPDATA``
- Site-wide data, config, cache, state (``ProgramData``)
* - ``PLATFORMDIRS_PERSONAL``
- Documents
* - ``PLATFORMDIRS_DOWNLOADS``
- Downloads
* - ``PLATFORMDIRS_MYPICTURES``
- Pictures
* - ``PLATFORMDIRS_MYVIDEO``
- Videos
* - ``PLATFORMDIRS_MYMUSIC``
- Music
* - ``PLATFORMDIRS_DESKTOPDIRECTORY``
- Desktop

Example — redirect cache to a separate drive:

.. code-block:: python

import os
os.environ["PLATFORMDIRS_LOCAL_APPDATA"] = r"X:\appdata"

import platformdirs
print(platformdirs.user_cache_dir("MyApp", "Acme"))
# X:\appdata\Acme\MyApp\Cache

Empty or whitespace-only values are ignored and the normal resolution applies.

.. note:: **Windows Store Python (MSIX)**

Python installed from the Microsoft Store runs in a sandboxed (AppContainer) environment.
Expand Down
17 changes: 15 additions & 2 deletions src/platformdirs/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import os
import sys
from functools import lru_cache
from typing import TYPE_CHECKING, Final

from .api import PlatformDirsABC
Expand Down Expand Up @@ -320,7 +319,21 @@ def _pick_get_win_folder() -> Callable[[str], str]:
return get_win_folder_from_registry


get_win_folder = lru_cache(maxsize=None)(_pick_get_win_folder())
_resolve_win_folder = _pick_get_win_folder()


def get_win_folder(csidl_name: str) -> str:
"""
Get a Windows folder path, checking for ``PLATFORMDIRS_*`` environment variable overrides first.

For example, ``CSIDL_LOCAL_APPDATA`` can be overridden by setting ``PLATFORMDIRS_LOCAL_APPDATA``.

"""
env_var = f"PLATFORMDIRS_{csidl_name.removeprefix('CSIDL_')}"
if override := os.environ.get(env_var, "").strip():
return override
return _resolve_win_folder(csidl_name)


__all__ = [
"Windows",
Expand Down
40 changes: 40 additions & 0 deletions tests/test_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
_KF_FLAG_DONT_VERIFY,
_KNOWN_FOLDER_GUIDS,
Windows,
get_win_folder,
get_win_folder_from_env_vars,
get_win_folder_if_csidl_name_not_env_var,
)
Expand Down Expand Up @@ -298,3 +299,42 @@ def test_pick_get_win_folder_ctypes(mocker: MockerFixture) -> None:
finally:
if sys.platform != "win32":
_cleanup_ctypes_mocks()


@pytest.mark.parametrize(
("csidl_name", "env_suffix"),
[
pytest.param("CSIDL_APPDATA", "APPDATA", id="appdata"),
pytest.param("CSIDL_LOCAL_APPDATA", "LOCAL_APPDATA", id="local_appdata"),
pytest.param("CSIDL_COMMON_APPDATA", "COMMON_APPDATA", id="common_appdata"),
pytest.param("CSIDL_PERSONAL", "PERSONAL", id="personal"),
pytest.param("CSIDL_DOWNLOADS", "DOWNLOADS", id="downloads"),
pytest.param("CSIDL_MYPICTURES", "MYPICTURES", id="mypictures"),
pytest.param("CSIDL_MYVIDEO", "MYVIDEO", id="myvideo"),
pytest.param("CSIDL_MYMUSIC", "MYMUSIC", id="mymusic"),
pytest.param("CSIDL_DESKTOPDIRECTORY", "DESKTOPDIRECTORY", id="desktop"),
],
)
def test_get_win_folder_override(monkeypatch: pytest.MonkeyPatch, csidl_name: str, env_suffix: str) -> None:
override_path = r"X:\custom\override"
monkeypatch.setattr("platformdirs.windows._resolve_win_folder", lambda _csidl: _WIN_FOLDERS[_csidl])
monkeypatch.setenv(f"PLATFORMDIRS_{env_suffix}", override_path)
assert get_win_folder(csidl_name) == override_path


def test_get_win_folder_override_whitespace_only_ignored(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("platformdirs.windows._resolve_win_folder", lambda csidl: _WIN_FOLDERS[csidl])
monkeypatch.setenv("PLATFORMDIRS_LOCAL_APPDATA", " ")
assert get_win_folder("CSIDL_LOCAL_APPDATA") == _WIN_FOLDERS["CSIDL_LOCAL_APPDATA"]


def test_get_win_folder_override_not_set_falls_back(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("platformdirs.windows._resolve_win_folder", lambda csidl: _WIN_FOLDERS[csidl])
monkeypatch.delenv("PLATFORMDIRS_LOCAL_APPDATA", raising=False)
assert get_win_folder("CSIDL_LOCAL_APPDATA") == _WIN_FOLDERS["CSIDL_LOCAL_APPDATA"]


def test_get_win_folder_override_strips_whitespace(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("platformdirs.windows._resolve_win_folder", lambda csidl: _WIN_FOLDERS[csidl])
monkeypatch.setenv("PLATFORMDIRS_LOCAL_APPDATA", " X:\\custom ")
assert get_win_folder("CSIDL_LOCAL_APPDATA") == r"X:\custom"