platformdirs 4.13.2__tar.gz → 4.13.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {platformdirs-4.13.2 → platformdirs-4.13.4}/PKG-INFO +1 -1
  2. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/android.py +2 -2
  3. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/ios.py +17 -18
  4. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/testing.py +4 -2
  5. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/version.py +2 -2
  6. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/windows.py +2 -1
  7. {platformdirs-4.13.2 → platformdirs-4.13.4}/tests/test_android.py +26 -0
  8. {platformdirs-4.13.2 → platformdirs-4.13.4}/tests/test_ios.py +21 -1
  9. {platformdirs-4.13.2 → platformdirs-4.13.4}/tests/test_testing.py +55 -0
  10. {platformdirs-4.13.2 → platformdirs-4.13.4}/tests/test_unix.py +1 -1
  11. {platformdirs-4.13.2 → platformdirs-4.13.4}/tests/test_windows.py +8 -0
  12. {platformdirs-4.13.2 → platformdirs-4.13.4}/.gitignore +0 -0
  13. {platformdirs-4.13.2 → platformdirs-4.13.4}/LICENSE +0 -0
  14. {platformdirs-4.13.2 → platformdirs-4.13.4}/README.md +0 -0
  15. {platformdirs-4.13.2 → platformdirs-4.13.4}/pyproject.toml +0 -0
  16. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/__init__.py +0 -0
  17. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/__main__.py +0 -0
  18. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/_xdg.py +0 -0
  19. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/api.py +0 -0
  20. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/macos.py +0 -0
  21. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/py.typed +0 -0
  22. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/pytest_plugin.py +0 -0
  23. {platformdirs-4.13.2 → platformdirs-4.13.4}/src/platformdirs/unix.py +0 -0
  24. {platformdirs-4.13.2 → platformdirs-4.13.4}/tests/conftest.py +0 -0
  25. {platformdirs-4.13.2 → platformdirs-4.13.4}/tests/test_api.py +0 -0
  26. {platformdirs-4.13.2 → platformdirs-4.13.4}/tests/test_comp_with_appdirs.py +0 -0
  27. {platformdirs-4.13.2 → platformdirs-4.13.4}/tests/test_macos.py +0 -0
  28. {platformdirs-4.13.2 → platformdirs-4.13.4}/tests/test_main.py +0 -0
  29. {platformdirs-4.13.2 → platformdirs-4.13.4}/tests/test_pytest_plugin.py +0 -0
  30. {platformdirs-4.13.2 → platformdirs-4.13.4}/tox.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: platformdirs
