Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/changelog/523.bugfix.2.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
``python -m platformdirs`` now lists :func:`~platformdirs.user_desktop_dir`, which was missing from the properties it
prints.
4 changes: 4 additions & 0 deletions docs/changelog/523.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Stop :func:`~platformdirs.site_data_dir`, :func:`~platformdirs.site_config_dir` and
:func:`~platformdirs.site_applications_dir` raising ``IndexError`` on Unix and macOS when ``$XDG_DATA_DIRS`` or
``$XDG_CONFIG_DIRS`` holds only separators and whitespace, such as ``":"``. These values now fall back to the platform
defaults, and each entry is stripped of surrounding whitespace.
1 change: 1 addition & 0 deletions src/platformdirs/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"user_pictures_dir",
"user_videos_dir",
"user_music_dir",
"user_desktop_dir",
"user_projects_dir",
"user_publicshare_dir",
"user_templates_dir",
Expand Down
17 changes: 11 additions & 6 deletions src/platformdirs/_xdg.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ def user_data_dir(self) -> str:

@property
def _site_data_dirs(self) -> list[str]:
if xdg_dirs := os.environ.get("XDG_DATA_DIRS", "").strip():
return [self._append_app_name_and_version(p) for p in xdg_dirs.split(os.pathsep) if p.strip()]
if xdg_dirs := _xdg_dir_list("XDG_DATA_DIRS"):
return [self._append_app_name_and_version(p) for p in xdg_dirs]
return super()._site_data_dirs

@property
Expand All @@ -38,8 +38,8 @@ def user_config_dir(self) -> str:

@property
def _site_config_dirs(self) -> list[str]:
if xdg_dirs := os.environ.get("XDG_CONFIG_DIRS", "").strip():
return [self._append_app_name_and_version(p) for p in xdg_dirs.split(os.pathsep) if p.strip()]
if xdg_dirs := _xdg_dir_list("XDG_CONFIG_DIRS"):
return [self._append_app_name_and_version(p) for p in xdg_dirs]
return super()._site_config_dirs

@property
Expand Down Expand Up @@ -155,8 +155,8 @@ def user_applications_dir(self) -> str:

@property
def _site_applications_dirs(self) -> list[str]:
if xdg_dirs := os.environ.get("XDG_DATA_DIRS", "").strip():
return [os.path.join(p, "applications") for p in xdg_dirs.split(os.pathsep) if p.strip()] # ruff:ignore[os-path-join]
if xdg_dirs := _xdg_dir_list("XDG_DATA_DIRS"):
return [os.path.join(p, "applications") for p in xdg_dirs] # ruff:ignore[os-path-join]
return super()._site_applications_dirs

@property
Expand All @@ -166,6 +166,11 @@ def site_applications_dir(self) -> str:
return os.pathsep.join(dirs) if self.multipath else dirs[0]


def _xdg_dir_list(env_var: str) -> list[str]:
"""Stripped non-blank entries of ``env_var``, so a value of only separators and whitespace falls back like unset."""
return [stripped for path in os.environ.get(env_var, "").split(os.pathsep) if (stripped := path.strip())]


__all__ = [
"XDGMixin",
]
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"user_pictures_dir",
"user_videos_dir",
"user_music_dir",
"user_desktop_dir",
"user_projects_dir",
"user_publicshare_dir",
"user_templates_dir",
Expand Down
85 changes: 43 additions & 42 deletions tests/test_macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ def _clear_xdg_env(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv(var, raising=False)


@pytest.fixture
def _builtin_py_prefix(mocker: MockerFixture) -> None:
"""Keep ``sys.prefix`` off the ``/opt/python`` Homebrew heuristic so directories use the system defaults."""
py_version = sys.version_info
mocker.patch(
"sys.prefix",
"/Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework"
f"/Versions/{py_version.major}.{py_version.minor}",
)


@pytest.mark.parametrize(
"params",
[
Expand All @@ -51,15 +62,8 @@ def _clear_xdg_env(monkeypatch: pytest.MonkeyPatch) -> None:
pytest.param({"appname": "foo", "version": "v1.0"}, id="app_name_version"),
],
)
@pytest.mark.usefixtures("_clear_xdg_env")
def test_macos(mocker: MockerFixture, params: dict[str, Any], func: str) -> None:
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)

