Skip to content

Commit 0e85d6f

Browse files
jackwalkerlabspre-commit-ci[bot]gaborbernat
authored
fix: retain Homebrew site directories inside virtual environments (#543)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Bernát Gábor <gaborjbernat@gmail.com>
1 parent b86a670 commit 0e85d6f

5 files changed

Lines changed: 74 additions & 10 deletions

File tree

docs/changelog/543.bugfix.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Use the base Python installation to locate Homebrew site directories on macOS, preserving shared data, config, cache and
2+
state paths inside virtual environments.

docs/platforms.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,9 @@ On all other platforms, this aliases ``user_config_dir``.
569569

570570
These are system-wide (and, generally, read-only) directories.
571571

572+
On macOS, the Homebrew base Python installation determines shared data, config, cache and state locations. Activating a
573+
virtual environment preserves those locations.
574+
572575
``site_data_dir``
573576
=================
574577

src/platformdirs/macos.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ def _base_user_app_support_dir(self) -> str:
2929
return self._append_app_name_and_version(os.path.expanduser("~/Library/Application Support")) # ruff:ignore[os-path-expanduser]
3030

3131
def _base_site_dirs(self) -> list[str]:
32-
is_homebrew = "/opt/python" in sys.prefix
33-
homebrew_prefix = sys.prefix.split("/opt/python")[0] if is_homebrew else ""
32+
is_homebrew = "/opt/python" in sys.base_prefix
33+
homebrew_prefix = sys.base_prefix.split("/opt/python")[0] if is_homebrew else ""
3434
path_list = [self._append_app_name_and_version(f"{homebrew_prefix}/share")] if is_homebrew else []
3535
path_list.append(self._append_app_name_and_version("/Library/Application Support"))
3636
return path_list
@@ -70,8 +70,8 @@ def user_cache_dir(self) -> str:
7070

7171
@property
7272
def _site_cache_dirs(self) -> list[str]:
73-
is_homebrew = "/opt/python" in sys.prefix
74-
homebrew_prefix = sys.prefix.split("/opt/python")[0] if is_homebrew else ""
73+
is_homebrew = "/opt/python" in sys.base_prefix
74+
homebrew_prefix = sys.base_prefix.split("/opt/python")[0] if is_homebrew else ""
7575
path_list = [self._append_app_name_and_version(f"{homebrew_prefix}/var/cache")] if is_homebrew else []
7676
path_list.append(self._append_app_name_and_version("/Library/Caches"))
7777
return path_list

tests/test_comp_with_appdirs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ def test_has_all_properties() -> None:
4949
"app_name_author_version",
5050
],
5151
)
52-
def test_compatibility(params: dict[str, Any], func: str) -> None:
52+
def test_compatibility(params: dict[str, Any], func: str, monkeypatch: pytest.MonkeyPatch) -> None:
53+
# Compare system defaults here; appdirs has no Homebrew-specific locations.
54+
monkeypatch.setattr(sys, "base_prefix", "/usr")
5355
# Only test functions that are part of appdirs
5456
if getattr(appdirs, func, None) is None:
5557
pytest.skip(f"`{func}` does not exist in `appdirs`")

tests/test_macos.py

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import os
44
import sys
55
from pathlib import Path
6-
from typing import TYPE_CHECKING, Any
6+
from typing import TYPE_CHECKING, Any, Final
77

88
import pytest
99

@@ -50,15 +50,15 @@ def home() -> str:
5050

5151
@pytest.fixture
5252
def _homebrew_py_prefix(mocker: MockerFixture) -> None:
53-
mocker.patch("sys.prefix", "/opt/homebrew/opt/python@3.13/Frameworks/Python.framework/Versions/3.13")
53+
mocker.patch("sys.base_prefix", "/opt/homebrew/opt/python@3.13/Frameworks/Python.framework/Versions/3.13")
5454

5555

