Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/platformdirs/unix.py: 1%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Unix."""
3from __future__ import annotations
5import os
6import re
7import stat
8import sys
9import warnings
10from contextlib import suppress
11from pathlib import Path
12from tempfile import gettempdir
13from typing import Final, NoReturn
15from ._xdg import XDGMixin, _expand_user, _xdg_dir
16from .api import PlatformDirsABC, RuntimeDirWarning
18if sys.platform == "win32":
20 def getuid() -> NoReturn:
21 msg = "should only be used on Unix"
22 raise RuntimeError(msg)
24else:
25 from os import getuid
28class _UnixDefaults(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
29 """Default directories for Unix/Linux without XDG environment variable overrides.
31 The XDG env var handling is in :class:`~platformdirs._xdg.XDGMixin`.
33 """
35 @property
36 def _use_site(self) -> bool:
37 return self.use_site_for_root and getuid() == 0
39 @property
40 def user_data_dir(self) -> str:
41 """Data directory tied to the user, e.g. ``~/.local/share/$appname/$version`` or ``$XDG_DATA_HOME/$appname/$version``."""
42 return self._append_app_name_and_version(_expand_user("~/.local/share"), private=True)
44 @property
45 def _site_data_dirs(self) -> list[str]:
46 return [self._join_app_name_and_version("/usr/local/share"), self._join_app_name_and_version("/usr/share")]
48 @property
49 def user_config_dir(self) -> str:
50 """Config directory tied to the user, e.g. ``~/.config/$appname/$version`` or ``$XDG_CONFIG_HOME/$appname/$version``."""
51 return self._append_app_name_and_version(_expand_user("~/.config"), private=True)
53 @property
54 def _site_config_dirs(self) -> list[str]:
55 return [self._join_app_name_and_version("/etc/xdg")]
57 @property
58 def user_cache_dir(self) -> str:
59 """Cache directory tied to the user, e.g. ``~/.cache/$appname/$version`` or ``$XDG_CACHE_HOME/$appname/$version``."""
60 return self._append_app_name_and_version(_expand_user("~/.cache"), private=True)
62 @property
63 def site_cache_dir(self) -> str:
64 """Cache directory shared by users, e.g. ``/var/cache/$appname/$version``."""
65 return self._append_app_name_and_version("/var/cache", private=False)
67 @property
68 def user_state_dir(self) -> str:
69 """State directory tied to the user, e.g. ``~/.local/state/$appname/$version`` or ``$XDG_STATE_HOME/$appname/$version``."""
70 return self._append_app_name_and_version(_expand_user("~/.local/state"), private=True)
72 @property
73 def site_state_dir(self) -> str:
74 """State directory shared by users, e.g. ``/var/lib/$appname/$version``."""
75 return self._append_app_name_and_version("/var/lib", private=False)
77 @property
78 def user_log_dir(self) -> str:
79 """Log directory tied to the user, same as `user_state_dir` if not opinionated else ``log`` in it."""
80 path = self.user_state_dir
81 if self.opinion:
82 path = os.path.join(path, "log") # ruff:ignore[os-path-join]
83 self._optionally_create_directory(path, private=True)
84 return path
86 @property
87 def site_log_dir(self) -> str:
88 """Log directory shared by users, e.g. ``/var/log/$appname/$version``.
90 Unlike `user_log_dir`, ``opinion`` has no effect since ``/var/log`` is inherently a log directory.
92 """
93 return self._append_app_name_and_version("/var/log", private=False)
95 @property
96 def user_documents_dir(self) -> str:
97 """Documents directory tied to the user, e.g. ``~/Documents``."""
98 return self._user_media_dir("XDG_DOCUMENTS_DIR", "~/Documents")
100 @property
101 def user_downloads_dir(self) -> str:
102 """Downloads directory tied to the user, e.g. ``~/Downloads``."""
103 return self._user_media_dir("XDG_DOWNLOAD_DIR", "~/Downloads")
105 @property
106 def user_pictures_dir(self) -> str:
107 """Pictures directory tied to the user, e.g. ``~/Pictures``."""
108 return self._user_media_dir("XDG_PICTURES_DIR", "~/Pictures")
110 @property
111 def user_videos_dir(self) -> str:
112 """Videos directory tied to the user, e.g. ``~/Videos``."""
113 return self._user_media_dir("XDG_VIDEOS_DIR", "~/Videos")
115 @property
116 def user_music_dir(self) -> str:
117 """Music directory tied to the user, e.g. ``~/Music``."""
118 return self._user_media_dir("XDG_MUSIC_DIR", "~/Music")
120 @property
121 def user_desktop_dir(self) -> str:
122 """Desktop directory tied to the user, e.g. ``~/Desktop``."""
123 return self._user_media_dir("XDG_DESKTOP_DIR", "~/Desktop")
125 @property
126 def user_projects_dir(self) -> str:
127 """Projects directory tied to the user, e.g. ``~/Projects``."""
128 return self._user_media_dir("XDG_PROJECTS_DIR", "~/Projects")
130 @property
131 def user_publicshare_dir(self) -> str:
132 """Public share directory tied to the user, e.g. ``~/Public``."""
133 return self._user_media_dir("XDG_PUBLICSHARE_DIR", "~/Public")
135 @property
136 def user_templates_dir(self) -> str:
137 """Templates directory tied to the user, e.g. ``~/Templates``."""
138 return self._user_media_dir("XDG_TEMPLATES_DIR", "~/Templates")
140 def _user_media_dir(self, key: str, default: str) -> str:
141 if path := _get_user_dirs_folder(key):
142 self._optionally_create_media_directory(path)
143 return path
144 return _expand_user(default)
146 @property
147 def user_fonts_dir(self) -> str:
148 """Fonts directory tied to the user, e.g. ``~/.local/share/fonts``."""
149 return f"{_expand_user('~/.local/share')}/fonts"
151 @property
152 def user_preference_dir(self) -> str:
153 """Preference directory tied to the user, same as ``user_config_dir``."""
154 return self.user_config_dir
156 @property
157 def user_bin_dir(self) -> str:
158 """Bin directory tied to the user, e.g. ``~/.local/bin``."""
159 return _expand_user("~/.local/bin")
161 @property
162 def site_bin_dir(self) -> str:
163 """Bin directory shared by users, e.g. ``/usr/local/bin``."""
164 return "/usr/local/bin"
166 @property
167 def user_applications_dir(self) -> str:
168 """Applications directory tied to the user, e.g. ``~/.local/share/applications``."""
169 return f"{_expand_user('~/.local/share')}{os.sep}applications"
171 @property
172 def _site_applications_dirs(self) -> list[str]:
173 return [os.path.join(p, "applications") for p in ["/usr/local/share", "/usr/share"]] # ruff:ignore[os-path-join]
175 def _default_runtime_dir(self) -> str:
176 if sys.platform.startswith("openbsd"):
177 path = f"/tmp/run/user/{getuid()}" # ruff:ignore[hardcoded-temp-file]
178 elif sys.platform.startswith(("freebsd", "netbsd")):
179 path = f"/var/run/user/{getuid()}"
180 else:
181 path = f"/run/user/{getuid()}"
182 return path if os.path.lexists(path) and not _runtime_dir_problem(path) else self._temp_runtime_dir()
184 def _temp_runtime_dir(self) -> str:
185 # Another user can pre-create this predictable name, and XDG requires an owned runtime dir with mode 0700.
186 path = f"{gettempdir()}/runtime-{getuid()}"
187 with suppress(FileExistsError):
188 self._optionally_create_directory(path, private=True)
189 if problem := _runtime_dir_problem(path, check_mode=False):
190 msg = f"runtime directory {problem}; set XDG_RUNTIME_DIR instead"
191 raise PermissionError(msg)
192 # Earlier releases created this directory with the default 0755.
193 if self.ensure_exists:
194 Path(path).chmod(_RUNTIME_DIR_MODE)
195 return path
197 @property
198 def site_runtime_dir(self) -> str:
199 """Runtime directory shared by users, e.g. ``/run/$appname/$version`` or ``$XDG_RUNTIME_DIR/$appname/$version``.
201 Note that this behaves almost exactly like `user_runtime_dir` if ``$XDG_RUNTIME_DIR`` is set, but will fall back
202 to paths associated to the root user instead of a regular logged-in user if it's not set.
204 If you wish to ensure that a logged-in root user path is returned e.g. ``/run/user/0``, use `user_runtime_dir`
205 instead.
207 For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/$appname/$version`` if ``$XDG_RUNTIME_DIR`` is not set.
209 """
210 if sys.platform.startswith(("freebsd", "openbsd", "netbsd")):
211 path = "/var/run"
212 else:
213 path = "/run"
214 return self._append_app_name_and_version(path, private=False)
217class Unix(XDGMixin, _UnixDefaults):
218 """On Unix/Linux, we follow the `XDG Basedir Spec <https://specifications.freedesktop.org/basedir/latest/>`_.
220 The spec allows overriding directories with environment variables. The examples shown are the default values,
221 alongside the name of the environment variable that overrides them. Makes use of the `appname
222 <platformdirs.api.PlatformDirsABC.appname>`, `version <platformdirs.api.PlatformDirsABC.version>`, `multipath
223 <platformdirs.api.PlatformDirsABC.multipath>`, `opinion <platformdirs.api.PlatformDirsABC.opinion>`, `ensure_exists
224 <platformdirs.api.PlatformDirsABC.ensure_exists>`.
226 """
228 @property
229 def user_data_dir(self) -> str:
230 """Data directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
231 return self.site_data_dir if self._use_site else super().user_data_dir
233 @property
234 def user_config_dir(self) -> str:
235 """Config directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
236 return self.site_config_dir if self._use_site else super().user_config_dir
238 @property
239 def user_cache_dir(self) -> str:
240 """Cache directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
241 return self.site_cache_dir if self._use_site else super().user_cache_dir
243 @property
244 def user_state_dir(self) -> str:
245 """State directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
246 return self.site_state_dir if self._use_site else super().user_state_dir
248 @property
249 def user_log_dir(self) -> str:
250 """Log directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
251 return self.site_log_dir if self._use_site else super().user_log_dir
253 @property
254 def user_applications_dir(self) -> str:
255 """Applications directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
256 return self.site_applications_dir if self._use_site else super().user_applications_dir
258 @property
259 def user_runtime_dir(self) -> str:
260 """Runtime directory tied to the user, e.g. ``$XDG_RUNTIME_DIR/$appname/$version``.
262 Accepts ``$XDG_RUNTIME_DIR`` only as a directory, not a symlink, that the user owns with mode ``0700``, as the
263 XDG spec requires, and creates a missing one that way under ``ensure_exists``. When the variable is unset or
264 fails that check, emits one :class:`~platformdirs.RuntimeDirWarning` per cause and falls back to the platform
265 default (``/tmp/run/user/<uid>`` on OpenBSD, ``/var/run/user/<uid>`` on FreeBSD/NetBSD, ``/run/user/<uid>``
266 elsewhere) if it passes the same check, else to ``runtime-<uid>`` in the temporary directory. Root with
267 ``use_site_for_root`` gets the site equivalent.
269 :raises PermissionError: if the temporary fallback is a symlink, not a directory, or owned by another user.
271 """
272 if self._use_site:
273 # XDGMixin.site_runtime_dir reads $XDG_RUNTIME_DIR, which belongs to the user who started the root process.
274 return super(XDGMixin, self).site_runtime_dir
275 if not (path := _xdg_dir("XDG_RUNTIME_DIR")):
276 reason = "XDG_RUNTIME_DIR is not set"
277 else:
278 with suppress(OSError):
279 self._optionally_create_directory(path, private=True)
280 if not (problem := _runtime_dir_problem(path)):
281 return self._append_app_name_and_version(path, private=True)
282 reason = f"XDG_RUNTIME_DIR {problem}"
283 _warn_once(f"{reason}, falling back to {(fallback := self._default_runtime_dir())}")
284 return self._append_app_name_and_version(fallback, private=True)
286 @property
287 def user_bin_dir(self) -> str:
288 """Bin directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
289 return self.site_bin_dir if self._use_site else super().user_bin_dir
291 @property
292 def user_data_path(self) -> Path:
293 """Data path tied to the user, or the first site entry when root with ``use_site_for_root``."""
294 return self.site_data_path if self._use_site else super().user_data_path
296 @property
297 def user_config_path(self) -> Path:
298 """Config path tied to the user, or the first site entry when root with ``use_site_for_root``."""
299 return self.site_config_path if self._use_site else super().user_config_path
301 @property
302 def user_preference_path(self) -> Path:
303 """Preference path tied to the user, or the first site config entry when root with ``use_site_for_root``."""
304 return self.site_config_path if self._use_site else super().user_preference_path
306 @property
307 def user_applications_path(self) -> Path:
308 """Applications path tied to the user, or the first site entry when root with ``use_site_for_root``."""
309 return self.site_applications_path if self._use_site else super().user_applications_path
312_USER_DIRS_LINE: Final = re.compile(
313 r"""
314 [ \t]*(?P<key>\w+)[ \t]*=[ \t]* # KEY=, blanks allowed around the key and after the =
315 (?:
316 "(?P<quoted>(?:[^"\\]|\\.)*)" # a double-quoted value with backslash escapes, text after it ignored
317 |(?P<bare>[^"\s].*?) # or an unquoted value, up to
318 (?:[ \t]+\#.*|[ \t]*)$ # a comment, which sh starts at a word-initial #, or the line end
319 )
320 """,
321 re.VERBOSE,
322)
325def _get_user_dirs_folder(key: str) -> str | None:
326 """Return directory from user-dirs.dirs config file.
328 ``xdg-user-dirs-update`` writes shell assignments, so match each line like ``xdg-user-dir`` does and keep the last
329 valid assignment to ``key``.
331 See https://freedesktop.org/wiki/Software/xdg-user-dirs/.
333 """
334 config_home = _xdg_dir("XDG_CONFIG_HOME") or _expand_user("~/.config")
335 user_dirs_config_path = Path(config_home) / "user-dirs.dirs"
336 folder = None
337 try:
338 # the file holds raw path bytes, and surrogateescape round-trips undecodable ones like os.fsdecode
339 with user_dirs_config_path.open(encoding=sys.getfilesystemencoding(), errors="surrogateescape") as stream:
340 for line in stream:
341 if (entry := _USER_DIRS_LINE.match(line)) and entry["key"] == key:
342 folder = _resolve_user_dirs_value(entry) or folder
343 except OSError: # a missing or unreadable file leaves the defaults in place
344 return None
345 return folder
348def _resolve_user_dirs_value(entry: re.Match[str]) -> str | None:
349 value = entry["bare"] if entry["quoted"] is None else entry["quoted"]
350 if value == "$HOME" or value.startswith("$HOME/"):
351 # A home of / would give "//Desktop", which pathlib keeps as a distinct root.
352 prefix, value = _expand_user("~").rstrip("/"), value.removeprefix("$HOME")
353 elif value.startswith("/"):
354 prefix = ""
355 else:
356 return None
357 if entry["quoted"] is not None:
358 # xdg-user-dirs-update backslash-escapes $, `, " and \ inside the quotes.
359 value = re.sub(r'\\([$`"\\])', r"\1", value)
360 return f"{prefix}{value}" or "/"
363def _runtime_dir_problem(path: str, *, check_mode: bool = True) -> str | None:
364 """Return why ``path`` fails the checks of Qt's ``checkXdgRuntimeDir``, or ``None`` if it passes or is missing."""
365 try:
366 info = os.lstat(path)
367 except FileNotFoundError:
368 return None
369 except OSError as error:
370 return f"{path} cannot be checked: {error.strerror}"
371 mode = info.st_mode & 0o777
372 checks = (
373 (stat.S_ISLNK(info.st_mode), "is a symlink"),
374 (not stat.S_ISDIR(info.st_mode), "is not a directory"),
375 (info.st_uid != (uid := getuid()), f"is owned by uid {info.st_uid}, not {uid}"),
376 (check_mode and mode != _RUNTIME_DIR_MODE, f"has mode {mode:04o}, not {_RUNTIME_DIR_MODE:04o}"),
377 )
378 return next((f"{path} {problem}" for failed, problem in checks if failed), None)
381def _warn_once(message: str) -> None:
382 # Each read of user_runtime_dir repeats the check, so a warning per read would flood long-running programs.
383 if message not in _WARNED:
384 _WARNED.add(message)
385 warnings.warn(message, RuntimeDirWarning, stacklevel=3)
388_WARNED: Final[set[str]] = set()
389_RUNTIME_DIR_MODE: Final = 0o700
392__all__ = [
393 "Unix",
394]