unix.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. """Unix."""
  2. from __future__ import annotations
  3. import os
  4. import sys
  5. from configparser import ConfigParser
  6. from pathlib import Path
  7. from typing import TYPE_CHECKING, NoReturn
  8. from .api import PlatformDirsABC
  9. if TYPE_CHECKING:
  10. from collections.abc import Iterator
  11. if sys.platform == "win32":
  12. def getuid() -> NoReturn:
  13. msg = "should only be used on Unix"
  14. raise RuntimeError(msg)
  15. else:
  16. from os import getuid
  17. class Unix(PlatformDirsABC): # noqa: PLR0904
  18. """
  19. On Unix/Linux, we follow the `XDG Basedir Spec <https://specifications.freedesktop.org/basedir-spec/basedir-spec-
  20. latest.html>`_.
  21. The spec allows overriding directories with environment variables. The examples shown are the default values,
  22. alongside the name of the environment variable that overrides them. Makes use of the `appname
  23. <platformdirs.api.PlatformDirsABC.appname>`, `version <platformdirs.api.PlatformDirsABC.version>`, `multipath
  24. <platformdirs.api.PlatformDirsABC.multipath>`, `opinion <platformdirs.api.PlatformDirsABC.opinion>`, `ensure_exists
  25. <platformdirs.api.PlatformDirsABC.ensure_exists>`.
  26. """
  27. @property
  28. def user_data_dir(self) -> str:
  29. """
  30. :return: data directory tied to the user, e.g. ``~/.local/share/$appname/$version`` or
  31. ``$XDG_DATA_HOME/$appname/$version``
  32. """
  33. path = os.environ.get("XDG_DATA_HOME", "")
  34. if not path.strip():
  35. path = os.path.expanduser("~/.local/share") # noqa: PTH111
  36. return self._append_app_name_and_version(path)
  37. @property
  38. def _site_data_dirs(self) -> list[str]:
  39. path = os.environ.get("XDG_DATA_DIRS", "")
  40. if not path.strip():
  41. path = f"/usr/local/share{os.pathsep}/usr/share"
  42. return [self._append_app_name_and_version(p) for p in path.split(os.pathsep)]
  43. @property
  44. def site_data_dir(self) -> str:
  45. """
  46. :return: data directories shared by users (if `multipath <platformdirs.api.PlatformDirsABC.multipath>` is
  47. enabled and ``XDG_DATA_DIRS`` is set and a multi path the response is also a multi path separated by the
  48. OS path separator), e.g. ``/usr/local/share/$appname/$version`` or ``/usr/share/$appname/$version``
  49. """
  50. # XDG default for $XDG_DATA_DIRS; only first, if multipath is False
  51. dirs = self._site_data_dirs
  52. if not self.multipath:
  53. return dirs[0]
  54. return os.pathsep.join(dirs)
  55. @property
  56. def user_config_dir(self) -> str:
  57. """
  58. :return: config directory tied to the user, e.g. ``~/.config/$appname/$version`` or
  59. ``$XDG_CONFIG_HOME/$appname/$version``
  60. """
  61. path = os.environ.get("XDG_CONFIG_HOME", "")
  62. if not path.strip():
  63. path = os.path.expanduser("~/.config") # noqa: PTH111
  64. return self._append_app_name_and_version(path)
  65. @property
  66. def _site_config_dirs(self) -> list[str]:
  67. path = os.environ.get("XDG_CONFIG_DIRS", "")
  68. if not path.strip():
  69. path = "/etc/xdg"
  70. return [self._append_app_name_and_version(p) for p in path.split(os.pathsep)]
  71. @property
  72. def site_config_dir(self) -> str:
  73. """
  74. :return: config directories shared by users (if `multipath <platformdirs.api.PlatformDirsABC.multipath>`
  75. is enabled and ``XDG_CONFIG_DIRS`` is set and a multi path the response is also a multi path separated by
  76. the OS path separator), e.g. ``/etc/xdg/$appname/$version``
  77. """
  78. # XDG default for $XDG_CONFIG_DIRS only first, if multipath is False
  79. dirs = self._site_config_dirs
  80. if not self.multipath:
  81. return dirs[0]
  82. return os.pathsep.join(dirs)
  83. @property
  84. def user_cache_dir(self) -> str:
  85. """
  86. :return: cache directory tied to the user, e.g. ``~/.cache/$appname/$version`` or
  87. ``~/$XDG_CACHE_HOME/$appname/$version``
  88. """
  89. path = os.environ.get("XDG_CACHE_HOME", "")
  90. if not path.strip():
  91. path = os.path.expanduser("~/.cache") # noqa: PTH111
  92. return self._append_app_name_and_version(path)
  93. @property
  94. def site_cache_dir(self) -> str:
  95. """:return: cache directory shared by users, e.g. ``/var/cache/$appname/$version``"""
  96. return self._append_app_name_and_version("/var/cache")
  97. @property
  98. def user_state_dir(self) -> str:
  99. """
  100. :return: state directory tied to the user, e.g. ``~/.local/state/$appname/$version`` or
  101. ``$XDG_STATE_HOME/$appname/$version``
  102. """
  103. path = os.environ.get("XDG_STATE_HOME", "")
  104. if not path.strip():
  105. path = os.path.expanduser("~/.local/state") # noqa: PTH111
  106. return self._append_app_name_and_version(path)
  107. @property
  108. def user_log_dir(self) -> str:
  109. """:return: log directory tied to the user, same as `user_state_dir` if not opinionated else ``log`` in it"""
  110. path = self.user_state_dir
  111. if self.opinion:
  112. path = os.path.join(path, "log") # noqa: PTH118
  113. self._optionally_create_directory(path)
  114. return path
  115. @property
  116. def user_documents_dir(self) -> str:
  117. """:return: documents directory tied to the user, e.g. ``~/Documents``"""
  118. return _get_user_media_dir("XDG_DOCUMENTS_DIR", "~/Documents")
  119. @property
  120. def user_downloads_dir(self) -> str:
  121. """:return: downloads directory tied to the user, e.g. ``~/Downloads``"""
  122. return _get_user_media_dir("XDG_DOWNLOAD_DIR", "~/Downloads")
  123. @property
  124. def user_pictures_dir(self) -> str:
  125. """:return: pictures directory tied to the user, e.g. ``~/Pictures``"""
  126. return _get_user_media_dir("XDG_PICTURES_DIR", "~/Pictures")
  127. @property
  128. def user_videos_dir(self) -> str:
  129. """:return: videos directory tied to the user, e.g. ``~/Videos``"""
  130. return _get_user_media_dir("XDG_VIDEOS_DIR", "~/Videos")
  131. @property
  132. def user_music_dir(self) -> str:
  133. """:return: music directory tied to the user, e.g. ``~/Music``"""
  134. return _get_user_media_dir("XDG_MUSIC_DIR", "~/Music")
  135. @property
  136. def user_desktop_dir(self) -> str:
  137. """:return: desktop directory tied to the user, e.g. ``~/Desktop``"""
  138. return _get_user_media_dir("XDG_DESKTOP_DIR", "~/Desktop")
  139. @property
  140. def user_runtime_dir(self) -> str:
  141. """
  142. :return: runtime directory tied to the user, e.g. ``/run/user/$(id -u)/$appname/$version`` or
  143. ``$XDG_RUNTIME_DIR/$appname/$version``.
  144. For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/user/$(id -u)/$appname/$version`` if
  145. exists, otherwise ``/tmp/runtime-$(id -u)/$appname/$version``, if``$XDG_RUNTIME_DIR``
  146. is not set.
  147. """
  148. path = os.environ.get("XDG_RUNTIME_DIR", "")
  149. if not path.strip():
  150. if sys.platform.startswith(("freebsd", "openbsd", "netbsd")):
  151. path = f"/var/run/user/{getuid()}"
  152. if not Path(path).exists():
  153. path = f"/tmp/runtime-{getuid()}" # noqa: S108
  154. else:
  155. path = f"/run/user/{getuid()}"
  156. return self._append_app_name_and_version(path)
  157. @property
  158. def site_runtime_dir(self) -> str:
  159. """
  160. :return: runtime directory shared by users, e.g. ``/run/$appname/$version`` or \
  161. ``$XDG_RUNTIME_DIR/$appname/$version``.
  162. Note that this behaves almost exactly like `user_runtime_dir` if ``$XDG_RUNTIME_DIR`` is set, but will
  163. fall back to paths associated to the root user instead of a regular logged-in user if it's not set.
  164. If you wish to ensure that a logged-in root user path is returned e.g. ``/run/user/0``, use `user_runtime_dir`
  165. instead.
  166. For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/$appname/$version`` if ``$XDG_RUNTIME_DIR`` is not set.
  167. """
  168. path = os.environ.get("XDG_RUNTIME_DIR", "")
  169. if not path.strip():
  170. if sys.platform.startswith(("freebsd", "openbsd", "netbsd")):
  171. path = "/var/run"
  172. else:
  173. path = "/run"
  174. return self._append_app_name_and_version(path)
  175. @property
  176. def site_data_path(self) -> Path:
  177. """:return: data path shared by users. Only return the first item, even if ``multipath`` is set to ``True``"""
  178. return self._first_item_as_path_if_multipath(self.site_data_dir)
  179. @property
  180. def site_config_path(self) -> Path:
  181. """:return: config path shared by the users, returns the first item, even if ``multipath`` is set to ``True``"""
  182. return self._first_item_as_path_if_multipath(self.site_config_dir)
  183. @property
  184. def site_cache_path(self) -> Path:
  185. """:return: cache path shared by users. Only return the first item, even if ``multipath`` is set to ``True``"""
  186. return self._first_item_as_path_if_multipath(self.site_cache_dir)
  187. def iter_config_dirs(self) -> Iterator[str]:
  188. """:yield: all user and site configuration directories."""
  189. yield self.user_config_dir
  190. yield from self._site_config_dirs
  191. def iter_data_dirs(self) -> Iterator[str]:
  192. """:yield: all user and site data directories."""
  193. yield self.user_data_dir
  194. yield from self._site_data_dirs
  195. def _get_user_media_dir(env_var: str, fallback_tilde_path: str) -> str:
  196. media_dir = _get_user_dirs_folder(env_var)
  197. if media_dir is None:
  198. media_dir = os.environ.get(env_var, "").strip()
  199. if not media_dir:
  200. media_dir = os.path.expanduser(fallback_tilde_path) # noqa: PTH111
  201. return media_dir
  202. def _get_user_dirs_folder(key: str) -> str | None:
  203. """
  204. Return directory from user-dirs.dirs config file.
  205. See https://freedesktop.org/wiki/Software/xdg-user-dirs/.
  206. """
  207. user_dirs_config_path = Path(Unix().user_config_dir) / "user-dirs.dirs"
  208. if user_dirs_config_path.exists():
  209. parser = ConfigParser()
  210. with user_dirs_config_path.open() as stream:
  211. # Add fake section header, so ConfigParser doesn't complain
  212. parser.read_string(f"[top]\n{stream.read()}")
  213. if key not in parser["top"]:
  214. return None
  215. path = parser["top"][key].strip('"')
  216. # Handle relative home paths
  217. return path.replace("$HOME", os.path.expanduser("~")) # noqa: PTH111
  218. return None
  219. __all__ = [
  220. "Unix",
  221. ]