Skip to content

Commit 682cdcb

Browse files
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
1 parent d63b824 commit 682cdcb

File tree

3 files changed

+97
-8
lines changed

3 files changed

+97
-8
lines changed

src/platformdirs/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ def __init__( # noqa: PLR0913
5858
"""
5959
self.multipath = multipath
6060
"""
61-
An optional parameter only applicable to Unix/Linux which indicates that the entire list of data dirs should be
62-
returned. By default, the first item would only be returned.
61+
An optional parameter which indicates that the entire list of data dirs should be returned.
62+
By default, the first item would only be returned.
6363
"""
6464
self.opinion = opinion #: A flag to indicating to use opinionated values.
6565
self.ensure_exists = ensure_exists

src/platformdirs/macos.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from __future__ import annotations
33

44
import os.path
5+
import sys
56

67
from .api import PlatformDirsABC
78

@@ -22,8 +23,20 @@ def user_data_dir(self) -> str:
2223

2324
@property
2425
def site_data_dir(self) -> str:
25-
""":return: data directory shared by users, e.g. ``/Library/Application Support/$appname/$version``"""
26-
return self._append_app_name_and_version("/Library/Application Support")
26+
"""
27+
:return: data directory shared by users, e.g. ``/Library/Application Support/$appname/$version``.
28+
If we're using a Python binary managed by `Homebrew <https://brew.sh>`_, the directory
29+
will be under the Homebrew prefix, e.g. ``/opt/homebrew/share/$appname/$version``.
30+
If `multipath <platformdirs.api.PlatformDirsABC.multipath>` is enabled and we're in Homebrew,
31+
the response is a multi-path string separated by ":", e.g.
32+
``/opt/homebrew/share/$appname/$version:/Library/Application Support/$appname/$version``
33+
"""
34+
is_homebrew = sys.prefix.startswith("/opt/homebrew")
35+
path_list = [self._append_app_name_and_version("/opt/homebrew/share")] if is_homebrew else []
36+
path_list.append(self._append_app_name_and_version("/Library/Application Support"))
37+
if self.multipath:
38+
return os.pathsep.join(path_list)
39+
return path_list[0]
2740

2841
@property
2942
def user_config_dir(self) -> str:
@@ -42,8 +55,20 @@ def user_cache_dir(self) -> str:
4255

4356
@property
4457
def site_cache_dir(self) -> str:
45-
""":return: cache directory shared by users, e.g. ``/Library/Caches/$appname/$version``"""
46-
return self._append_app_name_and_version("/Library/Caches")
58+
"""
59+
:return: cache directory shared by users, e.g. ``/Library/Caches/$appname/$version``.
60+
If we're using a Python binary managed by `Homebrew <https://brew.sh>`_, the directory
61+
will be under the Homebrew prefix, e.g. ``/opt/homebrew/var/cache/$appname/$version``.
62+
If `multipath <platformdirs.api.PlatformDirsABC.multipath>` is enabled and we're in Homebrew,
63+
the response is a multi-path string separated by ":", e.g.
64+
``/opt/homebrew/var/cache/$appname/$version:/Library/Caches/$appname/$version``
65+
"""
66+
is_homebrew = sys.prefix.startswith("/opt/homebrew")
67+
path_list = [self._append_app_name_and_version("/opt/homebrew/var/cache")] if is_homebrew else []
68+
path_list.append(self._append_app_name_and_version("/Library/Caches"))
69+
if self.multipath:
70+
return os.pathsep.join(path_list)
71+
return path_list[0]
4772

4873
@property
4974
def user_state_dir(self) -> str:

tests/test_macos.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,27 @@
11
from __future__ import annotations
22

33
import os
4+
import sys
45
from pathlib import Path
5-
from typing import Any
6+
from typing import TYPE_CHECKING, Any
67

78
import pytest
89

910
from platformdirs.macos import MacOS
1011

12+
if TYPE_CHECKING:
13+
from pytest_mock import MockerFixture
14+
15+
16+
@pytest.fixture(autouse=True)
17+
def _fix_os_pathsep(mocker: MockerFixture) -> None:
18+
"""
19+
If we're not actually running on macOS, set `os.pathsep` to what it should be on macOS.
20+
"""
21+
if sys.platform != "darwin": # pragma: darwin no cover
22+
mocker.patch("os.pathsep", ":")
23+
mocker.patch("os.path.pathsep", ":")
24+
1125

1226
@pytest.mark.parametrize(
1327
"params",
@@ -17,7 +31,15 @@
1731
pytest.param({"appname": "foo", "version": "v1.0"}, id="app_name_version"),
1832
],
1933
)
20-
def test_macos(params: dict[str, Any], func: str) -> None:
34+
def test_macos(mocker: MockerFixture, params: dict[str, Any], func: str) -> None:
35+
# Make sure we are not in Homebrew
36+
py_version = sys.version_info
37+
builtin_py_prefix = (
38+
"/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework"
39+
f"/Versions/{py_version.major}.{py_version.minor}"
40+
)
41+
mocker.patch("sys.prefix", builtin_py_prefix)
42+
2143
result = getattr(MacOS(**params), func)
2244

2345
home = str(Path("~").expanduser())
@@ -45,3 +67,45 @@ def test_macos(params: dict[str, Any], func: str) -> None:
4567
expected = expected_map[func]
4668

4769
assert result == expected
70+
71+
72+
@pytest.mark.parametrize(
73+
"params",
74+
[
75+
pytest.param({}, id="no_args"),
76+
pytest.param({"appname": "foo"}, id="app_name"),
77+
pytest.param({"appname": "foo", "version": "v1.0"}, id="app_name_version"),
78+
],
79+
)
80+
@pytest.mark.parametrize(
81+
"site_func",
82+
[
83+
"site_data_dir",
84+
"site_config_dir",
85+
"site_cache_dir",
86+
"site_runtime_dir",
87+
],
88+
)
89+
@pytest.mark.parametrize("multipath", [pytest.param(True, id="multipath"), pytest.param(False, id="singlepath")])
90+
def test_macos_homebrew(mocker: MockerFixture, params: dict[str, Any], multipath: bool, site_func: str) -> None:
91+
mocker.patch("sys.prefix", "/opt/homebrew/opt/python")
92+
93+
result = getattr(MacOS(multipath=multipath, **params), site_func)
94+
95+
home = str(Path("~").expanduser())
96+
suffix_elements = tuple(params[i] for i in ("appname", "version") if i in params)
97+
suffix = os.sep.join(("", *suffix_elements)) if suffix_elements else "" # noqa: PTH118
98+
99+
expected_map = {
100+
"site_data_dir": f"/opt/homebrew/share{suffix}",
101+
"site_config_dir": f"/opt/homebrew/share{suffix}",
102+
"site_cache_dir": f"/opt/homebrew/var/cache{suffix}",
103+
"site_runtime_dir": f"{home}/Library/Caches/TemporaryItems{suffix}",
104+
}
105+
if multipath:
106+
expected_map["site_data_dir"] += f":/Library/Application Support{suffix}"
107+
expected_map["site_config_dir"] += f":/Library/Application Support{suffix}"
108+
expected_map["site_cache_dir"] += f":/Library/Caches{suffix}"
109+
expected = expected_map[site_func]
110+
111+
assert result == expected

0 commit comments

Comments
 (0)