Skip to content

Commit 3a03d81

Browse files
committedAug 19, 2024·
pythonGH-73991: Prune pathlib.Path.delete()
Remove the *ignore_errors* and *on_error* arguments from `Path.delete()`. This functionality was carried over from `shutil`, but its design needs to be re-considered in its new context. For example, we may wish to support a *missing_ok* argument (like `Path.unlink()`), or automatically `chmod()` and retry operations when we hit a permission error (like `tempfile.TemporaryDirectory`), or retry operations with a backoff (like `test.support.os_helper.rmtree()`), or utilise exception groups, etc. It's best to leave our options open for now.
1 parent b0f462d commit 3a03d81

File tree

5 files changed

+9
-129
lines changed

5 files changed

+9
-129
lines changed
 

‎Doc/library/pathlib.rst

+1-10
Original file line numberDiff line numberDiff line change
@@ -1637,20 +1637,11 @@ Copying, renaming and deleting
16371637
:meth:`Path.delete` to remove a non-empty directory.
16381638

16391639

1640-
.. method:: Path.delete(ignore_errors=False, on_error=None)
1640+
.. method:: Path.delete()
16411641

16421642
Delete this file or directory. If this path refers to a non-empty
16431643
directory, its files and sub-directories are deleted recursively.
16441644

1645-
If *ignore_errors* is true, errors resulting from failed deletions will be
1646-
ignored. If *ignore_errors* is false or omitted, and a callable is given as
1647-
the optional *on_error* argument, it will be called with one argument of
1648-
type :exc:`OSError` each time an exception is raised. The callable can
1649-
handle the error to continue the deletion process or re-raise it to stop.
1650-
Note that the filename is available as the :attr:`~OSError.filename`
1651-
attribute of the exception object. If neither *ignore_errors* nor
1652-
*on_error* are supplied, exceptions are propagated to the caller.
1653-
16541645
.. note::
16551646

16561647
When deleting non-empty directories on platforms that lack the necessary

‎Lib/pathlib/_abc.py

+4-19
Original file line numberDiff line numberDiff line change
@@ -923,23 +923,13 @@ def rmdir(self):
923923
"""
924924
raise UnsupportedOperation(self._unsupported_msg('rmdir()'))
925925

926-
def delete(self, ignore_errors=False, on_error=None):
926+
def delete(self):
927927
"""
928928
Delete this file or directory (including all sub-directories).
929-
930-
If *ignore_errors* is true, exceptions raised from scanning the
931-
filesystem and removing files and directories are ignored. Otherwise,
932-
if *on_error* is set, it will be called to handle the error. If
933-
neither *ignore_errors* nor *on_error* are set, exceptions are
934-
propagated to the caller.
935929
"""
936-
if ignore_errors:
937-
def on_error(err):
938-
pass
939-
elif on_error is None:
930+
if self.is_dir(follow_symlinks=False):
940931
def on_error(err):
941932
raise err
942-
if self.is_dir(follow_symlinks=False):
943933
results = self.walk(
944934
on_error=on_error,
945935
top_down=False, # So we rmdir() empty directories.
@@ -955,14 +945,9 @@ def on_error(err):
955945
dirpath.joinpath(name).rmdir()
956946
except OSError as err:
957947
on_error(err)
958-
delete_self = self.rmdir
948+
self.rmdir()
959949
else:
960-
delete_self = self.unlink
961-
try:
962-
delete_self()
963-
except OSError as err:
964-
err.filename = str(self)
965-
on_error(err)
950+
self.unlink()
966951
delete.avoids_symlink_attacks = False
967952

968953
def owner(self, *, follow_symlinks=True):

‎Lib/pathlib/_local.py

+3-21
Original file line numberDiff line numberDiff line change
@@ -824,32 +824,14 @@ def rmdir(self):
824824
"""
825825
os.rmdir(self)
826826

827-
def delete(self, ignore_errors=False, on_error=None):
827+
def delete(self):
828828
"""
829829
Delete this file or directory (including all sub-directories).
830-
831-
If *ignore_errors* is true, exceptions raised from scanning the
832-
filesystem and removing files and directories are ignored. Otherwise,
833-
if *on_error* is set, it will be called to handle the error. If
834-
neither *ignore_errors* nor *on_error* are set, exceptions are
835-
propagated to the caller.
836830
"""
837831
if self.is_dir(follow_symlinks=False):
838-
onexc = None
839-
if on_error:
840-
def onexc(func, filename, err):
841-
err.filename = filename
842-
on_error(err)
843-
shutil.rmtree(str(self), ignore_errors, onexc=onexc)
832+
shutil.rmtree(str(self))
844833
else:
845-
try:
846-
self.unlink()
847-
except OSError as err:
848-
if not ignore_errors:
849-
if on_error:
850-
on_error(err)
851-
else:
852-
raise
834+
self.unlink()
853835

