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
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 contextlib import suppress
8from copy import copy
9from pathlib import Path
10from typing import TYPE_CHECKING
12if TYPE_CHECKING:
13 from collections.abc import Callable, Iterable, Iterator
14 from typing import Literal
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.
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.
24 """
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.
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`.
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.
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>`_).
58 """
59 self.multipath = multipath
60 """An optional parameter which indicates that the entire list of data dirs should be returned.
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.
65 """
66 self.opinion = opinion
67 """Whether to use opinionated values.
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.
72 """
73 self.ensure_exists = ensure_exists
74 """Create the directory a property returns, with missing parents, each time the property is read.
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.
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).
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.
86 """
88 @property
89 def appname(self) -> str | None:
90 """The name of the application."""
91 return self._appname
93 @appname.setter
94 def appname(self, value: str | None) -> None:
95 _ensure_inside_base("appname", value)
96 self._appname = value
98 @property
99 def appauthor(self) -> str | Literal[False] | None:
100 """The name of the app author or distributing body for this application.
102 Typically, it is the owning company name. Defaults to `appname`. You may pass ``False`` to disable it.
104 .. note::
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>``.
111 """
112 return self._appauthor
114 @appauthor.setter
115 def appauthor(self, value: str | Literal[False] | None) -> None:
116 _ensure_inside_base("appauthor", value)
117 self._appauthor = value
119 @property
120 def version(self) -> str | None:
121 """An optional version path element to append to the path.
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>``.
126 """
127 return self._version
129 @version.setter
130 def version(self, value: str | None) -> None:
131 _ensure_inside_base("version", value)
132 self._version = value
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
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]
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)
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)
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)
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
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)
180 @property
181 @abstractmethod
182 def user_data_dir(self) -> str:
183 """Data directory tied to the user."""
185 @property
186 @abstractmethod
187 def site_data_dir(self) -> str:
188 """Data directory shared by users."""
190 @property
191 def _site_data_dirs(self) -> list[str]:
192 return [self.site_data_dir]
194 @property
195 @abstractmethod
196 def user_config_dir(self) -> str:
197 """Config directory tied to the user."""
199 @property
200 @abstractmethod
201 def site_config_dir(self) -> str:
202 """Config directory shared by users."""
204 @property
205 def _site_config_dirs(self) -> list[str]:
206 return [self.site_config_dir]
208 @property
209 @abstractmethod
210 def user_cache_dir(self) -> str:
211 """Cache directory tied to the user."""
213 @property
214 @abstractmethod
215 def site_cache_dir(self) -> str:
216 """Cache directory shared by users."""
218 @property
219 def _site_cache_dirs(self) -> list[str]:
220 return [self.site_cache_dir]
222 @property
223 @abstractmethod
224 def user_state_dir(self) -> str:
225 """State directory tied to the user."""
227 @property
228 @abstractmethod
229 def site_state_dir(self) -> str:
230 """State directory shared by users."""
232 @property
233 @abstractmethod
234 def user_log_dir(self) -> str:
235 """Log directory tied to the user."""
237 @property
238 @abstractmethod
239 def site_log_dir(self) -> str:
240 """Log directory shared by users."""
242 @property
243 @abstractmethod
244 def user_documents_dir(self) -> str:
245 """Documents directory tied to the user."""
247 @property
248 @abstractmethod
249 def user_downloads_dir(self) -> str:
250 """Downloads directory tied to the user."""
252 @property
253 @abstractmethod
254 def user_pictures_dir(self) -> str:
255 """Pictures directory tied to the user."""
257 @property
258 @abstractmethod
259 def user_videos_dir(self) -> str:
260 """Videos directory tied to the user."""
262 @property
263 @abstractmethod
264 def user_music_dir(self) -> str:
265 """Music directory tied to the user."""
267 @property
268 @abstractmethod
269 def user_desktop_dir(self) -> str:
270 """Desktop directory tied to the user."""
272 @property
273 @abstractmethod
274 def user_projects_dir(self) -> str:
275 """Projects directory tied to the user."""
277 @property
278 @abstractmethod
279 def user_publicshare_dir(self) -> str:
280 """Public share directory tied to the user."""
282 @property
283 @abstractmethod
284 def user_templates_dir(self) -> str:
285 """Templates directory tied to the user."""
287 @property
288 @abstractmethod
289 def user_fonts_dir(self) -> str:
290 """Fonts directory tied to the user."""
292 @property
293 @abstractmethod
294 def user_preference_dir(self) -> str:
295 """Preference directory tied to the user."""
297 @property
298 @abstractmethod
299 def user_bin_dir(self) -> str:
300 """Bin directory tied to the user."""
302 @property
303 @abstractmethod
304 def site_bin_dir(self) -> str:
305 """Bin directory shared by users."""
307 @property
308 @abstractmethod
309 def user_applications_dir(self) -> str:
310 """Applications directory tied to the user."""
312 @property
313 @abstractmethod
314 def site_applications_dir(self) -> str:
315 """Applications directory shared by users."""
317 @property
318 def _site_applications_dirs(self) -> list[str]:
319 return [self.site_applications_dir]
321 @property
322 @abstractmethod
323 def user_runtime_dir(self) -> str:
324 """Runtime directory tied to the user."""
326 @property
327 @abstractmethod
328 def site_runtime_dir(self) -> str:
329 """Runtime directory shared by users."""
331 @property
332 def user_data_path(self) -> Path:
333 """Data path tied to the user."""
334 return Path(self.user_data_dir)
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)
341 @property
342 def user_config_path(self) -> Path:
343 """Config path tied to the user."""
344 return Path(self.user_config_dir)
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)
351 @property
352 def user_cache_path(self) -> Path:
353 """Cache path tied to the user."""
354 return Path(self.user_cache_dir)
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)
361 @property
362 def user_state_path(self) -> Path:
363 """State path tied to the user."""
364 return Path(self.user_state_dir)
366 @property
367 def site_state_path(self) -> Path:
368 """State path shared by users."""
369 return Path(self.site_state_dir)
371 @property
372 def user_log_path(self) -> Path:
373 """Log path tied to the user."""
374 return Path(self.user_log_dir)
376 @property
377 def site_log_path(self) -> Path:
378 """Log path shared by users."""
379 return Path(self.site_log_dir)
381 @property
382 def user_documents_path(self) -> Path:
383 """Documents path tied to the user."""
384 return Path(self.user_documents_dir)
386 @property
387 def user_downloads_path(self) -> Path:
388 """Downloads path tied to the user."""
389 return Path(self.user_downloads_dir)
391 @property
392 def user_pictures_path(self) -> Path:
393 """Pictures path tied to the user."""
394 return Path(self.user_pictures_dir)
396 @property
397 def user_videos_path(self) -> Path:
398 """Videos path tied to the user."""
399 return Path(self.user_videos_dir)
401 @property
402 def user_music_path(self) -> Path:
403 """Music path tied to the user."""
404 return Path(self.user_music_dir)
406 @property
407 def user_desktop_path(self) -> Path:
408 """Desktop path tied to the user."""
409 return Path(self.user_desktop_dir)
411 @property
412 def user_projects_path(self) -> Path:
413 """Projects path tied to the user."""
414 return Path(self.user_projects_dir)
416 @property
417 def user_publicshare_path(self) -> Path:
418 """Public share path tied to the user."""
419 return Path(self.user_publicshare_dir)
421 @property
422 def user_templates_path(self) -> Path:
423 """Templates path tied to the user."""
424 return Path(self.user_templates_dir)
426 @property
427 def user_fonts_path(self) -> Path:
428 """Fonts path tied to the user."""
429 return Path(self.user_fonts_dir)
431 @property
432 def user_preference_path(self) -> Path:
433 """Preference path tied to the user."""
434 return Path(self.user_preference_dir)
436 @property
437 def user_bin_path(self) -> Path:
438 """Bin path tied to the user."""
439 return Path(self.user_bin_dir)
441 @property
442 def site_bin_path(self) -> Path:
443 """Bin path shared by users."""
444 return Path(self.site_bin_dir)
446 @property
447 def user_applications_path(self) -> Path:
448 """Applications path tied to the user."""
449 return Path(self.user_applications_dir)
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])
456 @property
457 def user_runtime_path(self) -> Path:
458 """Runtime path tied to the user."""
459 return Path(self.user_runtime_dir)
461 @property
462 def site_runtime_path(self) -> Path:
463 """Runtime path shared by users."""
464 return Path(self.site_runtime_dir)
466 def iter_config_dirs(self) -> Iterator[str]:
467 """:yield: all user and site configuration directories."""
468 yield from _unique(self._iter_config_dirs())
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)
475 def iter_data_dirs(self) -> Iterator[str]:
476 """:yield: all user and site data directories."""
477 yield from _unique(self._iter_data_dirs())
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)
484 def iter_cache_dirs(self) -> Iterator[str]:
485 """:yield: all user and site cache directories."""
486 yield from _unique(self._iter_cache_dirs())
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)
493 def iter_state_dirs(self) -> Iterator[str]:
494 """:yield: all user and site state directories."""
495 yield from _unique(self._iter_state_dirs())
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
502 def iter_log_dirs(self) -> Iterator[str]:
503 """:yield: all user and site log directories."""
504 yield from _unique(self._iter_log_dirs())
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
511 def iter_runtime_dirs(self) -> Iterator[str]:
512 """:yield: all user and site runtime directories."""
513 yield from _unique(self._iter_runtime_dirs())
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
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
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)
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)
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)
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)
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)
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)
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.
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.
565 :param name: file path relative to the user directory, e.g. ``"sub/app.toml"``.
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.
571 """
572 return self._place_file(lambda dirs: dirs.user_config_path, name)
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)
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)
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)
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)
590 def place_runtime_file(self, name: str | os.PathLike[str]) -> Path:
591 """Like `place_config_file`, under `user_runtime_path`.
593 :raises PermissionError: on Unix, if another user owns the temporary fallback directory.
595 """
596 return self._place_file(lambda dirs: dirs.user_runtime_path, name)
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
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.
618 :param name: path relative to each directory, may contain subdirectories.
620 :returns: the file, or ``None`` when no directory holds one.
622 :raises ValueError: if ``name`` is absolute, has a drive or climbs out with ``..``.
624 """
625 return next(self._find_files(lambda dirs: dirs.iter_config_paths(), name), None)
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.
630 :param name: see :meth:`find_config_file`.
632 :returns: the files, user directory first.
634 :raises ValueError: see :meth:`find_config_file`.
636 """
637 return list(self._find_files(lambda dirs: dirs.iter_config_paths(), name))
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)
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))
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)
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))
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)
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))
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)
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))
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)
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))
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())
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)
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)
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
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)
727class RuntimeDirWarning(UserWarning):
728 """Emitted when the Unix :attr:`~platformdirs.unix.Unix.user_runtime_dir` cannot use ``$XDG_RUNTIME_DIR``."""