Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/platformdirs/api.py: 9%

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

390 statements  

1"""Base API.""" 

2 

3from __future__ import annotations 

4 

5import os 

6from abc import ABC, abstractmethod 

7from contextlib import suppress 

8from copy import copy 

9from pathlib import Path 

10from typing import TYPE_CHECKING 

11 

12if TYPE_CHECKING: 

13 from collections.abc import Callable, Iterable, Iterator 

14 from typing import Literal 

15 

16 

17class PlatformDirsABC(ABC): # ruff:ignore[too-many-public-methods] 

18 """Abstract base class defining all platform directory properties, their :class:`~pathlib.Path` variants, and iterators. 

19 

20 Platform-specific subclasses (e.g. :class:`~platformdirs.windows.Windows`, :class:`~platformdirs.macos.MacOS`, 

21 :class:`~platformdirs.unix.Unix`) implement the abstract properties to return the appropriate paths for each 

22 operating system. 

23 

24 """ 

25 

26 def __init__( # ruff:ignore[too-many-arguments, too-many-positional-arguments] 

27 self, 

28 appname: str | None = None, 

29 appauthor: str | Literal[False] | None = None, 

30 version: str | None = None, 

31 roaming: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] 

32 multipath: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] 

33 opinion: bool = True, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] 

34 ensure_exists: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] 

35 use_site_for_root: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument] 

36 ) -> None: 

37 """Create a new platform directory. 

38 

39 :param appname: See `appname`. 

40 :param appauthor: See `appauthor`. 

41 :param version: See `version`. 

42 :param roaming: See `roaming`. 

43 :param multipath: See `multipath`. 

44 :param opinion: See `opinion`. 

45 :param ensure_exists: See `ensure_exists`. 

46 :param use_site_for_root: See `use_site_for_root`. 

47 

48 """ 

49 self.appname = appname 

50 self.appauthor = appauthor 

51 self.version = version 

52 self.roaming = roaming 

53 """Whether to use the roaming appdata directory on Windows. 

54 

55 That means that for users on a Windows network setup for roaming profiles, this user data will be synced on 

56 login (see `here <https://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>`_). 

57 

58 """ 

59 self.multipath = multipath 

60 """An optional parameter which indicates that the entire list of data dirs should be returned. 

61 

62 When ``False``, only the first item is returned. Affects ``site_data_dir``, ``site_config_dir`` and 

63 ``site_applications_dir`` on Unix and macOS, and ``site_cache_dir`` on macOS under Homebrew. 

64 

65 """ 

66 self.opinion = opinion 

67 """Whether to use opinionated values. 

68 

69 When enabled, appends an additional subdirectory for certain directories: e.g. ``Cache`` for cache and ``Logs`` 

70 for logs on Windows, ``log`` for logs on Unix. 

71 

72 """ 

73 self.ensure_exists = ensure_exists 

74 """Create the directory a property returns, with missing parents, each time the property is read. 

75 

76 Media directories qualify only when the user set them through XDG. platformdirs never creates fonts, bin, 

77 applications or default media directories. Off by default. 

78 

79 """ 

80 self.use_site_for_root = use_site_for_root 

81 """Whether to redirect ``user_*_dir`` calls to their ``site_*_dir`` equivalents when running as root (uid 0). 

82 

83 Only has an effect on Unix. Disabled by default for backwards compatibility. When enabled, XDG user environment 

84 variables (e.g. ``XDG_DATA_HOME``) are bypassed for the redirected directories. 

85 

86 """ 

87 

88 @property 

89 def appname(self) -> str | None: 

90 """The name of the application.""" 

91 return self._appname 

92 

93 @appname.setter 

94 def appname(self, value: str | None) -> None: 

95 _ensure_inside_base("appname", value) 

96 self._appname = value 

97 

98 @property 

99 def appauthor(self) -> str | Literal[False] | None: 

100 """The name of the app author or distributing body for this application. 

101 

102 Typically, it is the owning company name. Defaults to `appname`. You may pass ``False`` to disable it. 

103 

104 .. note:: 

105 

106 On Windows, the directory structure is ``<base>/<appauthor>/<appname>``. When ``appauthor`` is ``None`` (the 

107 default), it falls back to ``appname``, resulting in ``<base>/<appname>/<appname>`` (e.g. 

108 ``AppData/Local/myapp/myapp``). Pass ``appauthor=False`` to omit the author directory entirely and get 

109 ``<base>/<appname>``. 

110 

111 """ 

