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
1 change: 1 addition & 0 deletions docs/changelog/604.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Keep ``os.pathsep`` in ``site_applications_path`` under ``multipath=True`` on platforms with one applications directory.
36 changes: 17 additions & 19 deletions src/platformdirs/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,6 @@ def _first_site_dir_as_path(self, dirs: list[str]) -> Path:
self._optionally_create_directory(path, private=False)
return Path(path)

def _first_item_as_path_if_multipath(self, directory: str) -> Path:
if self.multipath:
# If multipath is True, the first path is returned.
directory = directory.partition(os.pathsep)[0]
return Path(directory)

@property
@abstractmethod
def user_data_dir(self) -> str:
Expand All @@ -195,7 +189,7 @@ def site_data_dir(self) -> str:

@property
def _site_data_dirs(self) -> list[str]:
raise NotImplementedError
return [self.site_data_dir]

@property
@abstractmethod
Expand All @@ -209,7 +203,7 @@ def site_config_dir(self) -> str:

@property
def _site_config_dirs(self) -> list[str]:
raise NotImplementedError
return [self.site_config_dir]

@property
@abstractmethod
Expand All @@ -221,6 +215,10 @@ def user_cache_dir(self) -> str:
def site_cache_dir(self) -> str:
"""Cache directory shared by users."""

@property
def _site_cache_dirs(self) -> list[str]:
return [self.site_cache_dir]

@property
@abstractmethod
def user_state_dir(self) -> str:
Expand Down Expand Up @@ -318,7 +316,7 @@ def site_applications_dir(self) -> str:

@property
def _site_applications_dirs(self) -> list[str]:
raise NotImplementedError
return [self.site_applications_dir]

@property
@abstractmethod
Expand All @@ -337,8 +335,8 @@ def user_data_path(self) -> Path:

@property
def site_data_path(self) -> Path:
"""Data path shared by users."""
return Path(self.site_data_dir)
"""Data path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
return self._first_site_dir_as_path(self._site_data_dirs)

@property
def user_config_path(self) -> Path:
Expand All @@ -347,8 +345,8 @@ def user_config_path(self) -> Path:

@property
def site_config_path(self) -> Path:
"""Config path shared by users."""
return Path(self.site_config_dir)
"""Config path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
return self._first_site_dir_as_path(self._site_config_dirs)

@property
def user_cache_path(self) -> Path:
Expand All @@ -357,8 +355,8 @@ def user_cache_path(self) -> Path:

@property
def site_cache_path(self) -> Path:
"""Cache path shared by users."""
return Path(self.site_cache_dir)
"""Cache path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
return self._first_site_dir_as_path(self._site_cache_dirs)

@property
def user_state_path(self) -> Path:
Expand Down Expand Up @@ -453,7 +451,7 @@ def user_applications_path(self) -> Path:
@property
def site_applications_path(self) -> Path:
"""Applications path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
return self._first_item_as_path_if_multipath(self.site_applications_dir)
return Path(self._site_applications_dirs[0])

@property
def user_runtime_path(self) -> Path:
Expand All @@ -471,23 +469,23 @@ def iter_config_dirs(self) -> Iterator[str]:

def _iter_config_dirs(self) -> Iterator[str]:
yield self.user_config_dir
yield self.site_config_dir
yield from self._create_as_yielded(self._site_config_dirs)

def iter_data_dirs(self) -> Iterator[str]:
""":yield: all user and site data directories."""
yield from _unique(self._iter_data_dirs())

def _iter_data_dirs(self) -> Iterator[str]:
yield self.user_data_dir
yield self.site_data_dir
yield from self._create_as_yielded(self._site_data_dirs)

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

def _iter_cache_dirs(self) -> Iterator[str]:
yield self.user_cache_dir
yield self.site_cache_dir
yield from self._create_as_yielded(self._site_cache_dirs)

