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
7 changes: 4 additions & 3 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@

.. towncrier release notes start

********************
*********************
4.11.1 (2026-08-07)
********************
*********************

- Fix :func:`~platformdirs.user_desktop_dir` on Windows builds without ``ctypes``. ``CSIDL_DESKTOPDIRECTORY`` appeared
only in the ctypes lookup table, so the registry and environment variable resolvers raised ``ValueError`` for it. :pr:`519`
only in the ctypes lookup table, so the registry and environment variable resolvers raised ``ValueError`` for it.
:pr:`519`

*********************
4.11.0 (2026-07-21)
Expand Down
5 changes: 5 additions & 0 deletions docs/changelog/520.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Stop :meth:`~platformdirs.PlatformDirs.iter_cache_dirs`, :meth:`~platformdirs.PlatformDirs.iter_state_dirs`,
:meth:`~platformdirs.PlatformDirs.iter_log_dirs` and :meth:`~platformdirs.PlatformDirs.iter_runtime_dirs` yielding the
same directory twice on Unix when ``use_site_for_root`` is active - :pr:`469` fixed this for the config and data
iterators only. On macOS, :meth:`~platformdirs.PlatformDirs.iter_cache_dirs` now yields the Homebrew and
``/Library/Caches`` entries separately rather than one ``os.pathsep``-joined string when ``multipath`` is set.
18 changes: 13 additions & 5 deletions src/platformdirs/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,18 @@ def user_cache_dir(self) -> str:
return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches")) # ruff:ignore[os-path-expanduser]

@property
def site_cache_dir(self) -> str:
"""Cache directory shared by users, e.g. ``/Library/Caches/$appname/$version``. If we're using a Python binary managed by `Homebrew <https://brew.sh>`_, the directory will be under the Homebrew prefix, e.g. ``$homebrew_prefix/var/cache/$appname/$version``. If `multipath <platformdirs.api.PlatformDirsABC.multipath>` is enabled, and we're in Homebrew, the response is a multi-path string separated by ":", e.g. ``$homebrew_prefix/var/cache/$appname/$version:/Library/Caches/$appname/$version``."""
def _site_cache_dirs(self) -> list[str]:
is_homebrew = "/opt/python" in sys.prefix
homebrew_prefix = sys.prefix.split("/opt/python")[0] if is_homebrew else ""
path_list = [self._append_app_name_and_version(f"{homebrew_prefix}/var/cache")] if is_homebrew else []
path_list.append(self._append_app_name_and_version("/Library/Caches"))
if self.multipath:
return os.pathsep.join(path_list)
return path_list[0]
return path_list

@property
def site_cache_dir(self) -> str:
"""Cache directory shared by users, e.g. ``/Library/Caches/$appname/$version``. If we're using a Python binary managed by `Homebrew <https://brew.sh>`_, the directory will be under the Homebrew prefix, e.g. ``$homebrew_prefix/var/cache/$appname/$version``. If `multipath <platformdirs.api.PlatformDirsABC.multipath>` is enabled, and we're in Homebrew, the response is a multi-path string separated by ":", e.g. ``$homebrew_prefix/var/cache/$appname/$version:/Library/Caches/$appname/$version``."""
dirs = self._site_cache_dirs
return os.pathsep.join(dirs) if self.multipath else dirs[0]

@property
def site_cache_path(self) -> Path:
Expand Down Expand Up @@ -204,6 +207,11 @@ def iter_data_dirs(self) -> Iterator[str]:
yield self.user_data_dir
yield from self._site_data_dirs

def iter_cache_dirs(self) -> Iterator[str]:
""":yield: all user and site cache directories."""
yield self.user_cache_dir
yield from self._site_cache_dirs


