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
570 """
571 return self._place_file(lambda dirs: dirs.user_config_path, name)
572
573 def place_data_file(self, name: str | os.PathLike[str]) -> Path:
574 """Like `place_config_file`, under `user_data_path`."""
575 return self._place_file(lambda dirs: dirs.user_data_path, name)
576
577 def place_cache_file(self, name: str | os.PathLike[str]) -> Path:
578 """Like `place_config_file`, under `user_cache_path`."""
579 return self._place_file(lambda dirs: dirs.user_cache_path, name)
580
581 def place_state_file(self, name: str | os.PathLike[str]) -> Path:
582 """Like `place_config_file`, under `user_state_path`."""
583 return self._place_file(lambda dirs: dirs.user_state_path, name)
584
585 def place_log_file(self, name: str | os.PathLike[str]) -> Path:
586 """Like `place_config_file`, under `user_log_path`."""
587 return self._place_file(lambda dirs: dirs.user_log_path, name)
588
589 def place_runtime_file(self, name: str | os.PathLike[str]) -> Path:
590 """Like `place_config_file`, under `user_runtime_path`.
591
592 :raises PermissionError: on Unix, if another user owns the temporary fallback directory.
593
594 """
595 return self._place_file(lambda dirs: dirs.user_runtime_path, name)
596
597 def _place_file(self, user_path: Callable[[PlatformDirsABC], Path], name: str | os.PathLike[str]) -> Path:
598 _ensure_inside_base("name", relative := os.fspath(name))
599 if not Path(relative).parts:
600 msg = f"name must point to a file, got {relative!r}"
601 raise ValueError(msg)
602 # Under ensure_exists the lookup itself would create the user directory with the default mode, not 0o700.
603 (lookup := copy(self)).ensure_exists = False
604 # Under multipath *_dir joins the site directories with os.pathsep, so place under the single *_path.
605 _ensure_home_known(str(directory := user_path(lookup)))
606 path = directory / relative
607 if self._use_site:
608 # Site directories keep the default mode, as under ensure_exists, so other users can still read them.
609 path.parent.mkdir(parents=True, exist_ok=True)
610 else:
611 _make_private_directories(path.parent)
612 return path
613
614 def find_config_file(self, name: str | os.PathLike[str]) -> Path | None:
615 """Find the first ``name`` file in :meth:`iter_config_paths` order, without creating any directory.
616
617 :param name: path relative to each directory, may contain subdirectories.
618
619 :returns: the file, or ``None`` when no directory holds one.
620
621 :raises ValueError: if ``name`` is absolute, has a drive or climbs out with ``..``.
622
623 """
624 return next(self._find_files(PlatformDirsABC.iter_config_paths, name), None)
625
626 def find_config_files(self, name: str | os.PathLike[str]) -> list[Path]:
627 """Find every ``name`` file in :meth:`iter_config_paths` order, without creating any directory.
628
629 :param name: see :meth:`find_config_file`.
630
631 :returns: the files, user directory first.
632
633 :raises ValueError: see :meth:`find_config_file`.
634
635 """
636 return list(self._find_files(PlatformDirsABC.iter_config_paths, name))
637
638 def find_data_file(self, name: str | os.PathLike[str]) -> Path | None:
639 """Like :meth:`find_config_file`, searching :meth:`iter_data_paths`."""
640 return next(self._find_files(PlatformDirsABC.iter_data_paths, name), None)
641
642 def find_data_files(self, name: str | os.PathLike[str]) -> list[Path]:
643 """Like :meth:`find_config_files`, searching :meth:`iter_data_paths`."""
644 return list(self._find_files(PlatformDirsABC.iter_data_paths, name))
645
646 def find_cache_file(self, name: str | os.PathLike[str]) -> Path | None:
647 """Like :meth:`find_config_file`, searching :meth:`iter_cache_paths`."""
648 return next(self._find_files(PlatformDirsABC.iter_cache_paths, name), None)
649
650 def find_cache_files(self, name: str | os.PathLike[str]) -> list[Path]:
651 """Like :meth:`find_config_files`, searching :meth:`iter_cache_paths`."""
652 return list(self._find_files(PlatformDirsABC.iter_cache_paths, name))
653
654 def find_state_file(self, name: str | os.PathLike[str]) -> Path | None:
655 """Like :meth:`find_config_file`, searching :meth:`iter_state_paths`."""
656 return next(self._find_files(PlatformDirsABC.iter_state_paths, name), None)
657
658 def find_state_files(self, name: str | os.PathLike[str]) -> list[Path]:
659 """Like :meth:`find_config_files`, searching :meth:`iter_state_paths`."""
660 return list(self._find_files(PlatformDirsABC.iter_state_paths, name))
661
662 def find_log_file(self, name: str | os.PathLike[str]) -> Path | None:
663 """Like :meth:`find_config_file`, searching :meth:`iter_log_paths`."""
664 return next(self._find_files(PlatformDirsABC.iter_log_paths, name), None)
665
666 def find_log_files(self, name: str | os.PathLike[str]) -> list[Path]:
667 """Like :meth:`find_config_files`, searching :meth:`iter_log_paths`."""
668 return list(self._find_files(PlatformDirsABC.iter_log_paths, name))
669
670 def find_runtime_file(self, name: str | os.PathLike[str]) -> Path | None:
671 """Like :meth:`find_config_file`, searching :meth:`iter_runtime_paths`."""
672 return next(self._find_files(PlatformDirsABC.iter_runtime_paths, name), None)
673
674 def find_runtime_files(self, name: str | os.PathLike[str]) -> list[Path]:
675 """Like :meth:`find_config_files`, searching :meth:`iter_runtime_paths`."""
676 return list(self._find_files(PlatformDirsABC.iter_runtime_paths, name))
677
678 def _find_files(
679 self, iter_paths: Callable[[PlatformDirsABC], Iterator[Path]], name: str | os.PathLike[str]
680 ) -> Iterator[Path]:
681 _ensure_inside_base("name", relative := os.fspath(name))
682 # The iterators create each directory they yield under ensure_exists, and a lookup must only read the disk.
683 (lookup := copy(self)).ensure_exists = False
684 return (path for directory in iter_paths(lookup) if (path := directory / relative).is_file())
685
686
687def _ensure_inside_base(name: str, value: str | Literal[False] | None) -> None:
688 if not value:
689 return
690 # os.path.join discards the base when a later component has a root or a drive, and ``..`` climbs out of it.
691 drive, tail = os.path.splitdrive(value)
692 if drive or tail.startswith(("/", "\\")) or ".." in tail.replace("\\", "/").split("/"):
693 msg = f"{name} must stay inside the base directory, got {value!r}"
694 raise ValueError(msg)
695
696
697def _ensure_home_known(path: str) -> None:
698 # expanduser leaves "~" when the home is unknown, and creating it would make a "~" directory in the cwd.
699 if path.startswith("~"):
700 msg = f"could not determine the home directory, refusing to create {path!r}"
701 raise RuntimeError(msg)
702
703
704def _unique(dirs: Iterable[str]) -> Iterator[str]:
705 """:yield: ``dirs`` in order, skipping any directory already yielded."""
706 # Lazy on purpose: under ensure_exists reading a site_*_dir creates it, so draining ``dirs`` up front would
707 # create directories for a caller that stops after the first entry.
708 seen: set[str] = set()
709 for path in dirs:
710 if path not in seen:
711 seen.add(path)
712 yield path
713
714
715def _make_private_directories(path: Path) -> None:
716 # XDG asks for 0o700 on each directory created on the way, while Path.mkdir(parents=True) applies it to the last.
717 try:
718 path.mkdir(mode=0o700, exist_ok=True)
719 except FileNotFoundError:
720 _make_private_directories(path.parent)
721 path.mkdir(mode=0o700, exist_ok=True)
722
723
724class RuntimeDirWarning(UserWarning):
725 """Emitted when the Unix :attr:`~platformdirs.unix.Unix.user_runtime_dir` cannot use ``$XDG_RUNTIME_DIR``."""