Skip to content

Commit a19ae2a

Browse files
authored
Merge pull request #5691 from nicoddemus/backport-5627
[4.6] Handle only known functions in rm_rf (#5627)
2 parents 829941a + 0274c08 commit a19ae2a

File tree

2 files changed

+92
-42
lines changed

2 files changed

+92
-42
lines changed

src/_pytest/pathlib.py

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import sys
1212
import uuid
1313
import warnings
14+
from functools import partial
1415
from functools import reduce
1516
from os.path import expanduser
1617
from os.path import expandvars
@@ -46,38 +47,53 @@ def ensure_reset_dir(path):
4647
path.mkdir()
4748

4849

49-
def rm_rf(path):
50-
"""Remove the path contents recursively, even if some elements
51-
are read-only.
52-
"""
50+
def on_rm_rf_error(func, path, exc, **kwargs):
51+
"""Handles known read-only errors during rmtree."""
52+
start_path = kwargs["start_path"]
53+
excvalue = exc[1]
5354

54-
def chmod_w(p):
55-
import stat
55+
if not isinstance(excvalue, OSError) or excvalue.errno not in (
56+
errno.EACCES,
57+
errno.EPERM,
58+
):
59+
warnings.warn(
60+
PytestWarning("(rm_rf) error removing {}: {}".format(path, excvalue))
61+
)
62+
return
5663

57-
mode = os.stat(str(p)).st_mode
58-
os.chmod(str(p), mode | stat.S_IWRITE)
64+
if func not in (os.rmdir, os.remove, os.unlink):
65+
warnings.warn(
66+
PytestWarning("(rm_rf) error removing {}: {}".format(path, excvalue))
67+
)
68+
return
5969

60-
def force_writable_and_retry(function, p, excinfo):
61-
p = Path(p)
70+
# Chmod + retry.
71+
import stat
6272

63-
# for files, we need to recursively go upwards
64-
# in the directories to ensure they all are also
65-
# writable
66-
if p.is_file():
67-
for parent in p.parents:
68-
chmod_w(parent)
69-
# stop when we reach the original path passed to rm_rf
70-
if parent == path:
71-
break
73+
def chmod_rw(p):
74+
mode = os.stat(p).st_mode
75+
os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR)
7276

73-
chmod_w(p)
74-
try:
75-
# retry the function that failed
76-
function(str(p))
77-
except Exception as e:
78-
warnings.warn(PytestWarning("(rm_rf) error removing {}: {}".format(p, e)))
77+
# For files, we need to recursively go upwards in the directories to
78+
# ensure they all are also writable.
79+
p = Path(path)
80+
if p.is_file():
81+
for parent in p.parents:
82+
chmod_rw(str(parent))
83+
# stop when we reach the original path passed to rm_rf
84+
if parent == start_path:
85+
break
86+
chmod_rw(str(path))
87+
88+
func(path)
7989

80-
shutil.rmtree(str(path), onerror=force_writable_and_retry)
90+
91+
def rm_rf(path):
92+
"""Remove the path contents recursively, even if some elements
93+
are read-only.
94+
"""
95+
onerror = partial(on_rm_rf_error, start_path=path)
96+
shutil.rmtree(str(path), onerror=onerror)
8197

8298

8399
def find_prefixed(root, prefix):

testing/test_tmpdir.py

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from __future__ import division
44
from __future__ import print_function
55

6+
import errno
67
import os
78
import stat
89
import sys
@@ -319,7 +320,20 @@ def test_cleanup_locked(self, tmp_path):
319320
p, consider_lock_dead_if_created_before=p.stat().st_mtime + 1
320321
)
321322