112 return self._appauthor 

113 

114 @appauthor.setter 

115 def appauthor(self, value: str | Literal[False] | None) -> None: 

116 _ensure_inside_base("appauthor", value) 

117 self._appauthor = value 

118 

119 @property 

120 def version(self) -> str | None: 

121 """An optional version path element to append to the path. 

122 

123 You might want to use this if you want multiple versions of your app to be able to run independently. If used, 

124 this would typically be ``<major>.<minor>``. 

125 

126 """ 

127 return self._version 

128 

129 @version.setter 

130 def version(self, value: str | None) -> None: 

131 _ensure_inside_base("version", value) 

132 self._version = value 

133 

134 def _append_app_name_and_version(self, *base: str, private: bool) -> str: 

135 path = self._join_app_name_and_version(*base) 

136 self._optionally_create_directory(path, private=private) 

137 return path 

138 

139 def _join_app_name_and_version(self, *base: str) -> str: 

140 params = list(base[1:]) 

141 if self.appname: 

142 params.append(self.appname) 

143 if self.version: 

144 params.append(self.version) 

145 return os.path.join(base[0], *params) # ruff:ignore[os-path-join] 

146 

147 def _optionally_create_directory(self, path: str, *, private: bool) -> None: 

148 if not self.ensure_exists: 

149 return 

150 _ensure_home_known(path) 

151 if private: 

152 _make_private_directories(Path(path)) 

153 else: 

154 Path(path).mkdir(parents=True, exist_ok=True) 

155 

156 def _optionally_create_media_directory(self, path: str) -> None: 

157 # Only called for a media directory the user configured. Media directories are often symlinks to removable or 

158 # network storage, so a dangling link is returned as-is, like before ensure_exists covered them. 

159 with suppress(FileExistsError): 

160 self._optionally_create_directory(path, private=False) 

161 

162 def _select_site_dirs(self, dirs: list[str]) -> str: 

163 # Site lists are built without touching the disk, so ensure_exists only creates what the caller gets back. 

164 selected = dirs if self.multipath else dirs[:1] 

165 for path in selected: 

166 self._optionally_create_directory(path, private=False) 

167 return os.pathsep.join(selected) 

168 

169 def _create_as_yielded(self, dirs: Iterable[str]) -> Iterator[str]: 

170 for path in dirs: 

171 self._optionally_create_directory(path, private=False) 

172 yield path 

173 

174 def _first_site_dir_as_path(self, dirs: list[str]) -> Path: 

175 # A *_path property always returns the first entry, regardless of multipath, so only that entry is created. 

176 path = dirs[0] 

177 self._optionally_create_directory(path, private=False) 

178 return Path(path) 

179 

180 @property 

181 @abstractmethod 

182 def user_data_dir(self) -> str: 

183 """Data directory tied to the user.""" 

184 

185 @property 

186 @abstractmethod 

187 def site_data_dir(self) -> str: 

188 """Data directory shared by users.""" 

189 

190 @property 

191 def _site_data_dirs(self) -> list[str]: 

192 return [self.site_data_dir] 

193 

194 @property 

195 @abstractmethod 

196 def user_config_dir(self) -> str: 

197 """Config directory tied to the user.""" 

198 

199 @property 

200 @abstractmethod 

201 def site_config_dir(self) -> str: 

202 """Config directory shared by users.""" 

203 

204 @property 

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

206 return [self.site_config_dir] 

207 

208 @property 

209 @abstractmethod 

210 def user_cache_dir(self) -> str: 

211 """Cache directory tied to the user.""" 

212 

213 @property 

214 @abstractmethod 

215 def site_cache_dir(self) -> str: 

216 """Cache directory shared by users.""" 

217 

218 @property 

219 def _site_cache_dirs(self) -> list[str]: 

220 return [self.site_cache_dir] 

221 

222 @property 

223 @abstractmethod 

224 def user_state_dir(self) -> str: 

225 """State directory tied to the user.""" 

226 

