platformdirs 4.12.2__tar.gz → 4.12.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.12.2 → platformdirs-4.12.4}/PKG-INFO +1 -1
  2. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/api.py +24 -16
  3. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/testing.py +2 -2
  4. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/unix.py +1 -1
  5. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/version.py +2 -2
  6. {platformdirs-4.12.2 → platformdirs-4.12.4}/tests/test_api.py +13 -0
  7. {platformdirs-4.12.2 → platformdirs-4.12.4}/tests/test_unix.py +30 -4
  8. {platformdirs-4.12.2 → platformdirs-4.12.4}/.gitignore +0 -0
  9. {platformdirs-4.12.2 → platformdirs-4.12.4}/LICENSE +0 -0
  10. {platformdirs-4.12.2 → platformdirs-4.12.4}/README.md +0 -0
  11. {platformdirs-4.12.2 → platformdirs-4.12.4}/pyproject.toml +0 -0
  12. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/__init__.py +0 -0
  13. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/__main__.py +0 -0
  14. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/_xdg.py +0 -0
  15. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/android.py +0 -0
  16. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/ios.py +0 -0
  17. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/macos.py +0 -0
  18. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/py.typed +0 -0
  19. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/pytest_plugin.py +0 -0
  20. {platformdirs-4.12.2 → platformdirs-4.12.4}/src/platformdirs/windows.py +0 -0
  21. {platformdirs-4.12.2 → platformdirs-4.12.4}/tests/conftest.py +0 -0
  22. {platformdirs-4.12.2 → platformdirs-4.12.4}/tests/test_android.py +0 -0
  23. {platformdirs-4.12.2 → platformdirs-4.12.4}/tests/test_comp_with_appdirs.py +0 -0
  24. {platformdirs-4.12.2 → platformdirs-4.12.4}/tests/test_ios.py +0 -0
  25. {platformdirs-4.12.2 → platformdirs-4.12.4}/tests/test_macos.py +0 -0
  26. {platformdirs-4.12.2 → platformdirs-4.12.4}/tests/test_main.py +0 -0
  27. {platformdirs-4.12.2 → platformdirs-4.12.4}/tests/test_pytest_plugin.py +0 -0
  28. {platformdirs-4.12.2 → platformdirs-4.12.4}/tests/test_testing.py +0 -0
  29. {platformdirs-4.12.2 → platformdirs-4.12.4}/tests/test_windows.py +0 -0
  30. {platformdirs-4.12.2 → platformdirs-4.12.4}/tox.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: platformdirs
3
- Version: 4.12.2
3
+ Version: 4.12.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
@@ -555,10 +555,12 @@ class PlatformDirsABC(ABC): # ruff:ignore[too-many-public-methods]
555
555
  yield Path(path)
556
556
 
557
557
  def place_config_file(self, name: str | os.PathLike[str]) -> Path:
558
- """Return the path of ``name`` under `user_config_dir`, creating the missing directories on the way.
558
+ """Return the path of ``name`` under `user_config_path`, creating the missing directories on the way.
559
559
 
560
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. It does not create the file.
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.
562
564
 
563
565
  :param name: file path relative to the user directory, e.g. ``"sub/app.toml"``.
564
566
 
@@ -566,41 +568,47 @@ class PlatformDirsABC(ABC): # ruff:ignore[too-many-public-methods]
566
568
  :raises RuntimeError: if the user directory starts with ``~`` because the home directory is unknown.
567
569
 
568
570
  """
569
- return self._place_file(lambda dirs: dirs.user_config_dir, name)
571
+ return self._place_file(lambda dirs: dirs.user_config_path, name)
570
572
 
571
573
  def place_data_file(self, name: str | os.PathLike[str]) -> Path:
572
- """Like `place_config_file`, under `user_data_dir`."""
573
- return self._place_file(lambda dirs: dirs.user_data_dir, name)
574
+ """Like `place_config_file`, under `user_data_path`."""
575
+ return self._place_file(lambda dirs: dirs.user_data_path, name)
574
576
 
575
577
  def place_cache_file(self, name: str | os.PathLike[str]) -> Path:
576
- """Like `place_config_file`, under `user_cache_dir`."""
577
- return self._place_file(lambda dirs: dirs.user_cache_dir, name)
578
+ """Like `place_config_file`, under `user_cache_path`."""
579
+ return self._place_file(lambda dirs: dirs.user_cache_path, name)
578
580
 
579
581
  def place_state_file(self, name: str | os.PathLike[str]) -> Path:
580
- """Like `place_config_file`, under `user_state_dir`."""
581
- return self._place_file(lambda dirs: dirs.user_state_dir, name)
582
+ """Like `place_config_file`, under `user_state_path`."""
583
+ return self._place_file(lambda dirs: dirs.user_state_path, name)
582
584
 
583
585
  def place_log_file(self, name: str | os.PathLike[str]) -> Path:
584
- """Like `place_config_file`, under `user_log_dir`."""
585
- return self._place_file(lambda dirs: dirs.user_log_dir, name)
586
+ """Like `place_config_file`, under `user_log_path`."""
587
+ return self._place_file(lambda dirs: dirs.user_log_path, name)
586
588
 
587
589
  def place_runtime_file(self, name: str | os.PathLike[str]) -> Path:
588
- """Like `place_config_file`, under `user_runtime_dir`.
590
+ """Like `place_config_file`, under `user_runtime_path`.
589
591
 
590
592
  :raises PermissionError: on Unix, if another user owns the temporary fallback directory.
591
593
 
592
594
  """