5656
@pytest.fixture
5757
def _builtin_py_prefix(mocker: MockerFixture) -> None:
58-
"""Keep ``sys.prefix`` off the ``/opt/python`` Homebrew heuristic so directories use the system defaults."""
58+
"""Keep ``sys.base_prefix`` off the ``/opt/python`` Homebrew heuristic so directories use the system defaults."""
5959
py_version = sys.version_info
6060
mocker.patch(
61-
"sys.prefix",
61+
"sys.base_prefix",
6262
"/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework"
6363
f"/Versions/{py_version.major}.{py_version.minor}",
6464
)
@@ -153,7 +153,7 @@ def test_macos_homebrew(
153153
},
154154
]
155155
for prefix in test_data:
156-
mocker.patch("sys.prefix", prefix["sys_prefix"])
156+
mocker.patch("sys.base_prefix", prefix["sys_prefix"])
157157

158158
result = getattr(MacOS(multipath=multipath, **params), site_func)
159159

@@ -484,3 +484,60 @@ def test_macos_iter_runtime_dirs_no_duplicate(home: str) -> None:
484484
# site_runtime_dir is defined as user_runtime_dir.
485485
expected = os.path.join(f"{home}/Library/Caches/TemporaryItems", "foo") # ruff:ignore[os-path-join]
486486
assert list(MacOS(appname="foo").iter_runtime_dirs()) == [expected]
487+
488+
489+
@pytest.mark.usefixtures("_clear_xdg_env")
490+
@pytest.mark.parametrize(
491+
"homebrew_prefix",
492+
[
493+
pytest.param("/opt/homebrew", id="apple-silicon"),
494+
pytest.param("/usr/local", id="intel"),
495+
pytest.param("/custom/brew", id="custom-prefix"),
496+
],
497+
)
498+
@pytest.mark.parametrize("multipath", [pytest.param(True, id="multipath"), pytest.param(False, id="singlepath")])
499+
@pytest.mark.parametrize(
500+
"prop",
501+
[
502+
"site_data_dir",
503+
"site_config_dir",
504+
"site_cache_dir",
505+
"site_state_dir",
506+
"site_data_path",
507+
"site_config_path",
508+
"site_cache_path",
509+
"site_state_path",
510+
],
511+
)
512+
def test_homebrew_virtual_environment(
513+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, homebrew_prefix: str, prop: str, multipath: bool
514+
) -> None:
515+
monkeypatch.setattr(sys, "prefix", str(tmp_path / ".venv"))
516+
monkeypatch.setattr(
517+
sys, "base_prefix", f"{homebrew_prefix}/opt/python@3.13/Frameworks/Python.framework/Versions/3.13"
518+
)
519+
suffix: Final = "var/cache" if "cache" in prop else "share"
520+
expected: str | Path = f"{homebrew_prefix}/{suffix}{os.sep}Example{os.sep}1.0"
521+
if prop.endswith("_path"):
522+
expected = Path(expected)
523+
elif multipath and prop != "site_state_dir":
524+
fallback: Final = "Caches" if "cache" in prop else "Application Support"
525+
expected += f":/Library/{fallback}{os.sep}Example{os.sep}1.0"
526+
assert getattr(MacOS(appname="Example", version="1.0", multipath=multipath), prop) == expected
527+
528+
529+
@pytest.mark.usefixtures("_clear_xdg_env", "_builtin_py_prefix")
530+
@pytest.mark.parametrize(
531+
("prop", "expected"),
532+
[
533+
pytest.param("site_data_dir", "/Library/Application Support", id="data"),
534+
pytest.param("site_config_dir", "/Library/Application Support", id="config"),
535+
pytest.param("site_cache_dir", "/Library/Caches", id="cache"),
536+
pytest.param("site_state_dir", "/Library/Application Support", id="state"),
537+
],
538+
)
539+
def test_non_homebrew_base_ignores_virtual_environment_name(
540+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, prop: str, expected: str
541+
) -> None:
542+
monkeypatch.setattr(sys, "prefix", (tmp_path / "opt/python/.venv").as_posix())
543+
assert getattr(MacOS(), prop) == expected

0 commit comments

Comments
 (0)