227 @property 

228 @abstractmethod 

229 def site_state_dir(self) -> str: 

230 """State directory shared by users.""" 

231 

232 @property 

233 @abstractmethod 

234 def user_log_dir(self) -> str: 

235 """Log directory tied to the user.""" 

236 

237 @property 

238 @abstractmethod 

239 def site_log_dir(self) -> str: 

240 """Log directory shared by users.""" 

241 

242 @property 

243 @abstractmethod 

244 def user_documents_dir(self) -> str: 

245 """Documents directory tied to the user.""" 

246 

247 @property 

248 @abstractmethod 

249 def user_downloads_dir(self) -> str: 

250 """Downloads directory tied to the user.""" 

251 

252 @property 

253 @abstractmethod 

254 def user_pictures_dir(self) -> str: 

255 """Pictures directory tied to the user.""" 

256 

257 @property 

258 @abstractmethod 

259 def user_videos_dir(self) -> str: 

260 """Videos directory tied to the user.""" 

261 

262 @property 

263 @abstractmethod 

264 def user_music_dir(self) -> str: 

265 """Music directory tied to the user.""" 

266 

267 @property 

268 @abstractmethod 

269 def user_desktop_dir(self) -> str: 

270 """Desktop directory tied to the user.""" 

271 

272 @property 

273 @abstractmethod 

274 def user_projects_dir(self) -> str: 

275 """Projects directory tied to the user.""" 

276 

277 @property 

278 @abstractmethod 

279 def user_publicshare_dir(self) -> str: 

280 """Public share directory tied to the user.""" 

281 

282 @property 

283 @abstractmethod 

284 def user_templates_dir(self) -> str: 

285 """Templates directory tied to the user.""" 

286 

287 @property 

288 @abstractmethod 

289 def user_fonts_dir(self) -> str: 

290 """Fonts directory tied to the user.""" 

291 

292 @property 

293 @abstractmethod 

294 def user_preference_dir(self) -> str: 

295 """Preference directory tied to the user.""" 

296 

297 @property 

298 @abstractmethod 

299 def user_bin_dir(self) -> str: 

300 """Bin directory tied to the user.""" 

301 

302 @property 

303 @abstractmethod 

304 def site_bin_dir(self) -> str: 

305 """Bin directory shared by users.""" 

306 

307 @property 

308 @abstractmethod 

309 def user_applications_dir(self) -> str: 

310 """Applications directory tied to the user.""" 

311 

312 @property 

313 @abstractmethod 

314 def site_applications_dir(self) -> str: 

315 """Applications directory shared by users.""" 

316 

317 @property 

318 def _site_applications_dirs(self) -> list[str]: 

319 return [self.site_applications_dir] 

320 

321 @property 

322 @abstractmethod 

323 def user_runtime_dir(self) -> str: 

324 """Runtime directory tied to the user.""" 

325 

326 @property 

327 @abstractmethod 

328 def site_runtime_dir(self) -> str: 

329 """Runtime directory shared by users.""" 

330 

331 @property 

332 def user_data_path(self) -> Path: 

333 """Data path tied to the user.""" 

334 return Path(self.user_data_dir) 

335 

336 @property 

337 def site_data_path(self) -> Path: 

338 """Data path shared by users. Only return the first item, even if ``multipath`` is set to ``True``.""" 

339 return self._first_site_dir_as_path(self._site_data_dirs) 

340 

341 @property 

342 def user_config_path(self) -> Path: 

343 """Config path tied to the user.""" 

344 return Path(self.user_config_dir) 

345 

346 @property 

347 def site_config_path(self) -> Path: 

348 """Config path shared by users. Only return the first item, even if ``multipath`` is set to ``True``.""" 

349 return self._first_site_dir_as_path(self._site_config_dirs) 

350 

351 @property 

352 def user_cache_path(self) -> Path: 

353 """Cache path tied to the user.""" 

354 return Path(self.user_cache_dir) 

355 

356 @property 

357 def site_cache_path(self) -> Path: 

358 """Cache path shared by users. Only return the first item, even if ``multipath`` is set to ``True``.""" 

359 return self._first_site_dir_as_path(self._site_cache_dirs) 

360 

361 @property 

