Add support for Homebrew-installed software (#232)
* Add support for Homebrew-installed software
* mock os.pathsep when running macOS tests on non-macOS systems
* respond to review comments
* respond to review comments
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 @@
"""
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 @@
@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 <https://brew.sh>`_, the directory
+ will be under the Homebrew prefix, e.g. ``/opt/homebrew/share/$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.
+ ``/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 @@
@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 <https://brew.sh>`_, the directory
+ will be under the Homebrew prefix, e.g. ``/opt/homebrew/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.
+ ``/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 @@
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