Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/15341.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Temporary directory cleanup now removes empty directories beneath read-only parents instead of leaving the session's temporary files behind.
6 changes: 3 additions & 3 deletions src/_pytest/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,9 @@ def on_rm_rf_error(
return False

# Chmod + retry.
# For files, we need to recursively go upwards in the directories to
# ensure they all are also accessible and writable.
if p.is_file():
# Removing a file or an empty directory requires write access to its
# parent, which may also be inaccessible through its own parents.
if p.is_file() or func is os.rmdir:
for parent in p.parents: # pragma: no branch
_chmod_rwx(str(parent))
# Stop when we reach the original path passed to rm_rf.
Expand Down
34 changes: 34 additions & 0 deletions testing/test_tmpdir.py
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,40 @@ def test_on_rm_rf_error_chmod_retry_walks_parents(self, tmp_path: Path) -> None:
on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path)
assert not fn.exists()

@pytest.mark.skipif(not hasattr(os, "getuid"), reason="unix permissions")
def test_rm_rf_empty_dir_under_read_only_parent(self, tmp_path: Path) -> None:
root = tmp_path / "root"
parent = root / "parent"
empty = parent / "empty"
empty.mkdir(parents=True)
parent.chmod(0o500)

try:
rm_rf(root)
assert not root.exists()
finally:
if parent.exists():
parent.chmod(0o700)

def test_on_rm_rf_error_chmods_empty_dir_parents(
self, tmp_path: Path, monkeypatch: MonkeyPatch
) -> None:
parent = tmp_path / "parent"
empty = parent / "empty"
empty.mkdir(parents=True)
chmod_paths: list[str] = []

def record_chmod(path: str) -> bool:
chmod_paths.append(path)
return _chmod_rwx(path)

monkeypatch.setattr(pathlib, "_chmod_rwx", record_chmod)
assert on_rm_rf_error(
os.rmdir, str(empty), PermissionError(), start_path=tmp_path
)
assert str(parent) in chmod_paths
assert not empty.exists()


def attempt_symlink_to(path, to_path):
"""Try to make a symlink from "path" to "to_path", skipping in case this platform
Expand Down
Loading