def iter_state_dirs(self) -> Iterator[str]:
""":yield: all user and site state directories."""
Expand Down
42 changes: 1 addition & 41 deletions src/platformdirs/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,13 @@

from __future__ import annotations

import os.path
import re
import sys
from typing import TYPE_CHECKING, Final

if TYPE_CHECKING:
from collections.abc import Iterator
from typing import Final

from ._xdg import XDGMixin, _expand_user
from .api import PlatformDirsABC

if TYPE_CHECKING:
from pathlib import Path


class _MacOSDefaults(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
"""Default platform directories for macOS without XDG environment variable overrides.
Expand Down Expand Up @@ -43,16 +36,6 @@ def user_data_dir(self) -> str:
def _site_data_dirs(self) -> list[str]:
return self._base_site_dirs()

@property
def site_data_path(self) -> Path:
"""Data path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
return self._first_site_dir_as_path(self._site_data_dirs)

@property
def site_config_path(self) -> Path:
"""Config path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
return self._first_site_dir_as_path(self._site_config_dirs)

@property
def user_config_dir(self) -> str:
"""Config directory tied to the user, same as `user_data_dir`."""
Expand All @@ -78,11 +61,6 @@ 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``."""
return self._select_site_dirs(self._site_cache_dirs)

@property
def site_cache_path(self) -> Path:
"""Cache path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
return self._first_site_dir_as_path(self._site_cache_dirs)

@property
def user_state_dir(self) -> str:
"""State directory tied to the user, same as `user_data_dir` without ``$XDG_DATA_HOME``, e.g. ``~/Library/Application Support/$appname/$version``."""
Expand Down Expand Up @@ -179,12 +157,6 @@ def user_applications_dir(self) -> str:
def _site_applications_dirs(self) -> list[str]:
return ["/Applications"]

@property
def site_applications_dir(self) -> str:
"""Applications directory shared by users, e.g. ``/Applications``."""
dirs = self._site_applications_dirs
return os.pathsep.join(dirs) if self.multipath else dirs[0]

@property
def user_runtime_dir(self) -> str:
"""Runtime directory tied to the user, e.g. ``~/Library/Caches/TemporaryItems/$appname/$version``."""
Expand All @@ -195,18 +167,6 @@ def site_runtime_dir(self) -> str:
"""Runtime directory shared by users, same as `user_runtime_dir`."""
return self.user_runtime_dir

def _iter_config_dirs(self) -> Iterator[str]:
yield self.user_config_dir
yield from self._create_as_yielded(self._site_config_dirs)

def _iter_data_dirs(self) -> Iterator[str]:
yield self.user_data_dir
yield from self._create_as_yielded(self._site_data_dirs)

def _iter_cache_dirs(self) -> Iterator[str]:
yield self.user_cache_dir
yield from self._create_as_yielded(self._site_cache_dirs)


class MacOS(XDGMixin, _MacOSDefaults):
"""Platform directories for the macOS operating system.
Expand Down
13 changes: 4 additions & 9 deletions src/platformdirs/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ def isolated_dirs(root: str | os.PathLike[str]) -> Iterator[Path]:
)
for name in PlatformDirsABC.__abstractmethods__
}
# The *_path and iter_* accessors of the Unix and macOS site kinds read these lists instead of the *_dir property.
# Unix and macOS build the site lists from search paths; the base versions derive them from the redirected dirs.
patches |= {
f"_site_{kind}_dirs": _redirect_site_list(base / f"site_{kind}") for kind in ("data", "config", "cache")
name: value
for name, value in vars(PlatformDirsABC).items()
if name.startswith("_site_") and name.endswith("_dirs")
}
cls = platformdirs.PlatformDirs
saved = {name: vars(cls)[name] for name in patches if name in vars(cls)}
Expand All @@ -76,13 +78,6 @@ def resolve(self: PlatformDirsABC) -> str:
return property(resolve)


def _redirect_site_list(path: Path) -> property:
def resolve(self: PlatformDirsABC) -> list[str]:
return [self._join_app_name_and_version(str(path))]

