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