From 411d04d238e91b8513a07806a557e4a4fcea09a8 Mon Sep 17 00:00:00 2001 From: 0x5t4l1n Date: Sun, 25 Oct 2026 00:52:26 +0530 Subject: [PATCH] fix: keep relative isolation roots stable across chdir --- docs/changelog/+relative-isolation.bugfix.rst | 2 ++ docs/howto.rst | 8 +++++--- src/platformdirs/testing.py | 7 ++++--- tests/test_testing.py | 18 ++++++++++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 docs/changelog/+relative-isolation.bugfix.rst diff --git a/docs/changelog/+relative-isolation.bugfix.rst b/docs/changelog/+relative-isolation.bugfix.rst new file mode 100644 index 00000000..10e02254 --- /dev/null +++ b/docs/changelog/+relative-isolation.bugfix.rst @@ -0,0 +1,2 @@ +Pin a relative ``isolated_dirs`` root on context entry so changing the working directory keeps directory lookups and +creation under the original isolation root. The context now yields an absolute path. diff --git a/docs/howto.rst b/docs/howto.rst index 98204e21..167faafc 100644 --- a/docs/howto.rst +++ b/docs/howto.rst @@ -244,9 +244,11 @@ isolates under ``tmp_path`` and yields the root: assert my_app_init(PlatformDirs("MyApp")) == {"theme": "dark"} Each kind lands in ``/``, for example ``/user_config/MyApp``, and the module-level functions follow -the same layout. On Unix, isolation ignores ``use_site_for_root`` so user and site kinds stay separate even for root. -The redirection covers :data:`~platformdirs.PlatformDirs` in the current process only: a subprocess, or another platform -class such as :class:`~platformdirs.unix.Unix` on macOS, still resolves the real directories. +the same layout. A relative root is made absolute when entering the context and the yielded root is absolute, so +changing the working directory inside the context keeps all paths under the original root. On Unix, isolation ignores +``use_site_for_root`` so user and site kinds stay separate even for root. The redirection covers +:data:`~platformdirs.PlatformDirs` in the current process only: a subprocess, or another platform class such as +:class:`~platformdirs.unix.Unix` on macOS, still resolves the real directories. Overriding directories with environment variables ================================================= diff --git a/src/platformdirs/testing.py b/src/platformdirs/testing.py index 914a84e9..e1fa0ef0 100644 --- a/src/platformdirs/testing.py +++ b/src/platformdirs/testing.py @@ -39,14 +39,15 @@ def isolated_dirs(root: str | os.PathLike[str]) -> Generator[Path, None, None]: ``PlatformDirs("app", version="1.0").user_config_path`` becomes ``/user_config/app/1.0``. Every thread of the current process sees the change, and subprocesses do not. - On Unix, ``use_site_for_root`` is ignored inside the context so user and site directories keep separate kinds. + On Unix, ``use_site_for_root`` is ignored inside the context so user and site directories keep separate kinds. A + relative ``root`` is made absolute on entry, so changing the working directory does not move the isolation root. :param root: directory that holds the redirected directories - :returns: ``root`` as a :class:`~pathlib.Path` + :returns: the absolute isolation root as a :class:`~pathlib.Path` """ - base = Path(root) + base = Path(root).absolute() patches = { name: _redirect( base / (kind := name.removesuffix("_dir")), app=kind in _APP_KINDS, private=kind.startswith("user_") diff --git a/tests/test_testing.py b/tests/test_testing.py index 91288691..0f105e51 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -52,6 +52,24 @@ def test_isolated_dirs_yields_root_as_path(tmp_path: Path) -> None: assert root == tmp_path +@pytest.mark.parametrize("root_name", [".", "isolated"]) +def test_isolated_dirs_relative_root_survives_chdir( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, root_name: str +) -> None: + monkeypatch.chdir(tmp_path) + expected = tmp_path / root_name + (other := tmp_path / "other").mkdir() + with isolated_dirs(root_name) as root: + monkeypatch.chdir(other) + assert _outside(expected) == {} + assert root == expected + dirs = platformdirs.PlatformDirs("app", ensure_exists=True) + assert dirs.user_config_path == expected / "user_config" / "app" + assert dirs.user_config_path.is_dir() + assert not (other / "user_config").exists() + assert not (other / "isolated").exists() + + def test_isolated_dirs_redirects_every_accessor(tmp_path: Path) -> None: with isolated_dirs(tmp_path): assert _outside(tmp_path) == {}