Skip to content

Commit df22bec

Browse files
committed
fix(Unix.user_runtime_dir): nonexistent runtime dir
This fixes cases where XDG_RUNTIME_DIR is unset and the default runtime dir does not exist by getting a directory using Python's built-in tempfile. Fixes #368
1 parent 5720f26 commit df22bec

File tree

3 files changed

+47
-10
lines changed

3 files changed

+47
-10
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ optional-dependencies.docs = [
5353
optional-dependencies.test = [
5454
"appdirs==1.4.4",
5555
"covdefaults>=2.3",
56+
"pyfakefs>=5.9.2",
5657
"pytest>=8.3.4",
5758
"pytest-cov>=6",
5859
"pytest-mock>=3.14",

src/platformdirs/unix.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
from configparser import ConfigParser
88
from pathlib import Path
9+
from tempfile import gettempdir
910
from typing import TYPE_CHECKING, NoReturn
1011

1112
from .api import PlatformDirsABC
@@ -175,13 +176,17 @@ def user_runtime_dir(self) -> str:
175176
is not set.
176177
"""
177178
path = os.environ.get("XDG_RUNTIME_DIR", "")
178-
if not path.strip():
179-
if sys.platform.startswith(("freebsd", "openbsd", "netbsd")):
180-
path = f"/var/run/user/{getuid()}"
181-
if not Path(path).exists():
182-
path = f"/tmp/runtime-{getuid()}" # noqa: S108
183-
else:
184-
path = f"/run/user/{getuid()}"
179+
if path.strip():
180+
return self._append_app_name_and_version(path)
181+
182+
if sys.platform.startswith(("freebsd", "openbsd", "netbsd")):
183+
path = f"/var/run/user/{getuid()}"
184+
else:
185+
path = f"/run/user/{getuid()}"
186+
187+
if not os.access(path, os.W_OK):
188+
path = f"{gettempdir()}/runtime-{getuid()}"
189+
185190
return self._append_app_name_and_version(path)
186191

187192
@property

tests/test_unix.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
import sys
66
import typing
7+
from tempfile import gettempdir
78

89
import pytest
910

@@ -13,6 +14,7 @@
1314
if typing.TYPE_CHECKING:
1415
from pathlib import Path
1516

17+
from pyfakefs.fake_filesystem import FakeFilesystem
1618
from pytest_mock import MockerFixture
1719

1820

@@ -97,7 +99,7 @@ def _func_to_path(func: str) -> XDGVariable | None:
9799
"user_cache_dir": XDGVariable("XDG_CACHE_HOME", "~/.cache"),
98100
"user_state_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"),
99101
"user_log_dir": XDGVariable("XDG_STATE_HOME", "~/.local/state"),
100-
"user_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/run/user/1234"),
102+
"user_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/tmp/runtime-1234"), # noqa: S108
101103
"site_runtime_dir": XDGVariable("XDG_RUNTIME_DIR", "/run"),
102104
}
103105
return mapping.get(func)
@@ -154,10 +156,10 @@ def test_platform_on_bsd(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture,
154156

155157
assert Unix().site_runtime_dir == "/var/run"
156158

157-
mocker.patch("pathlib.Path.exists", return_value=True)
159+
mocker.patch("os.access", return_value=True)
158160
assert Unix().user_runtime_dir == "/var/run/user/1234"
159161

160-
mocker.patch("pathlib.Path.exists", return_value=False)
162+
mocker.patch("os.access", return_value=False)
161163
assert Unix().user_runtime_dir == "/tmp/runtime-1234" # noqa: S108
162164

163165

@@ -173,6 +175,35 @@ def test_platform_on_win32(monkeypatch: pytest.MonkeyPatch, mocker: MockerFixtur
173175
sys.modules["platformdirs.unix"] = prev_unix
174176

175177

178+
@pytest.mark.usefixtures("_getuid")
179+
@pytest.mark.parametrize(
180+
("platform", "default_dir"),
181+
[
182+
("freebsd", "/var/run/user/1234"),
183+
("linux", "/run/user/1234"),
184+
],
185+
)
186+
def test_xdg_runtime_dir_unset(
187+
monkeypatch: pytest.MonkeyPatch, mocker: MockerFixture, fs: FakeFilesystem, platform: str, default_dir: str
188+
) -> None:
189+
monkeypatch.delenv("XDG_RUNTIME_DIR", raising=False)
190+
mocker.patch("sys.platform", platform)
191+
192+
fs.create_dir(default_dir)
193+
194+
assert Unix().user_runtime_dir.startswith(default_dir)
195+
196+
# If the default directory isn't writable, we shouldn't use it.
197+
fs.chmod(default_dir, 0o000)
198+
assert not Unix().user_runtime_dir.startswith(default_dir)
199+
assert Unix().user_runtime_dir.startswith(gettempdir())
200+
201+
# If the runtime directory doesn't exist, we shouldn't use it.
202+
fs.rmdir(default_dir)
203+
assert not Unix().user_runtime_dir.startswith(default_dir)
204+
assert Unix().user_runtime_dir.startswith(gettempdir())
205+
206+
176207
def test_ensure_exists_creates_folder(mocker: MockerFixture, tmp_path: Path) -> None:
177208
mocker.patch.dict(os.environ, {"XDG_DATA_HOME": str(tmp_path)})
178209
data_path = Unix(appname="acme", ensure_exists=True).user_data_path

0 commit comments

Comments
 (0)