Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/platformdirs/api.py: 55%
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"""Base API."""
3from __future__ import annotations
5import os
6from abc import ABC, abstractmethod
7from copy import copy
8from pathlib import Path
9from typing import TYPE_CHECKING
11if TYPE_CHECKING:
12 from collections.abc import Callable, Iterable, Iterator
13 from typing import Literal
16class PlatformDirsABC(ABC): # ruff:ignore[too-many-public-methods]
17 """Abstract base class defining all platform directory properties, their :class:`~pathlib.Path` variants, and iterators.
19 Platform-specific subclasses (e.g. :class:`~platformdirs.windows.Windows`, :class:`~platformdirs.macos.MacOS`,
20 :class:`~platformdirs.unix.Unix`) implement the abstract properties to return the appropriate paths for each
21 operating system.
23 """
25 def __init__( # ruff:ignore[too-many-arguments, too-many-positional-arguments]
26 self,
27 appname: str | None = None,
28 appauthor: str | Literal[False] | None = None,
29 version: str | None = None,
30 roaming: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument]
31 multipath: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument]
32 opinion: bool = True, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument]
33 ensure_exists: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument]
34 use_site_for_root: bool = False, # ruff:ignore[boolean-type-hint-positional-argument, boolean-default-value-positional-argument]
35 ) -> None:
36 """Create a new platform directory.
38 :param appname: See `appname`.
39 :param appauthor: See `appauthor`.
40 :param version: See `version`.
41 :param roaming: See `roaming`.
42 :param multipath: See `multipath`.
43 :param opinion: See `opinion`.
44 :param ensure_exists: See `ensure_exists`.
45 :param use_site_for_root: See `use_site_for_root`.
47 """
48 self.appname = appname
49 self.appauthor = appauthor
50 self.version = version
51 self.roaming = roaming
52 """Whether to use the roaming appdata directory on Windows.
54 That means that for users on a Windows network setup for roaming profiles, this user data will be synced on
55 login (see `here <https://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>`_).
57 """
58 self.multipath = multipath
59 """An optional parameter which indicates that the entire list of data dirs should be returned.
61 When ``False``, only the first item is returned. Affects ``site_data_dir``, ``site_config_dir`` and
62 ``site_applications_dir`` on Unix and macOS, and ``site_cache_dir`` on macOS under Homebrew. All but
63 ``site_applications_dir`` raise :class:`ValueError` under it when `appname` or `version` contains
64 :data:`os.pathsep`.
66 """
67 self.opinion = opinion
68 """Whether to use opinionated values.
70 When enabled, appends an additional subdirectory for certain directories: e.g. ``Cache`` for cache and ``Logs``
71 for logs on Windows, ``log`` for logs on Unix.
73 """
74 self.ensure_exists = ensure_exists
75 """Create the directory a property returns, with missing parents, each time the property is read.
77 Media directories qualify only when the user set them through XDG. platformdirs never creates fonts, bin,
78 applications or default media directories. Off by default.
80 """
81 self.use_site_for_root = use_site_for_root
82 """Whether to redirect ``user_*_dir`` calls to their ``site_*_dir`` equivalents when the effective uid is 0.
84 Only has an effect on Unix. Disabled by default for backwards compatibility. When enabled, XDG user environment
85 variables (e.g. ``XDG_DATA_HOME``) are bypassed for the redirected directories.
87 """
89 @property
90 def appname(self) -> str | None:
91 """The name of the application."""
92 return self._appname
94 @appname.setter
95 def appname(self, value: str | None) -> None:
96 _ensure_inside_base("appname", value)
97 self._appname = value
99 @property
100 def appauthor(self) -> str | Literal[False] | None:
101 """The name of the app author or distributing body for this application.
103 Typically, it is the owning company name. Defaults to `appname`. You may pass ``False`` to disable it.
105 .. note::
107 On Windows, the directory structure is ``<base>/<appauthor>/<appname>``. When ``appauthor`` is ``None`` (the
108 default), it falls back to ``appname``, resulting in ``<base>/<appname>/<appname>`` (e.g.
109 ``AppData/Local/myapp/myapp``). Pass ``appauthor=False`` to omit the author directory entirely and get
110 ``<base>/<appname>``.
112 """
113 return self._appauthor
115 @appauthor.setter
116 def appauthor(self, value: str | Literal[False] | None) -> None:
117 _ensure_inside_base("appauthor", value)
118 self._appauthor = value
120 @property
121 def version(self) -> str | None:
122 """An optional version path element to append to the path.
124 You might want to use this if you want multiple versions of your app to be able to run independently. If used,
125 this would typically be ``<major>.<minor>``.
127 """
128 return self._version
130 @version.setter
131 def version(self, value: str | None) -> None:
132 _ensure_inside_base("version", value)
133 self._version = value
135 def _append_app_name_and_version(self, *base: str, private: bool) -> str:
136 path = self._join_app_name_and_version(*base)
137 self._optionally_create_directory(path, private=private)
138 return path
140 def _join_app_name_and_version(self, *base: str) -> str:
141 params = list(base[1:])
142 if self.appname:
143 params.append(self.appname)
144 if self.version:
145 params.append(self.version)
146 return os.path.join(base[0], *params) # ruff:ignore[os-path-join]
148 def _optionally_create_directory(self, path: str, *, private: bool) -> None:
149 if not self.ensure_exists:
150 return
151 _ensure_home_known(path)
152 if private:
153 _make_private_directories(Path(path))
154 else:
155 Path(path).mkdir(parents=True, exist_ok=True)
157 def _optionally_create_media_directory(self, path: str) -> None:
158 # Only called for a media directory the user configured. Media directories are often symlinks to removable or
159 # network storage, so a dangling link is returned as-is, like before ensure_exists covered them.
160 try:
161 self._optionally_create_directory(path, private=False)
162 except FileExistsError:
163 if not (directory := Path(path)).is_symlink() or directory.exists():
164 raise
166 def _select_site_dirs(self, dirs: list[str]) -> str:
167 # Site lists are built without touching the disk, so ensure_exists only creates what the caller gets back.
168 selected = dirs if self.multipath else dirs[:1]
169 if self.multipath and any(os.pathsep in path for path in selected):
170 # Callers split the joined string on os.pathsep, so a separator inside one entry reads as two directories.
171 msg = f"multipath cannot join site directories that contain {os.pathsep!r}, got {selected!r}"
172 raise ValueError(msg)
173 for path in selected:
174 self._optionally_create_directory(path, private=False)
175 return os.pathsep.join(selected)
177 def _create_as_yielded(self, dirs: Iterable[str]) -> Iterator[str]:
178 for path in dirs:
179 self._optionally_create_directory(path, private=False)
180 yield path
182 def _first_site_dir_as_path(self, dirs: list[str]) -> Path:
183 # A *_path property always returns the first entry, regardless of multipath, so only that entry is created.
184 path = dirs[0]
185 self._optionally_create_directory(path, private=False)
186 return Path(path)
188 @property
189 @abstractmethod
190 def user_data_dir(self) -> str:
191 """Data directory tied to the user."""
193 @property
194 @abstractmethod
195 def site_data_dir(self) -> str:
196 """Data directory shared by users."""
198 @property
199 def _site_data_dirs(self) -> list[str]:
200 return [self.site_data_dir]
202 @property
203 @abstractmethod
204 def user_config_dir(self) -> str:
205 """Config directory tied to the user."""
207 @property
208 @abstractmethod
209 def site_config_dir(self) -> str:
210 """Config directory shared by users."""
212 @property
213 def _site_config_dirs(self) -> list[str]:
214 return [self.site_config_dir]
216 @property
217 @abstractmethod
218 def user_cache_dir(self) -> str:
219 """Cache directory tied to the user."""
221 @property
222 @abstractmethod
223 def site_cache_dir(self) -> str:
224 """Cache directory shared by users."""
226 @property
227 def _site_cache_dirs(self) -> list[str]:
228 return [self.site_cache_dir]
230 @property
231 @abstractmethod
232 def user_state_dir(self) -> str:
233 """State directory tied to the user."""
235 @property
236 @abstractmethod
237 def site_state_dir(self) -> str:
238 """State directory shared by users."""
240 @property
241 @abstractmethod
242 def user_log_dir(self) -> str:
243 """Log directory tied to the user."""
245 @property
246 @abstractmethod
247 def site_log_dir(self) -> str:
248 """Log directory shared by users."""
250 @property
251 @abstractmethod
252 def user_documents_dir(self) -> str:
253 """Documents directory tied to the user."""
255 @property
256 @abstractmethod
257 def user_downloads_dir(self) -> str:
258 """Downloads directory tied to the user."""
260 @property
261 @abstractmethod
262 def user_pictures_dir(self) -> str:
263 """Pictures directory tied to the user."""
265 @property
266 @abstractmethod
267 def user_videos_dir(self) -> str:
268 """Videos directory tied to the user."""
270 @property
271 @abstractmethod
272 def user_music_dir(self) -> str:
273 """Music directory tied to the user."""
275 @property
276 @abstractmethod
277 def user_desktop_dir(self) -> str:
278 """Desktop directory tied to the user."""
280 @property
281 @abstractmethod
282 def user_projects_dir(self) -> str:
283 """Projects directory tied to the user."""
285 @property
286 @abstractmethod
287 def user_publicshare_dir(self) -> str:
288 """Public share directory tied to the user."""
290 @property
291 @abstractmethod
292 def user_templates_dir(self) -> str:
293 """Templates directory tied to the user."""
295 @property
296 @abstractmethod
297 def user_fonts_dir(self) -> str:
298 """Fonts directory tied to the user."""
300 @property
301 @abstractmethod
302 def user_preference_dir(self) -> str:
303 """Preference directory tied to the user."""
305 @property
306 @abstractmethod
307 def user_bin_dir(self) -> str:
308 """Bin directory tied to the user."""
310 @property
311 @abstractmethod
312 def site_bin_dir(self) -> str:
313 """Bin directory shared by users."""
315 @property
316 @abstractmethod
317 def user_applications_dir(self) -> str:
318 """Applications directory tied to the user."""
320 @property
321 @abstractmethod
322 def site_applications_dir(self) -> str:
323 """Applications directory shared by users."""
325 @property
326 def _site_applications_dirs(self) -> list[str]:
327 return [self.site_applications_dir]
329 @property
330 @abstractmethod
331 def user_runtime_dir(self) -> str:
332 """Runtime directory tied to the user."""
334 @property
335 @abstractmethod
336 def site_runtime_dir(self) -> str:
337 """Runtime directory shared by users."""
339 @property
340 def user_data_path(self) -> Path:
341 """Data path tied to the user."""
342 return Path(self.user_data_dir)
344 @property
345 def site_data_path(self) -> Path:
346 """Data path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
347 return self._first_site_dir_as_path(self._site_data_dirs)
349 @property
350 def user_config_path(self) -> Path:
351 """Config path tied to the user."""
352 return Path(self.user_config_dir)
354 @property
355 def site_config_path(self) -> Path:
356 """Config path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
357 return self._first_site_dir_as_path(self._site_config_dirs)
359 @property
360 def user_cache_path(self) -> Path:
361 """Cache path tied to the user."""
362 return Path(self.user_cache_dir)
364 @property
365 def site_cache_path(self) -> Path:
366 """Cache path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
367 return self._first_site_dir_as_path(self._site_cache_dirs)
369 @property
370 def user_state_path(self) -> Path:
371 """State path tied to the user."""
372 return Path(self.user_state_dir)
374 @property
375 def site_state_path(self) -> Path:
376 """State path shared by users."""
377 return Path(self.site_state_dir)
379 @property
380 def user_log_path(self) -> Path:
381 """Log path tied to the user."""
382 return Path(self.user_log_dir)
384 @property
385 def site_log_path(self) -> Path:
386 """Log path shared by users."""
387 return Path(self.site_log_dir)
389 @property
390 def user_documents_path(self) -> Path:
391 """Documents path tied to the user."""
392 return Path(self.user_documents_dir)
394 @property
395 def user_downloads_path(self) -> Path:
396 """Downloads path tied to the user."""
397 return Path(self.user_downloads_dir)
399 @property
400 def user_pictures_path(self) -> Path:
401 """Pictures path tied to the user."""
402 return Path(self.user_pictures_dir)
404 @property
405 def user_videos_path(self) -> Path:
406 """Videos path tied to the user."""
407 return Path(self.user_videos_dir)
409 @property
410 def user_music_path(self) -> Path:
411 """Music path tied to the user."""
412 return Path(self.user_music_dir)
414 @property
415 def user_desktop_path(self) -> Path:
416 """Desktop path tied to the user."""
417 return Path(self.user_desktop_dir)
419 @property
420 def user_projects_path(self) -> Path:
421 """Projects path tied to the user."""
422 return Path(self.user_projects_dir)
424 @property
425 def user_publicshare_path(self) -> Path:
426 """Public share path tied to the user."""
427 return Path(self.user_publicshare_dir)
429 @property
430 def user_templates_path(self) -> Path:
431 """Templates path tied to the user."""
432 return Path(self.user_templates_dir)
434 @property
435 def user_fonts_path(self) -> Path:
436 """Fonts path tied to the user."""
437 return Path(self.user_fonts_dir)
439 @property
440 def user_preference_path(self) -> Path:
441 """Preference path tied to the user."""
442 return Path(self.user_preference_dir)
444 @property
445 def user_bin_path(self) -> Path:
446 """Bin path tied to the user."""
447 return Path(self.user_bin_dir)
449 @property
450 def site_bin_path(self) -> Path:
451 """Bin path shared by users."""
452 return Path(self.site_bin_dir)
454 @property
455 def user_applications_path(self) -> Path:
456 """Applications path tied to the user."""
457 return Path(self.user_applications_dir)
459 @property
460 def site_applications_path(self) -> Path:
461 """Applications path shared by users. Only return the first item, even if ``multipath`` is set to ``True``."""
462 return Path(self._site_applications_dirs[0])
464 @property
465 def user_runtime_path(self) -> Path:
466 """Runtime path tied to the user."""
467 return Path(self.user_runtime_dir)
469 @property
470 def site_runtime_path(self) -> Path:
471 """Runtime path shared by users."""
472 return Path(self.site_runtime_dir)
474 def iter_config_dirs(self) -> Iterator[str]:
475 """:yield: all user and site configuration directories."""
476 yield from _unique(self._iter_config_dirs())
478 def _iter_config_dirs(self) -> Iterator[str]:
479 if not self._use_site:
480 yield self.user_config_dir
481 yield from self._create_as_yielded(self._site_config_dirs)
483 def iter_data_dirs(self) -> Iterator[str]:
484 """:yield: all user and site data directories."""
485 yield from _unique(self._iter_data_dirs())
487 def _iter_data_dirs(self) -> Iterator[str]:
488 if not self._use_site:
489 yield self.user_data_dir
490 yield from self._create_as_yielded(self._site_data_dirs)
492 def iter_cache_dirs(self) -> Iterator[str]:
493 """:yield: all user and site cache directories."""
494 yield from _unique(self._iter_cache_dirs())
496 def _iter_cache_dirs(self) -> Iterator[str]:
497 if not self._use_site:
498 yield self.user_cache_dir
499 yield from self._create_as_yielded(self._site_cache_dirs)
501 def iter_state_dirs(self) -> Iterator[str]:
502 """:yield: all user and site state directories."""
503 yield from _unique(self._iter_state_dirs())
505 def _iter_state_dirs(self) -> Iterator[str]:
506 if not self._use_site:
507 yield self.user_state_dir
508 yield self.site_state_dir
510 def iter_log_dirs(self) -> Iterator[str]:
511 """:yield: all user and site log directories."""
512 yield from _unique(self._iter_log_dirs())
514 def _iter_log_dirs(self) -> Iterator[str]:
515 if not self._use_site:
516 yield self.user_log_dir
517 yield self.site_log_dir
519 def iter_runtime_dirs(self) -> Iterator[str]:
520 """:yield: all user and site runtime directories."""
521 yield from _unique(self._iter_runtime_dirs())
523 def _iter_runtime_dirs(self) -> Iterator[str]:
524 yield self.user_runtime_dir
525 # Root's user runtime dir is already the site one; site_runtime_dir reads the invoking user's XDG_RUNTIME_DIR.
526 if not self._use_site:
527 yield self.site_runtime_dir
529 @property
530 def _use_site(self) -> bool:
531 # Only Unix redirects root's user dirs to the site ones. The iterators then skip the user dir, since under
532 # multipath it is the joined site string that _unique cannot match against any single entry.
533 return False
535 def iter_config_paths(self) -> Iterator[Path]:
536 """:yield: all user and site configuration paths."""
537 for path in self.iter_config_dirs():
538 yield Path(path)
540 def iter_data_paths(self) -> Iterator[Path]:
541 """:yield: all user and site data paths."""
542 for path in self.iter_data_dirs():
543 yield Path(path)
545 def iter_cache_paths(self) -> Iterator[Path]:
546 """:yield: all user and site cache paths."""
547 for path in self.iter_cache_dirs():
548 yield Path(path)
550 def iter_state_paths(self) -> Iterator[Path]:
551 """:yield: all user and site state paths."""
552 for path in self.iter_state_dirs():
553 yield Path(path)
555 def iter_log_paths(self) -> Iterator[Path]:
556 """:yield: all user and site log paths."""
557 for path in self.iter_log_dirs():
558 yield Path(path)
560 def iter_runtime_paths(self) -> Iterator[Path]:
561 """:yield: all user and site runtime paths."""
562 for path in self.iter_runtime_dirs():
563 yield Path(path)
565 def place_config_file(self, name: str | os.PathLike[str]) -> Path:
566 """Return the path of ``name`` under `user_config_path`, creating the missing directories on the way.
568 Each directory it creates, including the user directory itself, gets mode ``0o700`` whether or not
569 `ensure_exists` is set; directories that already exist keep their mode. On Unix, root with `use_site_for_root`
570 places the file under the site directory instead and creates the missing directories with the default mode, so
571 other users can read them. It does not create the file.
573 :param name: file path relative to the user directory, e.g. ``"sub/app.toml"``.
575 :raises ValueError: if ``name`` is empty, absolute, has a drive or climbs out through ``..``.
576 :raises RuntimeError: if the user directory starts with ``~`` because the home directory is unknown.
577 :raises OSError: if it cannot create a missing directory, e.g. one on a missing drive or network share.
579 """
580 return self._place_file(lambda dirs: dirs.user_config_path, name)
582 def place_data_file(self, name: str | os.PathLike[str]) -> Path:
583 """Like `place_config_file`, under `user_data_path`."""
584 return self._place_file(lambda dirs: dirs.user_data_path, name)
586 def place_cache_file(self, name: str | os.PathLike[str]) -> Path:
587 """Like `place_config_file`, under `user_cache_path`."""
588 return self._place_file(lambda dirs: dirs.user_cache_path, name)
590 def place_state_file(self, name: str | os.PathLike[str]) -> Path:
591 """Like `place_config_file`, under `user_state_path`."""
592 return self._place_file(lambda dirs: dirs.user_state_path, name)
594 def place_log_file(self, name: str | os.PathLike[str]) -> Path:
595 """Like `place_config_file`, under `user_log_path`."""
596 return self._place_file(lambda dirs: dirs.user_log_path, name)
598 def place_runtime_file(self, name: str | os.PathLike[str]) -> Path:
599 """Like `place_config_file`, under `user_runtime_path`.
601 :raises PermissionError: on Unix, if another user owns the temporary fallback directory.
603 """
604 return self._place_file(lambda dirs: dirs.user_runtime_path, name)
606 def _place_file(self, user_path: Callable[[PlatformDirsABC], Path], name: str | os.PathLike[str]) -> Path:
607 _ensure_inside_base("name", relative := os.fspath(name))
608 if not Path(relative).parts:
609 msg = f"name must point to a file, got {relative!r}"
610 raise ValueError(msg)
611 # Under ensure_exists the lookup itself would create the user directory with the default mode, not 0o700.
612 (lookup := copy(self)).ensure_exists = False
613 # Under multipath *_dir joins the site directories with os.pathsep, so place under the single *_path.
614 _ensure_home_known(str(directory := user_path(lookup)))
615 path = directory / relative
616 if self._use_site:
617 # Site directories keep the default mode, as under ensure_exists, so other users can still read them.
618 path.parent.mkdir(parents=True, exist_ok=True)
619 else:
620 _make_private_directories(path.parent)
621 return path
623 def find_config_file(self, name: str | os.PathLike[str]) -> Path | None:
624 """Find the first ``name`` file in :meth:`iter_config_paths` order, without creating any directory.
626 :param name: path relative to each directory, may contain subdirectories.
628 :returns: the file, or ``None`` when no directory holds one.
630 :raises ValueError: if ``name`` is absolute, has a drive or climbs out with ``..``.
632 """
633 return next(self._find_files(lambda dirs: dirs.iter_config_paths(), name), None)
635 def find_config_files(self, name: str | os.PathLike[str]) -> list[Path]:
636 """Find every ``name`` file in :meth:`iter_config_paths` order, without creating any directory.
638 :param name: see :meth:`find_config_file`.
640 :returns: the files, user directory first.
642 :raises ValueError: see :meth:`find_config_file`.
644 """
645 return list(self._find_files(lambda dirs: dirs.iter_config_paths(), name))
647 def find_data_file(self, name: str | os.PathLike[str]) -> Path | None:
648 """Like :meth:`find_config_file`, searching :meth:`iter_data_paths`."""
649 return next(self._find_files(lambda dirs: dirs.iter_data_paths(), name), None)
651 def find_data_files(self, name: str | os.PathLike[str]) -> list[Path]:
652 """Like :meth:`find_config_files`, searching :meth:`iter_data_paths`."""
653 return list(self._find_files(lambda dirs: dirs.iter_data_paths(), name))
655 def find_cache_file(self, name: str | os.PathLike[str]) -> Path | None:
656 """Like :meth:`find_config_file`, searching :meth:`iter_cache_paths`."""
657 return next(self._find_files(lambda dirs: dirs.iter_cache_paths(), name), None)
659 def find_cache_files(self, name: str | os.PathLike[str]) -> list[Path]:
660 """Like :meth:`find_config_files`, searching :meth:`iter_cache_paths`."""
661 return list(self._find_files(lambda dirs: dirs.iter_cache_paths(), name))
663 def find_state_file(self, name: str | os.PathLike[str]) -> Path | None:
664 """Like :meth:`find_config_file`, searching :meth:`iter_state_paths`."""
665 return next(self._find_files(lambda dirs: dirs.iter_state_paths(), name), None)
667 def find_state_files(self, name: str | os.PathLike[str]) -> list[Path]:
668 """Like :meth:`find_config_files`, searching :meth:`iter_state_paths`."""
669 return list(self._find_files(lambda dirs: dirs.iter_state_paths(), name))
671 def find_log_file(self, name: str | os.PathLike[str]) -> Path | None:
672 """Like :meth:`find_config_file`, searching :meth:`iter_log_paths`."""
673 return next(self._find_files(lambda dirs: dirs.iter_log_paths(), name), None)
675 def find_log_files(self, name: str | os.PathLike[str]) -> list[Path]:
676 """Like :meth:`find_config_files`, searching :meth:`iter_log_paths`."""
677 return list(self._find_files(lambda dirs: dirs.iter_log_paths(), name))
679 def find_runtime_file(self, name: str | os.PathLike[str]) -> Path | None:
680 """Like :meth:`find_config_file`, searching :meth:`iter_runtime_paths`."""
681 return next(self._find_runtime_files(name), None)
683 def find_runtime_files(self, name: str | os.PathLike[str]) -> list[Path]:
684 """Like :meth:`find_config_files`, searching :meth:`iter_runtime_paths`."""
685 return list(self._find_runtime_files(name))
687 def _find_runtime_files(self, name: str | os.PathLike[str]) -> Iterator[Path]:
688 return self._find_files(lambda dirs: dirs.iter_runtime_paths(), name)
690 def _find_files(
691 self, iter_paths: Callable[[PlatformDirsABC], Iterator[Path]], name: str | os.PathLike[str]
692 ) -> Iterator[Path]:
693 _ensure_inside_base("name", relative := os.fspath(name))
694 # The iterators create each directory they yield under ensure_exists, and a lookup must only read the disk.
695 (lookup := copy(self)).ensure_exists = False
696 return (path for directory in iter_paths(lookup) if (path := directory / relative).is_file())
699def _ensure_inside_base(name: str, value: str | Literal[False] | None) -> None:
700 if not value:
701 return
702 # os.path.join discards the base when a later component has a root or a drive, and ``..`` climbs out of it.
703 drive, tail = os.path.splitdrive(value)
704 if drive or tail.startswith(("/", "\\")) or ".." in tail.replace("\\", "/").split("/"):
705 msg = f"{name} must stay inside the base directory, got {value!r}"
706 raise ValueError(msg)
709def _ensure_home_known(path: str) -> None:
710 # expanduser leaves "~" when the home is unknown, and creating it would make a "~" directory in the cwd.
711 if path.startswith("~"):
712 msg = f"could not determine the home directory, refusing to create {path!r}"
713 raise RuntimeError(msg)
716def _unique(dirs: Iterable[str]) -> Iterator[str]:
717 """:yield: ``dirs`` in order, skipping any directory already yielded."""
718 # Lazy on purpose: under ensure_exists reading a site_*_dir creates it, so draining ``dirs`` up front would
719 # create directories for a caller that stops after the first entry.
720 seen: set[Path] = set()
721 for path in dirs:
722 if (normalized := Path(path)) not in seen:
723 seen.add(normalized)
724 yield path
727def _make_private_directories(path: Path) -> None:
728 # XDG asks for 0o700 on each directory created on the way, while Path.mkdir(parents=True) applies it to the last.
729 try:
730 path.mkdir(mode=0o700, exist_ok=True)
731 except FileNotFoundError:
732 if path.parent == path: # a missing drive or network share has no parent to create
733 raise
734 _make_private_directories(path.parent)
735 path.mkdir(mode=0o700, exist_ok=True)
738class RuntimeDirWarning(UserWarning):
739 """Emitted when the Unix or macOS ``user_runtime_dir`` cannot use ``$XDG_RUNTIME_DIR``."""