return property(resolve)


__all__ = [
"isolated_dirs",
]
16 changes: 0 additions & 16 deletions src/platformdirs/unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,6 @@ def user_applications_dir(self) -> str:
def _site_applications_dirs(self) -> list[str]:
return [os.path.join(p, "applications") for p in ["/usr/local/share", "/usr/share"]] # ruff:ignore[os-path-join]

@property
def site_applications_dir(self) -> str:
"""Applications directory shared by users, e.g. ``/usr/local/share/applications``."""
dirs = self._site_applications_dirs
return os.pathsep.join(dirs) if self.multipath else dirs[0]

def _default_runtime_dir(self) -> str:
if sys.platform.startswith("openbsd"):
path = f"/tmp/run/user/{getuid()}" # ruff:ignore[hardcoded-temp-file]
Expand Down Expand Up @@ -222,16 +216,6 @@ def site_runtime_dir(self) -> str:
path = "/run"
return self._append_app_name_and_version(path, private=False)

@property
def site_data_path(self) -> Path:
"""Data path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
return self._first_site_dir_as_path(self._site_data_dirs)

@property
def site_config_path(self) -> Path:
"""Config path shared by users, returns the first item, even if ``multipath`` is set to ``True``."""
return self._first_site_dir_as_path(self._site_config_dirs)

def _iter_config_dirs(self) -> Iterator[str]:
# Under multipath the user dir is an os.pathsep-joined string that no single site entry matches, so the
# dedupe in iter_config_dirs cannot drop it. Skip it here instead.
Expand Down
8 changes: 8 additions & 0 deletions tests/test_android.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import os
import sys
from pathlib import Path
from typing import TYPE_CHECKING, Any, Final
Expand Down Expand Up @@ -324,6 +325,13 @@ def test_android_applications_path_function_takes_app_arguments(mocker: MockerFi
assert getattr(platformdirs, func)(appname="foo", version="1.0") == Path(_SCOPED_APPLICATIONS_DIR)


@pytest.mark.usefixtures("_example_android_folder")
def test_android_site_applications_path_multipath_keeps_pathsep() -> None:
appname = f"foo{os.pathsep}bar"
expected = Path(f"/data/data/com.example/files/{appname}")
assert Android(appname=appname, multipath=True).site_applications_path == expected


@pytest.mark.skipif(sys.platform == "win32", reason="Windows ignores POSIX mode bits")
@pytest.mark.usefixtures("_umask")
@pytest.mark.parametrize("prop", ["user_log_dir", "user_runtime_dir"])
Expand Down
2 changes: 1 addition & 1 deletion tests/test_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,7 +550,7 @@ def test_macos_site_runtime_path(home: str) -> None:

@pytest.mark.usefixtures("_clear_xdg_env", "_builtin_py_prefix")
def test_macos_ensure_exists_preexisting_dir(mocker: MockerFixture, tmp_path: Path) -> None:
mocker.patch("platformdirs.macos.os.path.expanduser", lambda p: str(tmp_path / p.lstrip("~/")))
mocker.patch("platformdirs._xdg.os.path.expanduser", lambda p: str(tmp_path / p.lstrip("~/")))
dirs = MacOS(appname="foo", ensure_exists=True)
first = dirs.user_data_dir
assert Path(first).exists()
Expand Down
6 changes: 6 additions & 0 deletions tests/test_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,12 @@ def test_windows(params: dict[str, Any], func: str) -> None:
assert result == expected_map[func]


def test_site_applications_path_multipath_keeps_pathsep(mocker: MockerFixture) -> None:
folder = rf"C:\ProgramData\Start{os.pathsep}Menu"
mocker.patch.dict(_WIN_FOLDERS, {"CSIDL_COMMON_PROGRAMS": folder})
assert Windows(multipath=True).site_applications_path == Path(folder)


@pytest.mark.parametrize(
("func", "csidl_name"),
[
Expand Down
Loading