Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ optional-dependencies.docs = [
optional-dependencies.test = [
"appdirs==1.4.4",
"covdefaults>=2.3",
"pyfakefs>=5.9.2",
"pytest>=8.3.4",
"pytest-cov>=6",
"pytest-mock>=3.14",
Expand Down
28 changes: 16 additions & 12 deletions src/platformdirs/unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 .api import PlatformDirsABC
Expand Down Expand Up @@ -167,21 +168,24 @@ 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, it tries the platform default location of that runtime directory
(``/var/run/user/$(id -u)`` on FreeBSD/OpenBSD/NetBSD, ``/run/user/$(id -u)`` otherwise).
If the default location is not writable, it uses a temporary directory instead.
"""
path = os.environ.get("XDG_RUNTIME_DIR", "")
if not path.strip():
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 path.strip():
return self._append_app_name_and_version(path)

if sys.platform.startswith(("freebsd", "openbsd", "netbsd")):
path = f"/var/run/user/{getuid()}"
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
Expand Down
37 changes: 34 additions & 3 deletions tests/test_unix.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import os
import sys
import typing
from tempfile import gettempdir

import pytest

Expand All @@ -13,6 +14,7 @@
if typing.TYPE_CHECKING:
from pathlib import Path

from pyfakefs.fake_filesystem import FakeFilesystem
from pytest_mock import MockerFixture


Expand Down Expand Up @@ -97,7 +99,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", "/tmp/runtime-1234"), # noqa: S108
"site_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/run"),
}
return mapping.get(func)
Expand Down Expand Up @@ -154,10 +156,10 @@ def test_platform_on_bsd(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture,

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


Expand All @@ -173,6 +175,35 @@ 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(
monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, fs: FakeFilesystem, platform: str, default_dir: str
) -> None:
monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
mocker.patch("sys.platform", platform)

fs.create_dir(default_dir)

assert Unix().user_runtime_dir.startswith(default_dir)

# If the default directory isn't writable, we shouldn't use it.
fs.chmod(default_dir, 0o000)
assert not Unix().user_runtime_dir.startswith(default_dir)
assert Unix().user_runtime_dir.startswith(gettempdir())

# If the runtime directory doesn't exist, we shouldn't use it.
fs.rmdir(default_dir)
assert not Unix().user_runtime_dir.startswith(default_dir)
assert Unix().user_runtime_dir.startswith(gettempdir())


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
Expand Down
Loading