3
- Version: 4.13.2
3
+ Version: 4.13.4
4
4
  Summary: A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`.
5
5
  Project-URL: Changelog, https://platformdirs.readthedocs.io/en/latest/changelog.html
6
6
  Project-URL: Documentation, https://platformdirs.readthedocs.io
@@ -189,7 +189,7 @@ def _android_folder() -> str | None:
189
189
  /data/(?:data|user/\d+) # internal storage of the primary user or of user N
190
190
  /[^/]+ # package name, one path segment
191
191
  )
192
- /files # the files directory the interpreter runs from
192
+ /files(?=/|\Z) # the files directory the interpreter runs from
193
193
  """,
194
194
  re.VERBOSE,
195
195
  )
@@ -209,7 +209,7 @@ def _android_folder() -> str | None:
209
209
  /(?:data|user/\d+) # the primary user or user N
210
210
  /[^/]+ # package name, one path segment
211
211
  )
212
- /files # the files directory the interpreter runs from
212
+ /files(?=/|\Z) # the files directory the interpreter runs from
213
213
  """,
214
214
  re.VERBOSE,
215
215
  )
@@ -2,8 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import os.path
6
-
5
+ from ._xdg import _expand_user
7
6
  from .api import PlatformDirsABC
8
7
 
9
8
 
@@ -23,7 +22,7 @@ class IOS(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
23
22
  @property
24
23
  def user_data_dir(self) -> str:
25
24
  """Data directory tied to the user, e.g. ``~/Library/Application Support/$appname/$version``."""
26
- return self._append_app_name_and_version(os.path.expanduser("~/Library/Application Support"), private=True) # ruff:ignore[os-path-expanduser] # Path.expanduser raises on an unknown home
25
+ return self._append_app_name_and_version(_expand_user("~/Library/Application Support"), private=True)
27
26
 
28
27
  @property
29
28
  def site_data_dir(self) -> str:
@@ -43,7 +42,7 @@ class IOS(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
43
42
  @property
44
43
  def user_cache_dir(self) -> str:
45
44
  """Cache directory tied to the user, e.g. ``~/Library/Caches/$appname/$version``."""
46
- return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches"), private=True) # ruff:ignore[os-path-expanduser] # Path.expanduser raises on an unknown home
45
+ return self._append_app_name_and_version(_expand_user("~/Library/Caches"), private=True)
47
46
 
48
47
  @property
49
48
  def site_cache_dir(self) -> str:
@@ -63,7 +62,7 @@ class IOS(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
63
62
  @property
64
63
  def user_log_dir(self) -> str:
65
64
  """Log directory tied to the user, e.g. ``~/Library/Logs/$appname/$version``."""
66
- return self._append_app_name_and_version(os.path.expanduser("~/Library/Logs"), private=True) # ruff:ignore[os-path-expanduser] # Path.expanduser raises on an unknown home
65
+ return self._append_app_name_and_version(_expand_user("~/Library/Logs"), private=True)
67
66
 
68
67
  @property
69
68
  def site_log_dir(self) -> str:
@@ -73,53 +72,53 @@ class IOS(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
73
72
  @property
74
73
  def user_documents_dir(self) -> str:
75
74
  """Documents directory tied to the user, e.g. ``~/Documents``."""
76
- return os.path.expanduser("~/Documents") # ruff:ignore[os-path-expanduser]
75
+ return _expand_user("~/Documents")
77
76
 
78
77
  # The app container has Documents but no other media folder, so the rest live inside it, as Qt places them.
79
78
  @property
80
79
  def user_downloads_dir(self) -> str:
81
80
  """Downloads directory tied to the user, e.g. ``~/Documents/Downloads``."""
82
- return os.path.expanduser("~/Documents/Downloads") # ruff:ignore[os-path-expanduser]
81
+ return _expand_user("~/Documents/Downloads")
83
82
 
84
83
  @property
85
84
  def user_pictures_dir(self) -> str:
86
85
  """Pictures directory tied to the user, e.g. ``~/Documents/Pictures``."""
87
- return os.path.expanduser("~/Documents/Pictures") # ruff:ignore[os-path-expanduser]
86
+ return _expand_user("~/Documents/Pictures")
88
87
 
89
88
  @property
90
89
  def user_videos_dir(self) -> str:
91
90
  """Videos directory tied to the user, e.g. ``~/Documents/Movies``."""
92
- return os.path.expanduser("~/Documents/Movies") # ruff:ignore[os-path-expanduser]
91
+ return _expand_user("~/Documents/Movies")
93
92
 
94
93
  @property
95
94
  def user_music_dir(self) -> str:
96
95
  """Music directory tied to the user, e.g. ``~/Documents/Music``."""
97
- return os.path.expanduser("~/Documents/Music") # ruff:ignore[os-path-expanduser]
96
+ return _expand_user("~/Documents/Music")
98
97
 
99
98
  @property
100
99
  def user_desktop_dir(self) -> str:
101
100
  """Desktop directory tied to the user, e.g. ``~/Documents/Desktop``."""
102
- return os.path.expanduser("~/Documents/Desktop") # ruff:ignore[os-path-expanduser]
101
+ return _expand_user("~/Documents/Desktop")
103
102
 
104
103
  @property
105
104
  def user_projects_dir(self) -> str:
106
105
  """Projects directory tied to the user, e.g. ``~/Documents/Projects``."""
107
- return os.path.expanduser("~/Documents/Projects") # ruff:ignore[os-path-expanduser]
106
+ return _expand_user("~/Documents/Projects")
108
107
 
109
108
  @property
110
109
  def user_publicshare_dir(self) -> str:
111
110
  """Public share directory tied to the user, e.g. ``~/Documents/Public``."""
112
- return os.path.expanduser("~/Documents/Public") # ruff:ignore[os-path-expanduser]
111
+ return _expand_user("~/Documents/Public")
113
112
 
114
113
  @property
115
114
  def user_templates_dir(self) -> str:
116
115
  """Templates directory tied to the user, e.g. ``~/Documents/Templates``."""
117
- return os.path.expanduser("~/Documents/Templates") # ruff:ignore[os-path-expanduser]
116
+ return _expand_user("~/Documents/Templates")
118
117
 
119
118
  @property
120
119
  def user_fonts_dir(self) -> str:
121
120
  """Fonts directory tied to the user, e.g. ``~/Library/Fonts``."""
122
- return os.path.expanduser("~/Library/Fonts") # ruff:ignore[os-path-expanduser]
121
+ return _expand_user("~/Library/Fonts")
123
122
 
124
123
  @property
125
124
  def user_preference_dir(self) -> str:
@@ -130,7 +129,7 @@ class IOS(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
130
129
  @property
131
130
  def user_bin_dir(self) -> str:
132
131
  """Bin directory tied to the user, e.g. ``~/.local/bin``."""
133
- return os.path.expanduser("~/.local/bin") # ruff:ignore[os-path-expanduser]
132
+ return _expand_user("~/.local/bin")
134
133
 
135
134
  @property
136
135
  def site_bin_dir(self) -> str:
@@ -140,7 +139,7 @@ class IOS(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
140
139
  @property
141
140
  def user_applications_dir(self) -> str:
142
141
  """Applications directory tied to the user, e.g. ``~/Applications``."""
143
- return os.path.expanduser("~/Applications") # ruff:ignore[os-path-expanduser]
142
+ return _expand_user("~/Applications")
144
143
 
145
144
  @property
146
145
  def site_applications_dir(self) -> str:
@@ -150,7 +149,7 @@ class IOS(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
150
149
  @property
151
150
  def user_runtime_dir(self) -> str:
152
151
  """Runtime directory tied to the user, e.g. ``~/tmp/$appname/$version``."""
153
- return self._append_app_name_and_version(os.path.expanduser("~/tmp"), private=True) # ruff:ignore[os-path-expanduser] # Path.expanduser raises on an unknown home
152
+ return self._append_app_name_and_version(_expand_user("~/tmp"), private=True)
154
153
 
155
154
  @property
156
155
  def site_runtime_dir(self) -> str:
@@ -39,6 +39,8 @@ def isolated_dirs(root: str | os.PathLike[str]) -> Generator[Path, None, None]:
39
39
  ``PlatformDirs("app", version="1.0").user_config_path`` becomes ``<root>/user_config/app/1.0``. Every thread of the
40
40
  current process sees the change, and subprocesses do not.
41
41
 
42
+ On Unix, ``use_site_for_root`` is ignored inside the context so user and site directories keep separate kinds.
43
+
42
44
  :param root: directory that holds the redirected directories
43
45
 
44
46
  :returns: ``root`` as a :class:`~pathlib.Path`
@@ -51,11 +53,11 @@ def isolated_dirs(root: str | os.PathLike[str]) -> Generator[Path, None, None]:
51
53
  )
52
54
  for name in PlatformDirsABC.__abstractmethods__
53
55
  }
54
- # Unix and macOS build the site lists from search paths; the base versions derive them from the redirected dirs.
56
+ # Base site lists follow the redirected dirs, and disabling root redirection keeps user and site kinds separate.
55
57
  patches |= {
56
58
  name: value
57
59
  for name, value in vars(PlatformDirsABC).items()
58
- if name.startswith("_site_") and name.endswith("_dirs")
60
+ if name == "_use_site" or (name.startswith("_site_") and name.endswith("_dirs"))
59
61
  }
60
62
  cls = platformdirs.PlatformDirs
61
63
  saved = {name: vars(cls)[name] for name in patches if name in vars(cls)}
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '4.13.2'
22
- __version_tuple__ = version_tuple = (4, 13, 2)
21
+ __version__ = version = '4.13.4'
22
+ __version_tuple__ = version_tuple = (4, 13, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -9,6 +9,7 @@ from functools import cache
9
9
  from pathlib import Path
10
10
  from typing import TYPE_CHECKING, Final
11
11
 
12
+ from ._xdg import _expand_user
12
13
  from .api import PlatformDirsABC
13
14
 
14
15
  if TYPE_CHECKING:
@@ -139,7 +140,7 @@ class Windows(PlatformDirsABC): # ruff:ignore[too-many-public-methods]
139
140
  @property
140
141
  def user_projects_dir(self) -> str:
141
142
  r"""Projects directory tied to the user, e.g. ``%USERPROFILE%\Projects``."""
142
- return os.path.normpath(os.path.expanduser("~/Projects")) # ruff:ignore[os-path-expanduser]
143
+ return os.path.normpath(_expand_user("~/Projects"))
143
144
 
144
145
  @property
145
146
  def user_publicshare_dir(self) -> str:
@@ -341,3 +341,29 @@ def test_android_opinion_subdir_created_private(
341
341
  mocker.patch("platformdirs.android._android_folder", return_value=str(tmp_path), autospec=True)
342
342
  subdir = Path(getattr(Android(appname="app", ensure_exists=True), prop)).name
343
343
  assert modes(tmp_path) == {"cache": 0o700, "cache/app": 0o700, f"cache/app/{subdir}": 0o700}
344
+
345
+
346
+ @pytest.mark.usefixtures("_clear_android_caches")
347
+ @pytest.mark.parametrize(
348
+ "path",
349
+ [
350
+ pytest.param("/data/data/org.example.app/filesystem", id="internal-prefix"),
351
+ pytest.param("/data/user/10/org.example.app/files-backup/lib", id="secondary-user-prefix"),
352
+ pytest.param("/data/data/org.example.app/files\n", id="internal-newline"),
353
+ pytest.param(
354
+ "/mnt/expand/8e06fc2f-a86a-44e8-81ce-109e0eedd5ed/user/10/org.example.app/filesystem",
355
+ id="adopted-prefix",
356
+ ),
357
+ pytest.param(
358
+ "/mnt/expand/8e06fc2f-a86a-44e8-81ce-109e0eedd5ed/user/10/org.example.app/files\n",
359
+ id="adopted-newline",
360
+ ),
361
+ ],
362
+ )
363
+ def test_android_folder_requires_files_segment(
364
+ mocker: MockerFixture, monkeypatch: pytest.MonkeyPatch, path: str
365
+ ) -> None:
366
+ mocker.patch.dict(sys.modules, {"jnius": None})
367
+ monkeypatch.setattr(sys, "path", [path])
368
+ with pytest.raises(RuntimeError, match=r"^cannot find the Android app folder"):
369
+ _ = Android().user_data_dir
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import importlib
4
+ import re
4
5
  import sys
5
6
  from pathlib import Path
6
7
  from typing import TYPE_CHECKING, Any
@@ -124,7 +125,7 @@ def test_ios_ensure_exists_without_home_creates_nothing(
124
125
  monkeypatch.delenv("HOME", raising=False)
125
126
  mocker.patch.object(pwd, "getpwuid", side_effect=KeyError("getpwuid(): uid not found"))
126
127
  monkeypatch.chdir(tmp_path)
127
- with pytest.raises(RuntimeError, match=r"^could not determine the home directory, refusing to create '~/"):
128
+ with pytest.raises(RuntimeError, match=r"^could not determine the home directory"):
128
129
  _ = IOS(appname="app", ensure_exists=True).user_data_dir
129
130
  assert list(tmp_path.iterdir()) == []
130
131
 
@@ -141,3 +142,22 @@ def test_ios_ensure_exists_without_home_creates_nothing(
141
142
  def test_ios_ignores_xdg_variables(monkeypatch: pytest.MonkeyPatch, container: Path, env_var: str, func: str) -> None:
142
143
  monkeypatch.setenv(env_var, "/xdg")
143
144
  assert Path(getattr(IOS(), func)).is_relative_to(container)
145
+
146
+
147
+ @pytest.mark.usefixtures("_unknown_home")
148
+ @pytest.mark.parametrize(
149
+ ("prop", "path"),
150
+ [
151
+ pytest.param("user_data_dir", "~/Library/Application Support", id="data"),
152
+ pytest.param("user_config_dir", "~/Library/Application Support", id="config"),
153
+ pytest.param("user_cache_dir", "~/Library/Caches", id="cache"),
154
+ pytest.param("user_log_dir", "~/Library/Logs", id="log"),
155
+ pytest.param("user_runtime_dir", "~/tmp", id="runtime"),
156
+ pytest.param("user_documents_dir", "~/Documents", id="documents"),
157
+ pytest.param("user_applications_dir", "~/Applications", id="applications"),
158
+ pytest.param("user_fonts_dir", "~/Library/Fonts", id="fonts"),
159
+ ],
160
+ )
161
+ def test_without_home_ios_dir_raises(prop: str, path: str) -> None:
162
+ with pytest.raises(RuntimeError, match=re.escape(f"could not determine the home directory for {path!r}")):
163
+ _ = getattr(IOS(appname="app", version="1.0"), prop)
@@ -10,6 +10,7 @@ import pytest
10
10
  import platformdirs
11
11
  from platformdirs.android import Android
12
12
  from platformdirs.testing import isolated_dirs
13
+ from platformdirs.unix import Unix
13
14
 
14
15
  if TYPE_CHECKING:
15
16
  from collections.abc import Callable
@@ -141,3 +142,57 @@ def test_isolated_dirs_nested_restores_outer(tmp_path: Path) -> None:
141
142
  with isolated_dirs(tmp_path / "inner"):
142
143
  pass
143
144
  assert platformdirs.user_data_path("app") == tmp_path / "outer" / "user_data" / "app"
145
+
146
+
147
+ @pytest.mark.usefixtures("_unix_root")
148
+ @pytest.mark.parametrize(
149
+ ("kind", "suffix"),
150
+ [
151
+ pytest.param("user_data", "app/1.0", id="data"),
152
+ pytest.param("user_config", "app/1.0", id="config"),
153
+ pytest.param("user_preference", "app/1.0", id="preference"),
154
+ pytest.param("user_applications", "", id="applications"),
155
+ ],
156
+ )
157
+ @pytest.mark.parametrize("multipath", [pytest.param(False, id="single"), pytest.param(True, id="multiple")])
158
+ def test_isolated_dirs_root_paths_follow_redirected_dirs(
159
+ tmp_path: Path, kind: str, suffix: str, multipath: bool
160
+ ) -> None:
161
+ dirs = Unix("app", version="1.0", multipath=multipath, use_site_for_root=True)
162
+ expected = tmp_path / kind / suffix
163
+ with isolated_dirs(tmp_path):
164
+ assert getattr(dirs, f"{kind}_dir") == str(expected)
165
+ assert getattr(dirs, f"{kind}_path") == expected
166
+
167
+
168
+ @pytest.mark.usefixtures("_unix_root")
169
+ def test_isolated_dirs_root_iter_config_paths(tmp_path: Path) -> None:
170
+ with isolated_dirs(tmp_path):
171
+ assert list(Unix("app", use_site_for_root=True).iter_config_paths()) == [
172
+ tmp_path / "user_config/app",
173
+ tmp_path / "site_config/app",
174
+ ]
175
+
176
+
177
+ @pytest.mark.usefixtures("_unix_root")
178
+ def test_isolated_dirs_restores_root_redirection(tmp_path: Path, mocker: MockerFixture) -> None:
179
+ mocker.patch.dict(os.environ, {"XDG_CONFIG_HOME": "/user", "XDG_CONFIG_DIRS": "/site"})
180
+ dirs = Unix("app", use_site_for_root=True)
181
+ with isolated_dirs(tmp_path):
182
+ pass
183
+ assert dirs.user_config_dir == dirs.site_config_dir
184
+
185
+
186
+ @pytest.mark.usefixtures("_unix_root")
187
+ def test_isolated_dirs_nested_root_restores_after_exception(tmp_path: Path) -> None:
188
+ dirs = Unix("app", use_site_for_root=True)
189
+ with isolated_dirs(tmp_path):
190
+ with pytest.raises(LookupError), isolated_dirs(tmp_path / "inner"):
191
+ raise LookupError
192
+ assert dirs.user_config_path == tmp_path / "user_config/app"
193
+
194
+
195
+ @pytest.fixture
196
+ def _unix_root(mocker: MockerFixture) -> None:
197
+ mocker.patch("platformdirs.PlatformDirs", Unix)
198
+ mocker.patch("platformdirs.unix.getuid", return_value=0)
@@ -34,7 +34,7 @@ def _reload_after_test() -> typing.Iterator[None]:
34
34
 
35
35
 
36
36
  @pytest.fixture
37
- def _as_root(mocker: MockerFixture) -> None:
37
+ def _as_root(mocker: MockerFixture, _clear_xdg_env: None) -> None:
38
38
  mocker.patch("platformdirs.unix.getuid", return_value=0)
39
39
 
40
40
 
@@ -148,6 +148,14 @@ def test_get_win_folder_from_env_vars_public_fallback(mocker: MockerFixture, env
148
148
  assert get_win_folder_from_env_vars("CSIDL_PUBLIC") == str(Path("C:/Users/Public"))
149
149
 
150
150
 
151
+ def test_user_projects_dir_raises_without_home(mocker: MockerFixture) -> None:
152
+ # os.path.expanduser leaves the tilde in place when the home is unknown (only Path.expanduser raises),
153
+ # so the Projects directory must not come back as a literal ~ path.
154
+ mocker.patch.object(os.path, "expanduser", return_value="~/Projects")
155
+ with pytest.raises(RuntimeError, match=r"^could not determine the home directory for '~/Projects'"):
156
+ _ = Windows().user_projects_dir
157
+
158
+
151
159
  def test_roaming_uses_appdata(mocker: MockerFixture) -> None:
152
160
  mock = mocker.patch("platformdirs.windows.get_win_folder", side_effect=lambda csidl: _WIN_FOLDERS[csidl])
153
161
  _result = Windows(appname="foo", roaming=True).user_data_dir
File without changes
File without changes
File without changes
File without changes