322-
def test_rmtree(self, tmp_path):
323+
def test_cleanup_ignores_symlink(self, tmp_path):
324+
the_symlink = tmp_path / (self.PREFIX + "current")
325+
attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5"))
326+
self._do_cleanup(tmp_path)
327+
328+
def test_removal_accepts_lock(self, tmp_path):
329+
folder = pathlib.make_numbered_dir(root=tmp_path, prefix=self.PREFIX)
330+
pathlib.create_cleanup_lock(folder)
331+
pathlib.maybe_delete_a_numbered_dir(folder)
332+
assert folder.is_dir()
333+
334+
335+
class TestRmRf:
336+
def test_rm_rf(self, tmp_path):
323337
from _pytest.pathlib import rm_rf
324338

325339
adir = tmp_path / "adir"
@@ -335,7 +349,7 @@ def test_rmtree(self, tmp_path):
335349
rm_rf(adir)
336350
assert not adir.exists()
337351

338-
def test_rmtree_with_read_only_file(self, tmp_path):
352+
def test_rm_rf_with_read_only_file(self, tmp_path):
339353
"""Ensure rm_rf can remove directories with read-only files in them (#5524)"""
340354
from _pytest.pathlib import rm_rf
341355

@@ -344,38 +358,58 @@ def test_rmtree_with_read_only_file(self, tmp_path):
344358

345359
fn.touch()
346360

347-
mode = os.stat(str(fn)).st_mode
348-
os.chmod(str(fn), mode & ~stat.S_IWRITE)
361+
self.chmod_r(fn)
349362

350363
rm_rf(fn.parent)
351364

352365
assert not fn.parent.is_dir()
353366

354-
def test_rmtree_with_read_only_directory(self, tmp_path):
367+
def chmod_r(self, path):
368+
mode = os.stat(str(path)).st_mode
369+
os.chmod(str(path), mode & ~stat.S_IWRITE)
370+
371+
def test_rm_rf_with_read_only_directory(self, tmp_path):
355372
"""Ensure rm_rf can remove read-only directories (#5524)"""
356373
from _pytest.pathlib import rm_rf
357374

358375
adir = tmp_path / "dir"
359376
adir.mkdir()
360377

361378
(adir / "foo.txt").touch()
362-
mode = os.stat(str(adir)).st_mode
363-
os.chmod(str(adir), mode & ~stat.S_IWRITE)
379+
self.chmod_r(adir)
364380

365381
rm_rf(adir)
366382

367383
assert not adir.is_dir()
368384

369-
def test_cleanup_ignores_symlink(self, tmp_path):
370-
the_symlink = tmp_path / (self.PREFIX + "current")
371-
attempt_symlink_to(the_symlink, tmp_path / (self.PREFIX + "5"))
372-
self._do_cleanup(tmp_path)
385+
def test_on_rm_rf_error(self, tmp_path):
386+
from _pytest.pathlib import on_rm_rf_error
373387

374-
def test_removal_accepts_lock(self, tmp_path):
375-
folder = pathlib.make_numbered_dir(root=tmp_path, prefix=self.PREFIX)
376-
pathlib.create_cleanup_lock(folder)
377-
pathlib.maybe_delete_a_numbered_dir(folder)
378-
assert folder.is_dir()
388+
adir = tmp_path / "dir"
389+
adir.mkdir()
390+
391+
fn = adir / "foo.txt"
392+
fn.touch()
393+
self.chmod_r(fn)
394+
395+
# unknown exception
396+
with pytest.warns(pytest.PytestWarning):
397+
exc_info = (None, RuntimeError(), None)
398+
on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path)
399+
assert fn.is_file()
400+
401+
permission_error = OSError()
402+
permission_error.errno = errno.EACCES
403+
404+
# unknown function
405+
with pytest.warns(pytest.PytestWarning):
406+
exc_info = (None, permission_error, None)
407+
on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path)
408+
assert fn.is_file()
409+
410+
exc_info = (None, permission_error, None)
411+
on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path)
412+
assert not fn.is_file()
379413

380414

381415
def attempt_symlink_to(path, to_path):

0 commit comments

Comments
 (0)