diff --git a/docs/changelog/604.bugfix.rst b/docs/changelog/604.bugfix.rst new file mode 100644 index 00000000..2e2acdea --- /dev/null +++ b/docs/changelog/604.bugfix.rst @@ -0,0 +1 @@ +Keep ``os.pathsep`` in ``site_applications_path`` under ``multipath=True`` on platforms with one applications directory. diff --git a/src/platformdirs/api.py b/src/platformdirs/api.py index 454ac994..632a6738 100644 --- a/src/platformdirs/api.py +++ b/src/platformdirs/api.py @@ -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: @@ -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 @@ -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 @@ -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: @@ -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 @@ -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: @@ -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: @@ -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: @@ -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: @@ -471,7 +469,7 @@ 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.""" @@ -479,7 +477,7 @@ def iter_data_dirs(self) -> Iterator[str]: 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.""" @@ -487,7 +485,7 @@ def iter_cache_dirs(self) -> Iterator[str]: 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.""" diff --git a/src/platformdirs/macos.py b/src/platformdirs/macos.py index 6cb9371a..f6b40d22 100644 --- a/src/platformdirs/macos.py +++ b/src/platformdirs/macos.py @@ -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. @@ -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`.""" @@ -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 `_, the directory will be under the Homebrew prefix, e.g. ``$homebrew_prefix/var/cache/$appname/$version``. If `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``.""" @@ -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``.""" @@ -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. diff --git a/src/platformdirs/testing.py b/src/platformdirs/testing.py index 929f6c32..a603822a 100644 --- a/src/platformdirs/testing.py +++ b/src/platformdirs/testing.py @@ -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)} @@ -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", ] diff --git a/src/platformdirs/unix.py b/src/platformdirs/unix.py index 31008e59..e7e78b2a 100644 --- a/src/platformdirs/unix.py +++ b/src/platformdirs/unix.py @@ -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] @@ -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. diff --git a/tests/test_android.py b/tests/test_android.py index d0a5544e..9d0781aa 100644 --- a/tests/test_android.py +++ b/tests/test_android.py @@ -1,5 +1,6 @@ from __future__ import annotations +import os import sys from pathlib import Path from typing import TYPE_CHECKING, Any, Final @@ -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"]) diff --git a/tests/test_macos.py b/tests/test_macos.py index 46195631..2e7dc0de 100644 --- a/tests/test_macos.py +++ b/tests/test_macos.py @@ -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() diff --git a/tests/test_windows.py b/tests/test_windows.py index f66d7645..98890fe2 100644 --- a/tests/test_windows.py +++ b/tests/test_windows.py @@ -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"), [