Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pip/_vendor/platformdirs/unix.py: 51%

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

215 statements  

1"""Unix.""" 

2 

3from __future__ import annotations 

4 

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 

14 

15from ._xdg import XDGMixin, _expand_user, _xdg_dir 

16from .api import PlatformDirsABC, RuntimeDirWarning 

17 

18if sys.platform == "win32": 

19 

20 def getuid() -> NoReturn: 

21 msg = "should only be used on Unix" 

22 raise RuntimeError(msg) 

23 

24else: 

25 from os import getuid 

26 

27 

28class _UnixDefaults(PlatformDirsABC): # ruff:ignore[too-many-public-methods] 

29 """Default directories for Unix/Linux without XDG environment variable overrides. 

30 

31 The XDG env var handling is in :class:`~platformdirs._xdg.XDGMixin`. 

32 

33 """ 

34 

35 @property 

36 def _use_site(self) -> bool: 

37 return self.use_site_for_root and getuid() == 0 

38 

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) 

43 

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")] 

47 

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) 

52 

53 @property 

54 def _site_config_dirs(self) -> list[str]: 

55 return [self._join_app_name_and_version("/etc/xdg")] 

56 

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) 

61 

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) 

66 

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) 

71 

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) 

76 

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 

85 

86 @property 

87 def site_log_dir(self) -> str: 

88 """Log directory shared by users, e.g. ``/var/log/$appname/$version``. 

89 

90 Unlike `user_log_dir`, ``opinion`` has no effect since ``/var/log`` is inherently a log directory. 

91 

92 """ 

93 return self._append_app_name_and_version("/var/log", private=False) 

94 

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") 

99 

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") 

104 

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") 

109 

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") 

114 

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") 

119 

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") 

124 

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") 

129 

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") 

134 

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") 

139 

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) 

145 

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" 

150 

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 

155 

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") 

160 

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" 

165 

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" 

170 

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] 

174 

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() 

183 

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 

196 

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``. 

200 

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. 

203 

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. 

206 

207 For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/$appname/$version`` if ``$XDG_RUNTIME_DIR`` is not set. 

208 

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) 

215 

216 

217class Unix(XDGMixin, _UnixDefaults): 

218 """On Unix/Linux, we follow the `XDG Basedir Spec <https://specifications.freedesktop.org/basedir/latest/>`_. 

219 

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>`. 

225 

226 """ 

227 

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 

232 

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 

237 

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 

242 

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 

247 

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 

252 

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 

257 

258 @property 

259 def user_runtime_dir(self) -> str: 

260 """Runtime directory tied to the user, e.g. ``$XDG_RUNTIME_DIR/$appname/$version``. 

261 

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. 

268 

269 :raises PermissionError: if the temporary fallback is a symlink, not a directory, or owned by another user. 

270 

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) 

285 

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 

290 

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 

295 

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 

300 

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 

305 

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 

310 

311 

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) 

323 

324 

325def _get_user_dirs_folder(key: str) -> str | None: 

326 """Return directory from user-dirs.dirs config file. 

327 

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``. 

330 

331 See https://freedesktop.org/wiki/Software/xdg-user-dirs/. 

332 

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 if not user_dirs_config_path.exists(): 

337 return None 

338 folder = None 

339 # the file holds raw path bytes, and surrogateescape round-trips undecodable ones like os.fsdecode 

340 with user_dirs_config_path.open(encoding=sys.getfilesystemencoding(), errors="surrogateescape") as stream: 

341 for line in stream: 

342 if (entry := _USER_DIRS_LINE.match(line)) and entry["key"] == key: 

343 folder = _resolve_user_dirs_value(entry) or folder 

344 return folder 

345 

346 

347def _resolve_user_dirs_value(entry: re.Match[str]) -> str | None: 

348 value = entry["bare"] if entry["quoted"] is None else entry["quoted"] 

349 if value == "$HOME" or value.startswith("$HOME/"): 

350 prefix, value = _expand_user("~"), value.removeprefix("$HOME") 

351 elif value.startswith("/"): 

352 prefix = "" 

353 else: 

354 return None 

355 if entry["quoted"] is not None: 

356 # xdg-user-dirs-update backslash-escapes $, `, " and \ inside the quotes. 

357 value = re.sub(r"\\(.)", r"\1", value) 

358 return prefix + value 

359 

360 

361def _runtime_dir_problem(path: str, *, check_mode: bool = True) -> str | None: 

362 """Return why ``path`` fails the checks of Qt's ``checkXdgRuntimeDir``, or ``None`` if it passes or is missing.""" 

363 try: 

364 info = os.lstat(path) 

365 except FileNotFoundError: 

366 return None 

367 except OSError as error: 

368 return f"{path} cannot be checked: {error.strerror}" 

369 mode = info.st_mode & 0o777 

370 checks = ( 

371 (stat.S_ISLNK(info.st_mode), "is a symlink"), 

372 (not stat.S_ISDIR(info.st_mode), "is not a directory"), 

373 (info.st_uid != (uid := getuid()), f"is owned by uid {info.st_uid}, not {uid}"), 

374 (check_mode and mode != _RUNTIME_DIR_MODE, f"has mode {mode:04o}, not {_RUNTIME_DIR_MODE:04o}"), 

375 ) 

376 return next((f"{path} {problem}" for failed, problem in checks if failed), None) 

377 

378 

379def _warn_once(message: str) -> None: 

380 # Each read of user_runtime_dir repeats the check, so a warning per read would flood long-running programs. 

381 if message not in _WARNED: 

382 _WARNED.add(message) 

383 warnings.warn(message, RuntimeDirWarning, stacklevel=3) 

384 

385 

386_WARNED: Final[set[str]] = set() 

387_RUNTIME_DIR_MODE: Final = 0o700 

388 

389 

390__all__ = [ 

391 "Unix", 

392]