Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/platformdirs/unix.py: 46%
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 posixpath
7import re
8import stat
9import sys
10import warnings
11from contextlib import suppress
12from pathlib import Path
13from tempfile import gettempdir
14from typing import TYPE_CHECKING, Final, NoReturn
16from ._xdg import XDGMixin, _expand_user, _xdg_dir
17from .api import PlatformDirsABC, RuntimeDirWarning
19if TYPE_CHECKING:
20 from collections.abc import Iterator
22if sys.platform == "win32":
24 def geteuid() -> NoReturn:
25 msg = "should only be used on Unix"
26 raise RuntimeError(msg)
28else:
29 try:
30 from os import geteuid
31 except ImportError:
32 from os import getuid as geteuid
35class _UnixDefaults(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
36 """Default directories for Unix/Linux without XDG environment variable overrides.
38 The XDG env var handling is in :class:`~platformdirs._xdg.XDGMixin`.
40 """
42 @property
43 def _use_site(self) -> bool:
44 return self.use_site_for_root and geteuid() == 0
46 @property
47 def user_data_dir(self) -> str:
48 """Data directory tied to the user, e.g. ``~/.local/share/$appname/$version`` or ``$XDG_DATA_HOME/$appname/$version``."""
49 return self._append_app_name_and_version(_expand_user("~/.local/share"), private=True)
51 @property
52 def _site_data_dirs(self) -> list[str]:
53 return [self._join_app_name_and_version("/usr/local/share"), self._join_app_name_and_version("/usr/share")]
55 @property
56 def user_config_dir(self) -> str:
57 """Config directory tied to the user, e.g. ``~/.config/$appname/$version`` or ``$XDG_CONFIG_HOME/$appname/$version``."""
58 return self._append_app_name_and_version(_expand_user("~/.config"), private=True)
60 @property
61 def _site_config_dirs(self) -> list[str]:
62 return [self._join_app_name_and_version("/etc/xdg")]
64 @property
65 def user_cache_dir(self) -> str:
66 """Cache directory tied to the user, e.g. ``~/.cache/$appname/$version`` or ``$XDG_CACHE_HOME/$appname/$version``."""
67 return self._append_app_name_and_version(_expand_user("~/.cache"), private=True)
69 @property
70 def site_cache_dir(self) -> str:
71 """Cache directory shared by users, e.g. ``/var/cache/$appname/$version``."""
72 return self._append_app_name_and_version("/var/cache", private=False)
74 @property
75 def user_state_dir(self) -> str:
76 """State directory tied to the user, e.g. ``~/.local/state/$appname/$version`` or ``$XDG_STATE_HOME/$appname/$version``."""
77 return self._append_app_name_and_version(_expand_user("~/.local/state"), private=True)
79 @property
80 def site_state_dir(self) -> str:
81 """State directory shared by users, e.g. ``/var/lib/$appname/$version``."""
82 return self._append_app_name_and_version("/var/lib", private=False)
84 @property
85 def user_log_dir(self) -> str:
86 """Log directory tied to the user, same as `user_state_dir` if not opinionated else ``log`` in it."""
87 path = self.user_state_dir
88 if self.opinion:
89 path = os.path.join(path, "log") # ruff:ignore[os-path-join]
90 self._optionally_create_directory(path, private=True)
91 return path
93 @property
94 def site_log_dir(self) -> str:
95 """Log directory shared by users, e.g. ``/var/log/$appname/$version``.
97 Unlike `user_log_dir`, ``opinion`` has no effect since ``/var/log`` is inherently a log directory.
99 """
100 return self._append_app_name_and_version("/var/log", private=False)
102 @property
103 def user_documents_dir(self) -> str:
104 """Documents directory tied to the user, e.g. ``~/Documents``."""
105 return self._user_media_dir("XDG_DOCUMENTS_DIR", "~/Documents")
107 @property
108 def user_downloads_dir(self) -> str:
109 """Downloads directory tied to the user, e.g. ``~/Downloads``."""
110 return self._user_media_dir("XDG_DOWNLOAD_DIR", "~/Downloads")
112 @property
113 def user_pictures_dir(self) -> str:
114 """Pictures directory tied to the user, e.g. ``~/Pictures``."""
115 return self._user_media_dir("XDG_PICTURES_DIR", "~/Pictures")
117 @property
118 def user_videos_dir(self) -> str:
119 """Videos directory tied to the user, e.g. ``~/Videos``."""
120 return self._user_media_dir("XDG_VIDEOS_DIR", "~/Videos")
122 @property
123 def user_music_dir(self) -> str:
124 """Music directory tied to the user, e.g. ``~/Music``."""
125 return self._user_media_dir("XDG_MUSIC_DIR", "~/Music")
127 @property
128 def user_desktop_dir(self) -> str:
129 """Desktop directory tied to the user, e.g. ``~/Desktop``."""
130 return self._user_media_dir("XDG_DESKTOP_DIR", "~/Desktop")
132 @property
133 def user_projects_dir(self) -> str:
134 """Projects directory tied to the user, e.g. ``~/Projects``."""
135 return self._user_media_dir("XDG_PROJECTS_DIR", "~/Projects")
137 @property
138 def user_publicshare_dir(self) -> str:
139 """Public share directory tied to the user, e.g. ``~/Public``."""
140 return self._user_media_dir("XDG_PUBLICSHARE_DIR", "~/Public")
142 @property
143 def user_templates_dir(self) -> str:
144 """Templates directory tied to the user, e.g. ``~/Templates``."""
145 return self._user_media_dir("XDG_TEMPLATES_DIR", "~/Templates")
147 def _user_media_dir(self, key: str, default: str) -> str:
148 if path := _get_user_dirs_folder(key):
149 self._optionally_create_media_directory(path)
150 return path
151 return _expand_user(default)
153 @property
154 def user_fonts_dir(self) -> str:
155 """Fonts directory tied to the user, e.g. ``~/.local/share/fonts``."""
156 return f"{_expand_user('~/.local/share')}/fonts"
158 @property
159 def user_preference_dir(self) -> str:
160 """Preference directory tied to the user, same as ``user_config_dir``."""
161 return self.user_config_dir
163 @property
164 def user_bin_dir(self) -> str:
165 """Bin directory tied to the user, e.g. ``~/.local/bin``."""
166 return _expand_user("~/.local/bin")
168 @property
169 def site_bin_dir(self) -> str:
170 """Bin directory shared by users, e.g. ``/usr/local/bin``."""
171 return "/usr/local/bin"
173 @property
174 def user_applications_dir(self) -> str:
175 """Applications directory tied to the user, e.g. ``~/.local/share/applications``."""
176 return f"{_expand_user('~/.local/share')}{os.sep}applications"
178 @property
179 def _site_applications_dirs(self) -> list[str]:
180 return [os.path.join(p, "applications") for p in ["/usr/local/share", "/usr/share"]] # ruff:ignore[os-path-join]
182 def _default_runtime_dir(self) -> str:
183 if sys.platform.startswith("openbsd"):
184 path = f"/tmp/run/user/{geteuid()}" # ruff:ignore[hardcoded-temp-file]
185 elif sys.platform.startswith(("freebsd", "netbsd")):
186 path = f"/var/run/user/{geteuid()}"
187 else:
188 path = f"/run/user/{geteuid()}"
189 return self._temp_runtime_dir() if _runtime_dir_problem(path, missing_ok=False) else path
191 def _temp_runtime_dir(self) -> str:
192 # Another user can pre-create this predictable name, and XDG requires an owned runtime dir with mode 0700.
193 path = _temp_runtime_fallback()
194 with suppress(FileExistsError):
195 self._optionally_create_directory(path, private=True)
196 if problem := _runtime_dir_problem(path, missing_ok=not self.ensure_exists, check_mode=False):
197 msg = f"runtime directory {problem}; set XDG_RUNTIME_DIR instead"
198 raise PermissionError(msg)
199 # Earlier releases created this directory with the default 0755.
200 if self.ensure_exists:
201 Path(path).chmod(_RUNTIME_DIR_MODE)
202 return path
204 @property
205 def site_runtime_dir(self) -> str:
206 """Runtime directory shared by users, e.g. ``/run/$appname/$version`` or ``$XDG_RUNTIME_DIR/$appname/$version``.
208 Note that this behaves almost exactly like `user_runtime_dir` if ``$XDG_RUNTIME_DIR`` is set, but will fall back
209 to paths associated to the root user instead of a regular logged-in user if it's not set.
211 If you wish to ensure that a logged-in root user path is returned e.g. ``/run/user/0``, use `user_runtime_dir`
212 instead.
214 For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/$appname/$version`` if ``$XDG_RUNTIME_DIR`` is not set.
216 """
217 if sys.platform.startswith(("freebsd", "openbsd", "netbsd")):
218 path = "/var/run"
219 else:
220 path = "/run"
221 return self._append_app_name_and_version(path, private=False)
224class Unix(XDGMixin, _UnixDefaults):
225 """On Unix/Linux, we follow the `XDG Basedir Spec <https://specifications.freedesktop.org/basedir/latest/>`_.
227 The spec allows overriding directories with environment variables. The examples shown are the default values,
228 alongside the name of the environment variable that overrides them. Makes use of the `appname
229 <platformdirs.api.PlatformDirsABC.appname>`, `version <platformdirs.api.PlatformDirsABC.version>`, `multipath
230 <platformdirs.api.PlatformDirsABC.multipath>`, `opinion <platformdirs.api.PlatformDirsABC.opinion>`, `ensure_exists
231 <platformdirs.api.PlatformDirsABC.ensure_exists>`.
233 """
235 @property
236 def user_data_dir(self) -> str:
237 """Data directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
238 return self.site_data_dir if self._use_site else super().user_data_dir
240 @property
241 def user_config_dir(self) -> str:
242 """Config directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
243 return self.site_config_dir if self._use_site else super().user_config_dir
245 @property
246 def user_cache_dir(self) -> str:
247 """Cache directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
248 return self.site_cache_dir if self._use_site else super().user_cache_dir
250 @property
251 def user_state_dir(self) -> str:
252 """State directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
253 return self.site_state_dir if self._use_site else super().user_state_dir
255 @property
256 def user_log_dir(self) -> str:
257 """Log directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
258 return self.site_log_dir if self._use_site else super().user_log_dir
260 @property
261 def user_applications_dir(self) -> str:
262 """Applications directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
263 return self.site_applications_dir if self._use_site else super().user_applications_dir
265 @property
266 def user_runtime_dir(self) -> str:
267 """Runtime directory tied to the user, e.g. ``$XDG_RUNTIME_DIR/$appname/$version``.
269 Accepts ``$XDG_RUNTIME_DIR`` only as a directory, not a symlink, that the user owns with mode ``0700``, as the
270 XDG spec requires, and only below parents no other user can replace. Creates a missing one that way under
271 ``ensure_exists``. When the variable is unset or fails that check, emits one
272 :class:`~platformdirs.RuntimeDirWarning` per cause and falls back to the platform default
273 (``/tmp/run/user/<uid>`` on OpenBSD, ``/var/run/user/<uid>`` on FreeBSD/NetBSD, ``/run/user/<uid>`` elsewhere)
274 if it passes the same check, else to ``runtime-<uid>`` in the temporary directory. Root with
275 ``use_site_for_root`` gets the site equivalent.
277 :raises PermissionError: if the temporary fallback is a symlink, not a directory, owned by another user, or
278 missing right after its creation.
280 """
281 if self._use_site:
282 # XDGMixin.site_runtime_dir reads $XDG_RUNTIME_DIR, which belongs to the user who started the root process.
283 return super(XDGMixin, self).site_runtime_dir
284 if not (path := _xdg_dir("XDG_RUNTIME_DIR")):
285 reason = "XDG_RUNTIME_DIR is not set"
286 else:
287 # Creating the missing levels follows whatever symlink sits at a parent, so check the parents first.
288 if not (skipped := bool(_runtime_dir_parent_problem(path))):
289 with suppress(OSError):
290 self._optionally_create_directory(path, private=True)
291 # A leaf left missing because the parent check skipped creation is reported as that parent problem.
292 if not (
293 problem := _runtime_dir_problem(path, missing_ok=skipped or not self.ensure_exists)
294 or _runtime_dir_parent_problem(path)
295 ):
296 return self._append_app_name_and_version(path, private=True)
297 reason = f"XDG_RUNTIME_DIR {problem}"
298 _warn_once(f"{reason}, falling back to {(fallback := self._default_runtime_dir())}")
299 return self._append_app_name_and_version(fallback, private=True)
301 def place_runtime_file(self, name: str | os.PathLike[str]) -> Path:
302 """Like :meth:`~platformdirs.api.PlatformDirsABC.place_runtime_file`.
304 Creates the runtime directory before checking it, so another user cannot create it in between.
306 :raises PermissionError: if the temporary fallback is a symlink, not a directory, owned by another user, or
307 missing right after its creation.
309 """
311 def created(lookup: PlatformDirsABC) -> Path:
312 lookup.ensure_exists = True
313 return lookup.user_runtime_path
315 return self._place_file(created, name)
317 def _find_runtime_files(self, name: str | os.PathLike[str]) -> Iterator[Path]:
318 return filter(_in_own_runtime_dir, super()._find_runtime_files(name))
320 @property
321 def user_bin_dir(self) -> str:
322 """Bin directory tied to the user, or site equivalent when root with ``use_site_for_root``."""
323 return self.site_bin_dir if self._use_site else super().user_bin_dir
325 @property
326 def user_fonts_dir(self) -> str:
327 """Fonts directory tied to the user, ignoring ``$XDG_DATA_HOME`` when root with ``use_site_for_root``."""
328 return super(XDGMixin, self).user_fonts_dir if self._use_site else super().user_fonts_dir
330 def _xdg_media_dir(self, env_var: str) -> str | None:
331 # The XDG_*_DIR variables and user-dirs.dirs belong to the user who started the root process.
332 return None if self._use_site else super()._xdg_media_dir(env_var)
334 def _user_media_dir(self, key: str, default: str) -> str:
335 return _expand_user(default) if self._use_site else super()._user_media_dir(key, default)
337 @property
338 def user_data_path(self) -> Path:
339 """Data path tied to the user, or the first site entry when root with ``use_site_for_root``."""
340 return self.site_data_path if self._use_site else super().user_data_path
342 @property
343 def user_config_path(self) -> Path:
344 """Config path tied to the user, or the first site entry when root with ``use_site_for_root``."""
345 return self.site_config_path if self._use_site else super().user_config_path
347 @property
348 def user_preference_path(self) -> Path:
349 """Preference path tied to the user, or the first site config entry when root with ``use_site_for_root``."""
350 return self.site_config_path if self._use_site else super().user_preference_path
352 @property
353 def user_applications_path(self) -> Path:
354 """Applications path tied to the user, or the first site entry when root with ``use_site_for_root``."""
355 return self.site_applications_path if self._use_site else super().user_applications_path
358_USER_DIRS_LINE: Final = re.compile(
359 r"""
360 [ \t]*(?P<key>\w+)[ \t]*=[ \t]* # KEY=, blanks allowed around the key and after the =
361 (?:
362 "(?P<quoted>(?:[^"\\]|\\.)*)" # a double-quoted value with backslash escapes, text after it ignored
363 |(?P<bare>[^"\s](?:[^ \t\n]|[ \t]+[^ \t\n\#])*) # or an unquoted value; blanks join it only before a word
364 # that starts no comment, and each run is matched once so
365 # long runs stay linear
366 )
367 """,
368 re.VERBOSE,
369)
372def _get_user_dirs_folder(key: str) -> str | None:
373 """Return directory from user-dirs.dirs config file.
375 ``xdg-user-dirs-update`` writes shell assignments, so match each line like ``xdg-user-dir`` does and keep the last
376 valid assignment to ``key``.
378 See https://freedesktop.org/wiki/Software/xdg-user-dirs/.
380 """
381 config_home = _xdg_dir("XDG_CONFIG_HOME") or _expand_user("~/.config")
382 user_dirs_config_path = Path(config_home) / "user-dirs.dirs"
383 folder = None
384 try:
385 # the file holds raw path bytes, and surrogateescape round-trips undecodable ones like os.fsdecode
386 with user_dirs_config_path.open(encoding=sys.getfilesystemencoding(), errors="surrogateescape") as stream:
387 for line in stream:
388 if (entry := _USER_DIRS_LINE.match(line)) and entry["key"] == key:
389 folder = _resolve_user_dirs_value(entry) or folder
390 except OSError: # a missing or unreadable file leaves the defaults in place
391 return None
392 return folder
395def _resolve_user_dirs_value(entry: re.Match[str]) -> str | None:
396 value = entry["bare"] if entry["quoted"] is None else entry["quoted"]
397 if value == "$HOME" or value.startswith("$HOME/"):
398 # A home of / would give "//Desktop", which pathlib keeps as a distinct root.
399 prefix, value = _expand_user("~").rstrip("/"), value.removeprefix("$HOME")
400 elif value.startswith("/"):
401 prefix = ""
402 else:
403 return None
404 if entry["quoted"] is not None:
405 # xdg-user-dirs-update backslash-escapes $, `, " and \ inside the quotes.
406 value = re.sub(r'\\([$`"\\])', r"\1", value)
407 return f"{prefix}{value}" or "/"
410def _temp_runtime_fallback() -> str:
411 # A temporary directory of / would give "//runtime-<uid>", which pathlib keeps as a distinct root.
412 return posixpath.join(gettempdir(), f"runtime-{geteuid()}")
415def _in_own_runtime_dir(path: Path) -> bool:
416 # The lookup let a missing runtime directory pass, so another user may have created it since. The fallback goes
417 # first because a rejected XDG_RUNTIME_DIR, such as /tmp, contains it.
418 for base in (_temp_runtime_fallback(), _xdg_dir("XDG_RUNTIME_DIR")):
419 if base and path.is_relative_to(base):
420 return not _runtime_dir_problem(base, missing_ok=False, check_mode=False)
421 return True
424def _runtime_dir_problem(path: str, *, missing_ok: bool, check_mode: bool = True) -> str | None:
425 """Return why ``path`` fails the checks of Qt's ``checkXdgRuntimeDir``, or ``None`` if it passes.
427 A missing ``path`` passes under ``missing_ok``. A caller that then creates it or reads below it has to check again,
428 since another user can create it in between.
430 """
431 try:
432 # A trailing / or /. makes lstat follow a final symlink, while Path, which creates the directory, drops it.
433 info = os.lstat(str(Path(path)))
434 except FileNotFoundError:
435 return None if missing_ok else f"{path} is missing"
436 except OSError as error:
437 return f"{path} cannot be checked: {error.strerror}"
438 mode = info.st_mode & 0o777
439 checks = (
440 (stat.S_ISLNK(info.st_mode), "is a symlink"),
441 (not stat.S_ISDIR(info.st_mode), "is not a directory"),
442 (info.st_uid != (uid := geteuid()), f"is owned by uid {info.st_uid}, not {uid}"),
443 (check_mode and mode != _RUNTIME_DIR_MODE, f"has mode {mode:04o}, not {_RUNTIME_DIR_MODE:04o}"),
444 )
445 return next((f"{path} {problem}" for failed, problem in checks if failed), None)
448def _runtime_dir_parent_problem(path: str) -> str | None:
449 if sys.platform == "win32": # every directory reports as writable by everyone there
450 return None
451 in_shared_dir = False
452 for parent in reversed(Path(path).parents):
453 try:
454 if in_shared_dir and (owner := os.lstat(parent).st_uid) not in {0, geteuid()}:
455 return f"{path} is below {parent}, owned by uid {owner}"
456 mode = parent.stat().st_mode # a symlink that got past the owner check is judged by its target
457 except OSError:
458 return None # the missing rest gets created, or _runtime_dir_problem reports the path itself
459 if (in_shared_dir := bool(mode & stat.S_IWOTH)) and not mode & stat.S_ISVTX:
460 return f"{path} is below {parent}, writable by every user"
461 return None
464def _warn_once(message: str) -> None:
465 # Each read of user_runtime_dir repeats the check, so a warning per read would flood long-running programs.
466 if message not in _WARNED:
467 _WARNED.add(message)
468 warnings.warn(message, RuntimeDirWarning, stacklevel=3)
471_WARNED: Final[set[str]] = set()
472_RUNTIME_DIR_MODE: Final = 0o700
475__all__ = [
476 "Unix",
477]