593
- return self._place_file(lambda dirs: dirs.user_runtime_dir, name)
595
+ return self._place_file(lambda dirs: dirs.user_runtime_path, name)
594
596
 
595
- def _place_file(self, user_dir: Callable[[PlatformDirsABC], str], name: str | os.PathLike[str]) -> Path:
597
+ def _place_file(self, user_path: Callable[[PlatformDirsABC], Path], name: str | os.PathLike[str]) -> Path:
596
598
  _ensure_inside_base("name", relative := os.fspath(name))
597
599
  if not Path(relative).parts:
598
600
  msg = f"name must point to a file, got {relative!r}"
599
601
  raise ValueError(msg)
600
602
  # Under ensure_exists the lookup itself would create the user directory with the default mode, not 0o700.
601
603
  (lookup := copy(self)).ensure_exists = False
602
- _ensure_home_known(directory := user_dir(lookup))
603
- _make_private_directories((path := Path(directory, relative)).parent)
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)
604
612
  return path
605
613
 
606
614
  def find_config_file(self, name: str | os.PathLike[str]) -> Path | None:
@@ -11,7 +11,7 @@ from platformdirs.api import PlatformDirsABC
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  import os
14
- from collections.abc import Iterator
14
+ from collections.abc import Generator
15
15
 
