diff --git a/src/platformdirs/unix.py b/src/platformdirs/unix.py index 6a37d25f..d0cb6719 100644 --- a/src/platformdirs/unix.py +++ b/src/platformdirs/unix.py @@ -6,6 +6,7 @@ import sys from configparser import ConfigParser from pathlib import Path +from tempfile import gettempdir from typing import TYPE_CHECKING, NoReturn from ._xdg import XDGMixin @@ -118,19 +119,18 @@ def user_desktop_dir(self) -> str: @property def user_runtime_dir(self) -> str: """ - :return: runtime directory tied to the user, e.g. ``/run/user/$(id -u)/$appname/$version`` or - ``$XDG_RUNTIME_DIR/$appname/$version``. + :return: runtime directory tied to the user, e.g. ``$XDG_RUNTIME_DIR/$appname/$version``. - For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/user/$(id -u)/$appname/$version`` if - exists, otherwise ``/tmp/runtime-$(id -u)/$appname/$version``, if``$XDG_RUNTIME_DIR`` - is not set. + If ``$XDG_RUNTIME_DIR`` is unset, tries the platform default (``/var/run/user/$(id -u)`` on + FreeBSD/OpenBSD/NetBSD, ``/run/user/$(id -u)`` otherwise). If the default is not writable, + falls back to a temporary directory. """ if sys.platform.startswith(("freebsd", "openbsd", "netbsd")): path = f"/var/run/user/{getuid()}" - if not Path(path).exists(): - path = f"/tmp/runtime-{getuid()}" # noqa: S108 else: path = f"/run/user/{getuid()}" + if not os.access(path, os.W_OK): + path = f"{gettempdir()}/runtime-{getuid()}" return self._append_app_name_and_version(path) @property diff --git a/tests/test_unix.py b/tests/test_unix.py index 4014326f..1f04aa9f 100644 --- a/tests/test_unix.py +++ b/tests/test_unix.py @@ -4,6 +4,7 @@ import os import sys import typing +from tempfile import gettempdir import pytest @@ -16,6 +17,12 @@ from pytest_mock import MockerFixture +@pytest.fixture(autouse=True) +def _reload_after_test() -> typing.Iterator[None]: + yield + importlib.reload(unix) + + @pytest.mark.parametrize( "prop", [ @@ -97,7 +104,7 @@ def _func_to_path(func: str) -> XDGVariable | None: "user_cache_dir": XDGVariable("XDG_CACHE_HOME", "~/.cache"), "user_state_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"), "user_log_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"), - "user_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/run/user/1234"), + "user_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", f"{gettempdir()}/runtime-1234"), "site_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/run"), } return mapping.get(func) @@ -151,13 +158,14 @@ def test_xdg_variable_custom_value(monkeypatch: pytest.MonkeyPatch, dirs_instanc def test_platform_on_bsd(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, platform: str) -> None: monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False) mocker.patch("sys.platform", platform) + mocker.patch("tempfile.tempdir", "/tmp") # noqa: S108 assert Unix().site_runtime_dir == "/var/run" - mocker.patch("pathlib.Path.exists", return_value=True) + mocker.patch("os.access", return_value=True) assert Unix().user_runtime_dir == "/var/run/user/1234" - mocker.patch("pathlib.Path.exists", return_value=False) + mocker.patch("os.access", return_value=False) assert Unix().user_runtime_dir == "/tmp/runtime-1234" # noqa: S108 @@ -173,6 +181,45 @@ def test_platform_on_win32(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixtur sys.modules["platformdirs.unix"] = prev_unix +@pytest.mark.usefixtures("_getuid") +@pytest.mark.parametrize( + ("platform", "default_dir"), + [ + ("freebsd", "/var/run/user/1234"), + ("linux", "/run/user/1234"), + ], +) +def test_xdg_runtime_dir_unset_writable( + monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, platform: str, default_dir: str +) -> None: + monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False) + mocker.patch("sys.platform", platform) + mocker.patch("os.access", return_value=True) + + assert Unix().user_runtime_dir == default_dir + + +@pytest.mark.usefixtures("_getuid") +@pytest.mark.parametrize( + ("platform", "default_dir"), + [ + ("freebsd", "/var/run/user/1234"), + ("linux", "/run/user/1234"), + ], +) +def test_xdg_runtime_dir_unset_not_writable( + monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, platform: str, default_dir: str +) -> None: + monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False) + mocker.patch("sys.platform", platform) + mocker.patch("os.access", return_value=False) + mocker.patch("tempfile.tempdir", "/tmp") # noqa: S108 + + result = Unix().user_runtime_dir + assert not result.startswith(default_dir) + assert result == "/tmp/runtime-1234" # noqa: S108 + + def test_ensure_exists_creates_folder(mocker: MockerFixture, tmp_path: Path) -> None: mocker.patch.dict(os.environ, {"XDG_DATA_HOME": str(tmp_path)}) data_path = Unix(appname="acme", ensure_exists=True).user_data_path