362 def user_state_path(self) -> Path: 

363 """State path tied to the user.""" 

364 return Path(self.user_state_dir) 

365 

366 @property 

367 def site_state_path(self) -> Path: 

368 """State path shared by users.""" 

369 return Path(self.site_state_dir) 

370 

371 @property 

372 def user_log_path(self) -> Path: 

373 """Log path tied to the user.""" 

374 return Path(self.user_log_dir) 

375 

376 @property 

377 def site_log_path(self) -> Path: 

378 """Log path shared by users.""" 

379 return Path(self.site_log_dir) 

380 

381 @property 

382 def user_documents_path(self) -> Path: 

383 """Documents path tied to the user.""" 

384 return Path(self.user_documents_dir) 

385 

386 @property 

387 def user_downloads_path(self) -> Path: 

388 """Downloads path tied to the user.""" 

389 return Path(self.user_downloads_dir) 

390 

391 @property 

392 def user_pictures_path(self) -> Path: 

393 """Pictures path tied to the user.""" 

394 return Path(self.user_pictures_dir) 

395 

396 @property 

397 def user_videos_path(self) -> Path: 

398 """Videos path tied to the user.""" 

399 return Path(self.user_videos_dir) 

400 

401 @property 

402 def user_music_path(self) -> Path: 

403 """Music path tied to the user.""" 

404 return Path(self.user_music_dir) 

405 

406 @property 

407 def user_desktop_path(self) -> Path: 

408 """Desktop path tied to the user.""" 

409 return Path(self.user_desktop_dir) 

410 

411 @property 

412 def user_projects_path(self) -> Path: 

413 """Projects path tied to the user.""" 

414 return Path(self.user_projects_dir) 

415 

416 @property 

417 def user_publicshare_path(self) -> Path: 

418 """Public share path tied to the user.""" 

419 return Path(self.user_publicshare_dir) 

420 

421 @property 

422 def user_templates_path(self) -> Path: 

423 """Templates path tied to the user.""" 

424 return Path(self.user_templates_dir) 

425 

426 @property 

427 def user_fonts_path(self) -> Path: 

428 """Fonts path tied to the user.""" 

429 return Path(self.user_fonts_dir) 

430 

431 @property 

432 def user_preference_path(self) -> Path: 

433 """Preference path tied to the user.""" 

434 return Path(self.user_preference_dir) 

435 

436 @property 

437 def user_bin_path(self) -> Path: 

438 """Bin path tied to the user.""" 

439 return Path(self.user_bin_dir) 

440 

441 @property 

442 def site_bin_path(self) -> Path: 

443 """Bin path shared by users.""" 

444 return Path(self.site_bin_dir) 

445 

446 @property 

447 def user_applications_path(self) -> Path: 

448 """Applications path tied to the user.""" 

449 return Path(self.user_applications_dir) 

450 

451 @property 

452 def site_applications_path(self) -> Path: 

453 """Applications path shared by users. Only return the first item, even if ``multipath`` is set to ``True``.""" 

454 return Path(self._site_applications_dirs[0]) 

455 

456 @property 

457 def user_runtime_path(self) -> Path: 

458 """Runtime path tied to the user.""" 

459 return Path(self.user_runtime_dir) 

460 

461 @property 

462 def site_runtime_path(self) -> Path: 

463 """Runtime path shared by users.""" 

464 return Path(self.site_runtime_dir) 

465 

466 def iter_config_dirs(self) -> Iterator[str]: 

467 """:yield: all user and site configuration directories.""" 

468 yield from _unique(self._iter_config_dirs()) 

469 

470 def _iter_config_dirs(self) -> Iterator[str]: 

471 if not self._use_site: 

472 yield self.user_config_dir 

473 yield from self._create_as_yielded(self._site_config_dirs) 

474 

475 def iter_data_dirs(self) -> Iterator[str]: 

476 """:yield: all user and site data directories.""" 

477 yield from _unique(self._iter_data_dirs()) 

478 

479 def _iter_data_dirs(self) -> Iterator[str]: 

480 if not self._use_site: 

481 yield self.user_data_dir 

482 yield from self._create_as_yielded(self._site_data_dirs) 

483 

484 def iter_cache_dirs(self) -> Iterator[str]: 