class MacOS(XDGMixin, _MacOSDefaults):
"""Platform directories for the macOS operating system.
Expand Down
24 changes: 24 additions & 0 deletions src/platformdirs/unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,30 @@ def iter_data_dirs(self) -> Iterator[str]:
yield self.user_data_dir
yield from self._site_data_dirs

def iter_cache_dirs(self) -> Iterator[str]:
""":yield: all user and site cache directories."""
if not self._use_site:
yield self.user_cache_dir
yield self.site_cache_dir

def iter_state_dirs(self) -> Iterator[str]:
""":yield: all user and site state directories."""
if not self._use_site:
yield self.user_state_dir
yield self.site_state_dir

def iter_log_dirs(self) -> Iterator[str]:
""":yield: all user and site log directories."""
if not self._use_site:
yield self.user_log_dir
yield self.site_log_dir

def iter_runtime_dirs(self) -> Iterator[str]:
""":yield: all user and site runtime directories."""
if not self._use_site:
yield self.user_runtime_dir
yield self.site_runtime_dir


class Unix(XDGMixin, _UnixDefaults):
"""On Unix/Linux, we follow the `XDG Basedir Spec <https://specifications.freedesktop.org/basedir/latest/>`_.
Expand Down
17 changes: 17 additions & 0 deletions tests/test_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,23 @@ def test_iter_config_dirs_homebrew(mocker: MockerFixture) -> None:
assert dirs == [f"{home}/Library/Application Support", "/opt/homebrew/share", "/Library/Application Support"]


@pytest.mark.usefixtures("_clear_xdg_env")
@pytest.mark.parametrize("multipath", [True, False])
def test_iter_cache_dirs_homebrew(mocker: MockerFixture, multipath: bool) -> None:
mocker.patch("sys.prefix", "/opt/homebrew/opt/python@3.13/Frameworks/Python.framework/Versions/3.13")
dirs = list(MacOS(multipath=multipath).iter_cache_dirs())
home = str(Path("~").expanduser())
assert dirs == [f"{home}/Library/Caches", "/opt/homebrew/var/cache", "/Library/Caches"]


@pytest.mark.usefixtures("_clear_xdg_env")
def test_iter_cache_paths_homebrew_multipath(mocker: MockerFixture) -> None:
mocker.patch("sys.prefix", "/opt/homebrew/opt/python@3.13/Frameworks/Python.framework/Versions/3.13")
paths = list(MacOS(multipath=True).iter_cache_paths())
home = str(Path("~").expanduser())
assert paths == [Path(f"{home}/Library/Caches"), Path("/opt/homebrew/var/cache"), Path("/Library/Caches")]


@pytest.mark.usefixtures("_clear_xdg_env")
def test_iter_data_dirs_no_homebrew(mocker: MockerFixture) -> None:
py_version = sys.version_info
Expand Down
43 changes: 43 additions & 0 deletions tests/test_unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,46 @@ def test_use_site_iter_dirs_no_duplicates(
monkeypatch.setenv(xdg_var, "/custom/xdg/path")
result = func(Unix(appname="foo", use_site_for_root=True))
assert list(result) == [os.path.join("/custom/xdg/path", "foo")] # ruff:ignore[os-path-join]


_SINGLE_SITE_ITER_CASES = [
(Unix.iter_cache_dirs, os.path.join("/var/cache", "foo")), # ruff:ignore[os-path-join]
(Unix.iter_state_dirs, os.path.join("/var/lib", "foo")), # ruff:ignore[os-path-join]
(Unix.iter_log_dirs, os.path.join("/var/log", "foo")), # ruff:ignore[os-path-join]
(
Unix.iter_runtime_dirs,
os.path.join( # ruff:ignore[os-path-join]
"/var/run" if sys.platform.startswith(("freebsd", "openbsd", "netbsd")) else "/run",
"foo",
),
),
]


@pytest.mark.parametrize(("func", "expected"), _SINGLE_SITE_ITER_CASES)
def test_use_site_iter_dirs_no_duplicates_single_site_dir(
mocker: MockerFixture,
monkeypatch: pytest.MonkeyPatch,
func: Callable[[Unix], Iterator[str]],
expected: str,
) -> None:
mocker.patch("platformdirs.unix.getuid", return_value=0)
monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
result = func(Unix(appname="foo", use_site_for_root=True))
assert list(result) == [expected]


@pytest.mark.parametrize(("func", "expected"), _SINGLE_SITE_ITER_CASES)
def test_iter_dirs_as_non_root_keeps_user_dir(
mocker: MockerFixture,
monkeypatch: pytest.MonkeyPatch,
func: Callable[[Unix], Iterator[str]],
expected: str,
) -> None:
mocker.patch("platformdirs.unix.getuid", return_value=1000)
monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
mocker.patch("os.access", return_value=True)
result = list(func(Unix(appname="foo", use_site_for_root=True)))
assert len(result) == 2
assert result[0] != expected
assert result[1] == expected