@pytest.mark.usefixtures("_clear_xdg_env", "_builtin_py_prefix")
def test_macos(params: dict[str, Any], func: str) -> None:
result = getattr(MacOS(**params), func)

home = str(Path("~").expanduser())
Expand Down Expand Up @@ -253,16 +257,8 @@ def test_macos_xdg_media_dirs(monkeypatch: pytest.MonkeyPatch, env_var: str, pro
pytest.param("XDG_DESKTOP_DIR", "user_desktop_dir", id="user_desktop_dir"),
],
)
@pytest.mark.usefixtures("_clear_xdg_env")
def test_macos_xdg_empty_falls_back(
monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, env_var: str, prop: str
) -> None:
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)
@pytest.mark.usefixtures("_clear_xdg_env", "_builtin_py_prefix")
def test_macos_xdg_empty_falls_back(monkeypatch: pytest.MonkeyPatch, env_var: str, prop: str) -> None:
monkeypatch.setenv(env_var, "")
home = str(Path("~").expanduser())
expected_map = {
Expand Down Expand Up @@ -349,6 +345,29 @@ def test_iter_config_dirs_homebrew(mocker: MockerFixture) -> None:
assert dirs == [f"{home}/Library/Application Support", "/opt/homebrew/share", "/Library/Application Support"]


@pytest.mark.usefixtures("_clear_xdg_env", "_builtin_py_prefix")
@pytest.mark.parametrize(
"value",
[
pytest.param(":", id="single"),
pytest.param("::", id="double"),
pytest.param(" : ", id="padded"),
pytest.param(": :", id="spaced"),
],
)
@pytest.mark.parametrize("prop", ["site_data_dir", "site_config_dir", "site_applications_dir"])
def test_site_dirs_fall_back_when_xdg_var_is_all_separators(
monkeypatch: pytest.MonkeyPatch, prop: str, value: str
) -> None:
monkeypatch.setenv("XDG_CONFIG_DIRS" if prop == "site_config_dir" else "XDG_DATA_DIRS", value)
expected = {
"site_data_dir": os.path.join("/Library/Application Support", "foo"), # ruff:ignore[os-path-join]
"site_config_dir": os.path.join("/Library/Application Support", "foo"), # ruff:ignore[os-path-join]
"site_applications_dir": "/Applications",
}[prop]
assert getattr(MacOS(appname="foo"), prop) == expected


@pytest.mark.usefixtures("_clear_xdg_env")
@pytest.mark.parametrize("multipath", [True, False])
def test_iter_cache_dirs_homebrew(mocker: MockerFixture, multipath: bool) -> None:
Expand All @@ -366,14 +385,8 @@ def test_iter_cache_paths_homebrew_multipath(mocker: MockerFixture) -> None:
assert paths == [Path(f"{home}/Library/Caches"), Path("/opt/homebrew/var/cache"), Path("/Library/Caches")]


@pytest.mark.usefixtures("_clear_xdg_env")
def test_iter_data_dirs_no_homebrew(mocker: MockerFixture) -> None:
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)
@pytest.mark.usefixtures("_clear_xdg_env", "_builtin_py_prefix")
def test_iter_data_dirs_no_homebrew() -> None:
dirs = list(MacOS().iter_data_dirs())
home = str(Path("~").expanduser())
assert dirs == [f"{home}/Library/Application Support", "/Library/Application Support"]
Expand Down Expand Up @@ -407,27 +420,15 @@ def test_no_xdg_site_dirs_leak(monkeypatch: pytest.MonkeyPatch) -> None:
assert data_value not in config_dirs


@pytest.mark.usefixtures("_clear_xdg_env")
def test_macos_site_runtime_path(mocker: MockerFixture) -> None:
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)
@pytest.mark.usefixtures("_clear_xdg_env", "_builtin_py_prefix")
def test_macos_site_runtime_path() -> None:
result = MacOS(appname="foo").site_runtime_path
home = str(Path("~").expanduser())
assert result == Path(f"{home}/Library/Caches/TemporaryItems/foo")