485 """:yield: all user and site cache directories.""" 

486 yield from _unique(self._iter_cache_dirs()) 

487 

488 def _iter_cache_dirs(self) -> Iterator[str]: 

489 if not self._use_site: 

490 yield self.user_cache_dir 

491 yield from self._create_as_yielded(self._site_cache_dirs) 

492 

493 def iter_state_dirs(self) -> Iterator[str]: 

494 """:yield: all user and site state directories.""" 

495 yield from _unique(self._iter_state_dirs()) 

496 

497 def _iter_state_dirs(self) -> Iterator[str]: 

498 if not self._use_site: 

499 yield self.user_state_dir 

500 yield self.site_state_dir 

501 

502 def iter_log_dirs(self) -> Iterator[str]: 

503 """:yield: all user and site log directories.""" 

504 yield from _unique(self._iter_log_dirs()) 

505 

506 def _iter_log_dirs(self) -> Iterator[str]: 

507 if not self._use_site: 

508 yield self.user_log_dir 

509 yield self.site_log_dir 

510 

511 def iter_runtime_dirs(self) -> Iterator[str]: 

512 """:yield: all user and site runtime directories.""" 

513 yield from _unique(self._iter_runtime_dirs()) 

514 

515 def _iter_runtime_dirs(self) -> Iterator[str]: 

516 yield self.user_runtime_dir 

517 # Root's user runtime dir is already the site one; site_runtime_dir reads the invoking user's XDG_RUNTIME_DIR. 

518 if not self._use_site: 

519 yield self.site_runtime_dir 

520 

521 @property 

522 def _use_site(self) -> bool: 

523 # Only Unix redirects root's user dirs to the site ones. The iterators then skip the user dir, since under 

524 # multipath it is the joined site string that _unique cannot match against any single entry. 

525 return False 

526 

527 def iter_config_paths(self) -> Iterator[Path]: 

528 """:yield: all user and site configuration paths.""" 

529 for path in self.iter_config_dirs(): 

530 yield Path(path) 

531 

532 def iter_data_paths(self) -> Iterator[Path]: 

533 """:yield: all user and site data paths.""" 

534 for path in self.iter_data_dirs(): 

535 yield Path(path) 

536 

537 def iter_cache_paths(self) -> Iterator[Path]: 

538 """:yield: all user and site cache paths.""" 

539 for path in self.iter_cache_dirs(): 

540 yield Path(path) 

541 

542 def iter_state_paths(self) -> Iterator[Path]: 

543 """:yield: all user and site state paths.""" 

544 for path in self.iter_state_dirs(): 

545 yield Path(path) 

546 

547 def iter_log_paths(self) -> Iterator[Path]: 

548 """:yield: all user and site log paths.""" 

549 for path in self.iter_log_dirs(): 

550 yield Path(path) 

551 

552 def iter_runtime_paths(self) -> Iterator[Path]: 

553 """:yield: all user and site runtime paths.""" 

554 for path in self.iter_runtime_dirs(): 

555 yield Path(path) 

556 

557 def place_config_file(self, name: str | os.PathLike[str]) -> Path: 

558 """Return the path of ``name`` under `user_config_path`, creating the missing directories on the way. 

559 

560 Each directory it creates, including the user directory itself, gets mode ``0o700`` whether or not 

561 `ensure_exists` is set; directories that already exist keep their mode. On Unix, root with `use_site_for_root` 

562 places the file under the site directory instead and creates the missing directories with the default mode, so 

563 other users can read them. It does not create the file. 

564 

565 :param name: file path relative to the user directory, e.g. ``"sub/app.toml"``. 

566 

567 :raises ValueError: if ``name`` is empty, absolute, has a drive or climbs out through ``..``. 

568 :raises RuntimeError: if the user directory starts with ``~`` because the home directory is unknown. 

569 :raises OSError: if it cannot create a missing directory, e.g. one on a missing drive or network share. 

570 

571 """ 

572 return self._place_file(lambda dirs: dirs.user_config_path, name) 

573 

574 def place_data_file(self, name: str | os.PathLike[str]) -> Path: 

575 """Like `place_config_file`, under `user_data_path`.""" 

576 return self._place_file(lambda dirs: dirs.user_data_path, name) 

