diff --git a/src/platformdirs/api.py b/src/platformdirs/api.py index 1315799..aa9ce7b 100644 --- a/src/platformdirs/api.py +++ b/src/platformdirs/api.py @@ -58,8 +58,8 @@ def __init__( # noqa: PLR0913 """ self.multipath = multipath """ - An optional parameter only applicable to Unix/Linux which indicates that the entire list of data dirs should be - returned. By default, the first item would only be returned. + An optional parameter which indicates that the entire list of data dirs should be returned. + By default, the first item would only be returned. """ self.opinion = opinion #: A flag to indicating to use opinionated values. self.ensure_exists = ensure_exists diff --git a/src/platformdirs/macos.py b/src/platformdirs/macos.py index 7800fe1..c01ce16 100644 --- a/src/platformdirs/macos.py +++ b/src/platformdirs/macos.py @@ -2,6 +2,7 @@ from __future__ import annotations import os.path +import sys from .api import PlatformDirsABC @@ -22,8 +23,20 @@ def user_data_dir(self) -> str: @property def site_data_dir(self) -> str: - """:return: data directory shared by users, e.g. ``/Library/Application Support/$appname/$version``""" - return self._append_app_name_and_version("/Library/Application Support") + """ + :return: data directory shared by users, e.g. ``/Library/Application Support/$appname/$version``. + If we're using a Python binary managed by `Homebrew `_, the directory + will be under the Homebrew prefix, e.g. ``/opt/homebrew/share/$appname/$version``. + If `multipath ` is enabled and we're in Homebrew, + the response is a multi-path string separated by ":", e.g. + ``/opt/homebrew/share/$appname/$version:/Library/Application Support/$appname/$version`` + """ + is_homebrew = sys.prefix.startswith("/opt/homebrew") + path_list = [self._append_app_name_and_version("/opt/homebrew/share")] if is_homebrew else [] + path_list.append(self._append_app_name_and_version("/Library/Application Support")) + if self.multipath: + return os.pathsep.join(path_list) + return path_list[0] @property def user_config_dir(self) -> str: @@ -42,8 +55,20 @@ def user_cache_dir(self) -> str: @property def site_cache_dir(self) -> str: - """:return: cache directory shared by users, e.g. ``/Library/Caches/$appname/$version``""" - return self._append_app_name_and_version("/Library/Caches") + """ + :return: 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. ``/opt/homebrew/var/cache/$appname/$version``. + If `multipath ` is enabled and we're in Homebrew, + the response is a multi-path string separated by ":", e.g. + ``/opt/homebrew/var/cache/$appname/$version:/Library/Caches/$appname/$version`` + """ + is_homebrew = sys.prefix.startswith("/opt/homebrew") + path_list = [self._append_app_name_and_version("/opt/homebrew/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] @property def user_state_dir(self) -> str: diff --git a/tests/test_macos.py b/tests/test_macos.py index decbec5..551ec0f 100644 --- a/tests/test_macos.py +++ b/tests/test_macos.py @@ -1,13 +1,27 @@ from __future__ import annotations import os +import sys from pathlib import Path -from typing import Any +from typing import TYPE_CHECKING, Any import pytest from platformdirs.macos import MacOS +if TYPE_CHECKING: + from pytest_mock import MockerFixture + + +@pytest.fixture(autouse=True) +def _fix_os_pathsep(mocker: MockerFixture) -> None: + """ + If we're not actually running on macOS, set `os.pathsep` to what it should be on macOS. + """ + if sys.platform != "darwin": # pragma: darwin no cover + mocker.patch("os.pathsep", ":") + mocker.patch("os.path.pathsep", ":") + @pytest.mark.parametrize( "params", @@ -17,7 +31,15 @@ pytest.param({"appname": "foo", "version": "v1.0"}, id="app_name_version"), ], ) -def test_macos(params: dict[str, Any], func: str) -> None: +def test_macos(mocker: MockerFixture, params: dict[str, Any], func: str) -> None: + # Make sure we are not in Homebrew + py_version = sys.version_info + builtin_py_prefix = ( + "/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework" + f"/Versions/{py_version.major}.{py_version.minor}" + ) + mocker.patch("sys.prefix", builtin_py_prefix) + result = getattr(MacOS(**params), func) home = str(Path("~").expanduser()) @@ -45,3 +67,45 @@ def test_macos(params: dict[str, Any], func: str) -> None: expected = expected_map[func] assert result == expected + + +@pytest.mark.parametrize( + "params", + [ + pytest.param({}, id="no_args"), + pytest.param({"appname": "foo"}, id="app_name"), + pytest.param({"appname": "foo", "version": "v1.0"}, id="app_name_version"), + ], +) +@pytest.mark.parametrize( + "site_func", + [ + "site_data_dir", + "site_config_dir", + "site_cache_dir", + "site_runtime_dir", + ], +) +@pytest.mark.parametrize("multipath", [pytest.param(True, id="multipath"), pytest.param(False, id="singlepath")]) +def test_macos_homebrew(mocker: MockerFixture, params: dict[str, Any], multipath: bool, site_func: str) -> None: + mocker.patch("sys.prefix", "/opt/homebrew/opt/python") + + result = getattr(MacOS(multipath=multipath, **params), site_func) + + home = str(Path("~").expanduser()) + suffix_elements = tuple(params[i] for i in ("appname", "version") if i in params) + suffix = os.sep.join(("", *suffix_elements)) if suffix_elements else "" # noqa: PTH118 + + expected_map = { + "site_data_dir": f"/opt/homebrew/share{suffix}", + "site_config_dir": f"/opt/homebrew/share{suffix}", + "site_cache_dir": f"/opt/homebrew/var/cache{suffix}", + "site_runtime_dir": f"{home}/Library/Caches/TemporaryItems{suffix}", + } + if multipath: + expected_map["site_data_dir"] += f":/Library/Application Support{suffix}" + expected_map["site_config_dir"] += f":/Library/Application Support{suffix}" + expected_map["site_cache_dir"] += f":/Library/Caches{suffix}" + expected = expected_map[site_func] + + assert result == expected