854836
delete.avoids_symlink_attacks = shutil.rmtree.avoids_symlink_attacks
855837

‎Lib/test/test_pathlib/test_pathlib.py

+1-71
Original file line numberDiff line numberDiff line change
@@ -904,10 +904,7 @@ def test_delete_unwritable(self):
904904
child_dir_path.chmod(new_mode)
905905
tmp.chmod(new_mode)
906906

907-
errors = []
908-
tmp.delete(on_error=errors.append)
909-
# Test whether onerror has actually been called.
910-
self.assertEqual(len(errors), 3)
907+
self.assertRaises(PermissionError, tmp.delete)
911908
finally:
912909
tmp.chmod(old_dir_mode)
913910
child_file_path.chmod(old_child_file_mode)
@@ -1003,16 +1000,6 @@ def close(fd):
10031000
self.assertTrue(dir2.is_dir())
10041001
self.assertEqual(close_count, 2)
10051002

1006-
close_count = 0
1007-
errors = []
1008-
1009-
with swap_attr(os, 'close', close) as orig_close:
1010-
dir1.delete(on_error=errors.append)
1011-
self.assertEqual(len(errors), 2)
1012-
self.assertEqual(errors[0].filename, str(dir2))
1013-
self.assertEqual(errors[1].filename, str(dir1))
1014-
self.assertEqual(close_count, 2)
1015-
10161003
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
10171004
@unittest.skipIf(sys.platform == "vxworks",
10181005
"fifo requires special path on VxWorks")
@@ -1028,63 +1015,6 @@ def test_delete_on_named_pipe(self):
10281015
p.delete()
10291016
self.assertFalse(p.exists())
10301017

1031-
@unittest.skipIf(sys.platform[:6] == 'cygwin',
1032-
"This test can't be run on Cygwin (issue #1071513).")
1033-
@os_helper.skip_if_dac_override
1034-
@os_helper.skip_unless_working_chmod
1035-
def test_delete_deleted_race_condition(self):
1036-
# bpo-37260
1037-
#
1038-
# Test that a file or a directory deleted after it is enumerated
1039-
# by scandir() but before unlink() or rmdr() is called doesn't
1040-
# generate any errors.
1041-
def on_error(exc):
1042-
assert exc.filename
1043-
if not isinstance(exc, PermissionError):
1044-
raise
1045-
# Make the parent and the children writeable.
1046-
for p, mode in zip(paths, old_modes):
1047-
p.chmod(mode)
1048-
# Remove other dirs except one.
1049-
keep = next(p for p in dirs if str(p) != exc.filename)
1050-
for p in dirs:
1051-
if p != keep:
1052-
p.rmdir()
1053-
# Remove other files except one.
1054-
keep = next(p for p in files if str(p) != exc.filename)
1055-
for p in files:
1056-
if p != keep:
1057-
p.unlink()
1058-
1059-
tmp = self.cls(self.base, 'delete')
1060-
tmp.mkdir()
1061-
paths = [tmp] + [tmp / f'child{i}' for i in range(6)]
1062-
dirs = paths[1::2]
1063-
files = paths[2::2]
1064-
for path in dirs:
1065-
path.mkdir()
1066-
for path in files:
1067-
path.write_text('')
1068-
1069-
old_modes = [path.stat().st_mode for path in paths]
1070-
1071-
# Make the parent and the children non-writeable.
1072-
new_mode = stat.S_IREAD | stat.S_IEXEC
1073-
for path in reversed(paths):
1074-
path.chmod(new_mode)
1075-
1076-
try:
1077-
tmp.delete(on_error=on_error)
1078-
except:
1079-
# Test failed, so cleanup artifacts.
1080-
for path, mode in zip(paths, old_modes):
1081-
try:
1082-
path.chmod(mode)
1083-
except OSError:
1084-
pass
1085-
tmp.delete()
1086-
raise
1087-
10881018
def test_delete_does_not_choke_on_failing_lstat(self):
10891019
try:
10901020
orig_lstat = os.lstat

‎Lib/test/test_pathlib/test_pathlib_abc.py

-8
Original file line numberDiff line numberDiff line change
@@ -2705,14 +2705,6 @@ def test_delete_missing(self):
27052705
# filename is guaranteed not to exist
27062706
filename = tmp / 'foo'
27072707
self.assertRaises(FileNotFoundError, filename.delete)
2708-
# test that ignore_errors option is honored
2709-
filename.delete(ignore_errors=True)
2710-
# test on_error
2711-
errors = []
2712-
filename.delete(on_error=errors.append)
2713-
self.assertEqual(len(errors), 1)
2714-
self.assertIsInstance(errors[0], FileNotFoundError)
2715-
self.assertEqual(errors[0].filename, str(filename))
27162708

27172709
def setUpWalk(self):
27182710
# Build:

0 commit comments

Comments
 (0)
Please sign in to comment.