577 

578 def place_cache_file(self, name: str | os.PathLike[str]) -> Path: 

579 """Like `place_config_file`, under `user_cache_path`.""" 

580 return self._place_file(lambda dirs: dirs.user_cache_path, name) 

581 

582 def place_state_file(self, name: str | os.PathLike[str]) -> Path: 

583 """Like `place_config_file`, under `user_state_path`.""" 

584 return self._place_file(lambda dirs: dirs.user_state_path, name) 

585 

586 def place_log_file(self, name: str | os.PathLike[str]) -> Path: 

587 """Like `place_config_file`, under `user_log_path`.""" 

588 return self._place_file(lambda dirs: dirs.user_log_path, name) 

589 

590 def place_runtime_file(self, name: str | os.PathLike[str]) -> Path: 

591 """Like `place_config_file`, under `user_runtime_path`. 

592 

593 :raises PermissionError: on Unix, if another user owns the temporary fallback directory. 

594 

595 """ 

596 return self._place_file(lambda dirs: dirs.user_runtime_path, name) 

597 

598 def _place_file(self, user_path: Callable[[PlatformDirsABC], Path], name: str | os.PathLike[str]) -> Path: 

599 _ensure_inside_base("name", relative := os.fspath(name)) 

600 if not Path(relative).parts: 

601 msg = f"name must point to a file, got {relative!r}" 

602 raise ValueError(msg) 

603 # Under ensure_exists the lookup itself would create the user directory with the default mode, not 0o700. 

604 (lookup := copy(self)).ensure_exists = False 

605 # Under multipath *_dir joins the site directories with os.pathsep, so place under the single *_path. 

606 _ensure_home_known(str(directory := user_path(lookup))) 

607 path = directory / relative 

608 if self._use_site: 

609 # Site directories keep the default mode, as under ensure_exists, so other users can still read them. 

610 path.parent.mkdir(parents=True, exist_ok=True) 

611 else: 

612 _make_private_directories(path.parent) 

613 return path 

614 

615 def find_config_file(self, name: str | os.PathLike[str]) -> Path | None: 

616 """Find the first ``name`` file in :meth:`iter_config_paths` order, without creating any directory. 

617 

618 :param name: path relative to each directory, may contain subdirectories. 

619 

620 :returns: the file, or ``None`` when no directory holds one. 

621 

622 :raises ValueError: if ``name`` is absolute, has a drive or climbs out with ``..``. 

623 

624 """ 

625 return next(self._find_files(lambda dirs: dirs.iter_config_paths(), name), None) 

626 

627 def find_config_files(self, name: str | os.PathLike[str]) -> list[Path]: 

628 """Find every ``name`` file in :meth:`iter_config_paths` order, without creating any directory. 

629 

630 :param name: see :meth:`find_config_file`. 

631 

632 :returns: the files, user directory first. 

633 

634 :raises ValueError: see :meth:`find_config_file`. 

635 

636 """ 

637 return list(self._find_files(lambda dirs: dirs.iter_config_paths(), name)) 

638 

639 def find_data_file(self, name: str | os.PathLike[str]) -> Path | None: 

640 """Like :meth:`find_config_file`, searching :meth:`iter_data_paths`.""" 

641 return next(self._find_files(lambda dirs: dirs.iter_data_paths(), name), None) 

642 

643 def find_data_files(self, name: str | os.PathLike[str]) -> list[Path]: 

644 """Like :meth:`find_config_files`, searching :meth:`iter_data_paths`.""" 

645 return list(self._find_files(lambda dirs: dirs.iter_data_paths(), name)) 

646 

647 def find_cache_file(self, name: str | os.PathLike[str]) -> Path | None: 

648 """Like :meth:`find_config_file`, searching :meth:`iter_cache_paths`.""" 

649 return next(self._find_files(lambda dirs: dirs.iter_cache_paths(), name), None) 

650 

651 def find_cache_files(self, name: str | os.PathLike[str]) -> list[Path]: 

652 """Like :meth:`find_config_files`, searching :meth:`iter_cache_paths`.""" 

653 return list(self._find_files(lambda dirs: dirs.iter_cache_paths(), name)) 

654 