@pytest.mark.usefixtures("_clear_xdg_env")
@pytest.mark.usefixtures("_clear_xdg_env", "_builtin_py_prefix")
def test_macos_ensure_exists_preexisting_dir(mocker: MockerFixture, tmp_path: Path) -> None:
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)
mocker.patch("platformdirs.macos.os.path.expanduser", lambda p: str(tmp_path / p.lstrip("~/")))
dirs = MacOS(appname="foo", ensure_exists=True)
first = dirs.user_data_dir
Expand Down
39 changes: 39 additions & 0 deletions tests/test_unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,17 @@ def test_xdg_variable_custom_value(monkeypatch: pytest.MonkeyPatch, dirs_instanc
assert result == "/custom-dir"


@pytest.mark.usefixtures("_getuid")
def test_xdg_variable_padded_value(monkeypatch: pytest.MonkeyPatch, dirs_instance: Unix, func: str) -> None:
xdg_variable = _func_to_path(func)
if xdg_variable is None:
return

monkeypatch.setenv(xdg_variable.name, " /custom-dir ")
result = getattr(dirs_instance, func)
assert result == "/custom-dir"


@pytest.mark.parametrize("opinion", [True, False])
def test_site_log_dir_fixed_path(opinion: bool) -> None:
result = Unix(appname="foo", opinion=opinion).site_log_dir
Expand Down Expand Up @@ -329,6 +340,34 @@ def test_iter_config_dirs_xdg(monkeypatch: pytest.MonkeyPatch) -> None:
assert dirs == ["/xdg/config", "/xdg/etc1", "/xdg/etc2"]


@pytest.mark.parametrize(
"value",
[
pytest.param(os.pathsep, id="single"),
pytest.param(os.pathsep * 2, id="double"),
pytest.param(f" {os.pathsep} ", id="padded"),
pytest.param(f"{os.pathsep} {os.pathsep}", id="spaced"),
],
)
@pytest.mark.parametrize("prop", ["site_data_dir", "site_config_dir", "site_applications_dir"])
def test_site_dirs_fall_back_when_xdg_var_is_all_separators(
monkeypatch: pytest.MonkeyPatch, prop: str, value: str
) -> None:
monkeypatch.setenv("XDG_CONFIG_DIRS" if prop == "site_config_dir" else "XDG_DATA_DIRS", value)
expected = {
"site_data_dir": os.path.join("/usr/local/share", "foo"), # ruff:ignore[os-path-join]
"site_config_dir": os.path.join("/etc/xdg", "foo"), # ruff:ignore[os-path-join]
"site_applications_dir": os.path.join("/usr/local/share", "applications"), # ruff:ignore[os-path-join]
}[prop]
assert getattr(Unix(appname="foo"), prop) == expected


def test_site_data_dir_multipath_falls_back_when_xdg_var_is_all_separators(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("XDG_DATA_DIRS", os.pathsep)
dirs = [os.path.join("/usr/local/share", "foo"), os.path.join("/usr/share", "foo")] # ruff:ignore[os-path-join]
assert Unix(appname="foo", multipath=True).site_data_dir == os.pathsep.join(dirs)


def test_user_media_dir_from_user_dirs_file(
mocker: MockerFixture, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down
5 changes: 0 additions & 5 deletions tests/test_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,6 @@ def test_windows(params: dict[str, Any], func: str) -> None:
assert result == expected_map[func]


def test_user_desktop_dir() -> None:
# The shared PROPS fixture skips this one, so Windows would otherwise never exercise the property.
assert Windows().user_desktop_dir == os.path.normpath(_WIN_FOLDERS["CSIDL_DESKTOPDIRECTORY"])


def test_roaming_uses_appdata(mocker: MockerFixture) -> None:
mock = mocker.patch("platformdirs.windows.get_win_folder", side_effect=lambda csidl: _WIN_FOLDERS[csidl])
_result = Windows(appname="foo", roaming=True).user_data_dir
Expand Down