16
16
  _APP_KINDS: Final[frozenset[str]] = frozenset({
17
17
  "user_data",
@@ -31,7 +31,7 @@ _APP_KINDS: Final[frozenset[str]] = frozenset({
31
31
 
32
32
 
33
33
  @contextmanager
34
- def isolated_dirs(root: str | os.PathLike[str]) -> Iterator[Path]:
34
+ def isolated_dirs(root: str | os.PathLike[str]) -> Generator[Path, None, None]:
35
35
  """Resolve every directory of :data:`~platformdirs.PlatformDirs` under ``root`` while the context is active.
36
36
 
37
37
  Each kind maps to ``<root>/<kind>``, where ``kind`` is the property name without ``_dir``, such as ``user_config``.
@@ -275,7 +275,7 @@ class Unix(XDGMixin, _UnixDefaults):
275
275
  if not (path := _xdg_dir("XDG_RUNTIME_DIR")):
276
276
  reason = "XDG_RUNTIME_DIR is not set"
277
277
  else:
278
- with suppress(FileExistsError):
278
+ with suppress(OSError):
279
279
  self._optionally_create_directory(path, private=True)
280
280
  if not (problem := _runtime_dir_problem(path)):
281
281
  return self._append_app_name_and_version(path, private=True)
@@ -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.12.2'
22
- __version_tuple__ = version_tuple = (4, 12, 2)
21
+ __version__ = version = '4.12.4'
22
+ __version_tuple__ = version_tuple = (4, 12, 4)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -475,6 +475,19 @@ def test_find_config_file_accepts_nested_path_like(dirs: Unix, config_paths: lis
475
475
  assert dirs.find_config_file(Path("sub", "app.toml")) == nested
476
476
 
477
477
 
478
+ @pytest.mark.parametrize("multipath", [pytest.param(True, id="multipath"), pytest.param(False, id="single-path")])
479
+ @pytest.mark.parametrize("kind", [pytest.param(kind, id=kind) for kind in ("config", "data")])
480
+ def test_find_file_finds_root_placed_file_in_first_site_dir(
481
+ dirs: Unix, mocker: MockerFixture, kind: str, multipath: bool
482
+ ) -> None:
483
+ mocker.patch("platformdirs.unix.getuid", return_value=0)
484
+ dirs.use_site_for_root = True
485
+ dirs.multipath = multipath
486
+ getattr(dirs, f"place_{kind}_file")("app.toml").touch()
487
+ first_site_dir = os.environ[f"XDG_{kind.upper()}_DIRS"].split(os.pathsep)[0]
488
+ assert getattr(dirs, f"find_{kind}_file")("app.toml") == Path(first_site_dir, "find-files-test", "app.toml")
489
+
490
+
478
491
  @pytest.mark.parametrize("suffix", _SUFFIXES)
479
492
  @pytest.mark.parametrize("kind", _KINDS)
480
493
  def test_find_creates_no_directory_with_ensure_exists(dirs: Unix, tmp_path: Path, kind: str, suffix: str) -> None:
@@ -400,6 +400,10 @@ _PROBLEMS: typing.Final = {
400
400
  "mode-755": "has mode 0755, not 0700",
401
401
  "mode-500": "has mode 0500, not 0700",
402
402
  }
403
+ _UNREACHABLE: typing.Final = {
404
+ "file": "Not a directory",
405
+ "symlink-loop": "Too many levels of symbolic links",
406
+ }
403
407
  _DEFAULT_PREFIXES: typing.Final = {
404
408
  "linux": "run/user",
405
409
  "freebsd": "var/run/user",
@@ -472,17 +476,20 @@ def test_runtime_dir_xdg_runtime_dir_of_other_user_warns(
472
476
 
473
477
  @_POSIX_ONLY
474
478
  @pytest.mark.usefixtures("runtime_uid")
479
+ @pytest.mark.parametrize("kind", list(_UNREACHABLE))
480
+ @pytest.mark.parametrize("ensure_exists", [pytest.param(False, id="lazy"), pytest.param(True, id="ensure-exists")])
475
481
  def test_runtime_dir_unreachable_xdg_runtime_dir_warns(
476
- runtime_temp_dir: Path, temp_dir: Path, monkeypatch: pytest.MonkeyPatch
482
+ runtime_temp_dir: Path, temp_dir: Path, monkeypatch: pytest.MonkeyPatch, kind: str, ensure_exists: bool
477
483
  ) -> None:
478
- (blocker := runtime_temp_dir / "blocker").touch()
484
+ _create(blocker := runtime_temp_dir / "blocker", kind)
479
485
  monkeypatch.setenv("XDG_RUNTIME_DIR", str(blocker / "session"))
480
486
  with pytest.warns(RuntimeDirWarning) as record:
481
- _ = Unix(appname="app").user_runtime_dir
487
+ assert Unix(appname="app", ensure_exists=ensure_exists).user_runtime_dir == str(temp_dir / "app")
482
488
  expected: typing.Final = (
483
- f"XDG_RUNTIME_DIR {blocker / 'session'} cannot be checked: Not a directory, falling back to {temp_dir}"
489
+ f"XDG_RUNTIME_DIR {blocker / 'session'} cannot be checked: {_UNREACHABLE[kind]}, falling back to {temp_dir}"
484
490
  )
485
491
  assert [str(warning.message) for warning in record] == [expected]
492
+ assert (temp_dir / "app").is_dir() is ensure_exists
486
493
 
487
494
 
488
495
  @_POSIX_ONLY
@@ -602,6 +609,8 @@ def _create(path: Path, kind: str) -> None:
602
609
  path.symlink_to(target)
603
610
  case "file":
604
611
  path.touch()
612
+ case "symlink-loop":
613
+ path.symlink_to(path)
605
614
 
606
615
 
607
616
  def test_ensure_exists_creates_folder(monkeypatch: pytest.MonkeyPatch, posix_tmp_path: str) -> None:
@@ -1226,6 +1235,23 @@ def test_user_path_site_redirect(
1226
1235
  assert getattr(dirs, prop) == Path(base) / ("applications" if prop == "user_applications_path" else "foo/1.0")
1227
1236
 
1228
1237
 
1238
+ @pytest.mark.skipif(sys.platform == "win32", reason="Windows ignores POSIX mode bits")
1239
+ @pytest.mark.parametrize("uid", [pytest.param(0, id="root"), pytest.param(1000, id="user")], indirect=True)
1240
+ @pytest.mark.parametrize("kind", ["config", "data"])
1241
+ def test_place_file_with_use_site_for_root_keeps_site_dir_readable(
1242
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, uid: int, kind: str
1243
+ ) -> None:
1244
+ monkeypatch.setenv(f"XDG_{kind.upper()}_HOME", str(tmp_path / "user"))
1245
+ monkeypatch.setenv(f"XDG_{kind.upper()}_DIRS", str(tmp_path / "site"))
1246
+ getattr(Unix(appname="foo", use_site_for_root=True), f"place_{kind}_file")("sub/app.toml")
1247
+ (reference := tmp_path / "reference").mkdir()
1248
+ expected: typing.Final = stat.S_IMODE(reference.stat().st_mode) if uid == 0 else 0o700
1249
+ created: typing.Final = (tmp_path / ("site" if uid == 0 else "user")).rglob("*")
1250
+ assert {path.name: oct(stat.S_IMODE(path.stat().st_mode)) for path in created} == dict.fromkeys(
1251
+ ["foo", "sub"], oct(expected)
1252
+ )
1253
+
1254
+
1229
1255
  @pytest.fixture
1230
1256
  def uid(request: pytest.FixtureRequest, mocker: MockerFixture) -> int:
1231
1257
  value: typing.Final = typing.cast("int", request.param)
File without changes
File without changes
File without changes
File without changes