655 def find_state_file(self, name: str | os.PathLike[str]) -> Path | None: 

656 """Like :meth:`find_config_file`, searching :meth:`iter_state_paths`.""" 

657 return next(self._find_files(lambda dirs: dirs.iter_state_paths(), name), None) 

658 

659 def find_state_files(self, name: str | os.PathLike[str]) -> list[Path]: 

660 """Like :meth:`find_config_files`, searching :meth:`iter_state_paths`.""" 

661 return list(self._find_files(lambda dirs: dirs.iter_state_paths(), name)) 

662 

663 def find_log_file(self, name: str | os.PathLike[str]) -> Path | None: 

664 """Like :meth:`find_config_file`, searching :meth:`iter_log_paths`.""" 

665 return next(self._find_files(lambda dirs: dirs.iter_log_paths(), name), None) 

666 

667 def find_log_files(self, name: str | os.PathLike[str]) -> list[Path]: 

668 """Like :meth:`find_config_files`, searching :meth:`iter_log_paths`.""" 

669 return list(self._find_files(lambda dirs: dirs.iter_log_paths(), name)) 

670 

671 def find_runtime_file(self, name: str | os.PathLike[str]) -> Path | None: 

672 """Like :meth:`find_config_file`, searching :meth:`iter_runtime_paths`.""" 

673 return next(self._find_files(lambda dirs: dirs.iter_runtime_paths(), name), None) 

674 

675 def find_runtime_files(self, name: str | os.PathLike[str]) -> list[Path]: 

676 """Like :meth:`find_config_files`, searching :meth:`iter_runtime_paths`.""" 

677 return list(self._find_files(lambda dirs: dirs.iter_runtime_paths(), name)) 

678 

679 def _find_files( 

680 self, iter_paths: Callable[[PlatformDirsABC], Iterator[Path]], name: str | os.PathLike[str] 

681 ) -> Iterator[Path]: 

682 _ensure_inside_base("name", relative := os.fspath(name)) 

683 # The iterators create each directory they yield under ensure_exists, and a lookup must only read the disk. 

684 (lookup := copy(self)).ensure_exists = False 

685 return (path for directory in iter_paths(lookup) if (path := directory / relative).is_file()) 

686 

687 

688def _ensure_inside_base(name: str, value: str | Literal[False] | None) -> None: 

689 if not value: 

690 return 

691 # os.path.join discards the base when a later component has a root or a drive, and ``..`` climbs out of it. 

692 drive, tail = os.path.splitdrive(value) 

693 if drive or tail.startswith(("/", "\\")) or ".." in tail.replace("\\", "/").split("/"): 

694 msg = f"{name} must stay inside the base directory, got {value!r}" 

695 raise ValueError(msg) 

696 

697 

698def _ensure_home_known(path: str) -> None: 

699 # expanduser leaves "~" when the home is unknown, and creating it would make a "~" directory in the cwd. 

700 if path.startswith("~"): 

701 msg = f"could not determine the home directory, refusing to create {path!r}" 

702 raise RuntimeError(msg) 

703 

704 

705def _unique(dirs: Iterable[str]) -> Iterator[str]: 

706 """:yield: ``dirs`` in order, skipping any directory already yielded.""" 

707 # Lazy on purpose: under ensure_exists reading a site_*_dir creates it, so draining ``dirs`` up front would 

708 # create directories for a caller that stops after the first entry. 

709 seen: set[str] = set() 

710 for path in dirs: 

711 if path not in seen: 

712 seen.add(path) 

713 yield path 

714 

715 

716def _make_private_directories(path: Path) -> None: 

717 # XDG asks for 0o700 on each directory created on the way, while Path.mkdir(parents=True) applies it to the last. 

718 try: 

719 path.mkdir(mode=0o700, exist_ok=True) 

720 except FileNotFoundError: 

721 if path.parent == path: # a missing drive or network share has no parent to create 

722 raise 

723 _make_private_directories(path.parent) 

724 path.mkdir(mode=0o700, exist_ok=True) 

725 

726 

727class RuntimeDirWarning(UserWarning): 

728 """Emitted when the Unix :attr:`~platformdirs.unix.Unix.user_runtime_dir` cannot use ``$XDG_RUNTIME_DIR``."""