Skip to content
48 changes: 24 additions & 24 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1633,29 +1633,9 @@ Copying, renaming and deleting
Added return value, return the new :class:`!Path` instance.


.. method:: Path.unlink(missing_ok=False)

Remove this file or symbolic link. If the path points to a directory,
use :func:`Path.rmdir` instead.

If *missing_ok* is false (the default), :exc:`FileNotFoundError` is
raised if the path does not exist.

If *missing_ok* is true, :exc:`FileNotFoundError` exceptions will be
ignored (same behavior as the POSIX ``rm -f`` command).

.. versionchanged:: 3.8
The *missing_ok* parameter was added.


.. method:: Path.rmdir()

Remove this directory. The directory must be empty.


.. method:: Path.rmtree(ignore_errors=False, on_error=None)
.. method:: Path.delete(ignore_errors=False, on_error=None)

Recursively delete this entire directory tree. The path must not refer to a symlink.
Recursively delete this file or directory tree.

If *ignore_errors* is true, errors resulting from failed removals will be
ignored. If *ignore_errors* is false or omitted, and a function is given to
Expand All @@ -1666,8 +1646,8 @@ Copying, renaming and deleting
.. note::

On platforms that support the necessary fd-based functions, a symlink
attack-resistant version of :meth:`~Path.rmtree` is used by default. On
other platforms, the :func:`~Path.rmtree` implementation is susceptible
attack-resistant version of :meth:`~Path.delete` is used by default. On
other platforms, the :func:`~Path.delete` implementation is susceptible
to a symlink attack: given proper timing and circumstances, attackers
can manipulate symlinks on the filesystem to delete files they would not
be able to access otherwise.
Expand All @@ -1681,6 +1661,26 @@ Copying, renaming and deleting
.. versionadded:: 3.14


.. method:: Path.unlink(missing_ok=False)

Remove this file or symbolic link. If the path points to a directory,
use :func:`Path.rmdir` instead.

If *missing_ok* is false (the default), :exc:`FileNotFoundError` is
raised if the path does not exist.

If *missing_ok* is true, :exc:`FileNotFoundError` exceptions will be
ignored (same behavior as the POSIX ``rm -f`` command).

.. versionchanged:: 3.8
The *missing_ok* parameter was added.


.. method:: Path.rmdir()

Remove this directory. The directory must be empty.


Permissions and ownership
^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
3 changes: 1 addition & 2 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,7 @@ pathlib
:func:`shutil.copyfile`.
* :meth:`~pathlib.Path.copytree` copies one directory tree to another, like
:func:`shutil.copytree`.
* :meth:`~pathlib.Path.rmtree` recursively removes a directory tree, like
:func:`shutil.rmtree`.
* :meth:`~pathlib.Path.delete` removes a file or directory tree.

(Contributed by Barney Gale in :gh:`73991`.)

Expand Down
11 changes: 5 additions & 6 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,9 +919,9 @@ def rmdir(self):
"""
raise UnsupportedOperation(self._unsupported_msg('rmdir()'))

def rmtree(self, ignore_errors=False, on_error=None):
def delete(self, ignore_errors=False, on_error=None):
"""
Recursively delete this directory tree.
Recursively delete this file or directory tree.

If *ignore_errors* is true, exceptions raised from scanning the tree
and removing files and directories are ignored. Otherwise, if
Expand All @@ -936,10 +936,9 @@ def on_error(err):
def on_error(err):
raise err
try:
if self.is_symlink():
raise OSError("Cannot call rmtree on a symbolic link")
elif self.is_junction():
raise OSError("Cannot call rmtree on a junction")
if not self.is_dir(follow_symlinks=False):
self.unlink()
return
results = self.walk(
on_error=on_error,
top_down=False, # Bottom-up so we rmdir() empty directories.
Expand Down
28 changes: 19 additions & 9 deletions Lib/pathlib/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,24 +830,34 @@ def rmdir(self):
"""
os.rmdir(self)

def rmtree(self, ignore_errors=False, on_error=None):
def delete(self, ignore_errors=False, on_error=None):
"""
Recursively delete this directory tree.
Recursively delete this file or directory tree.

If *ignore_errors* is true, exceptions raised from scanning the tree
and removing files and directories are ignored. Otherwise, if
*on_error* is set, it will be called to handle the error. If neither
*ignore_errors* nor *on_error* are set, exceptions are propagated to
the caller.
"""
if on_error:
def onexc(func, filename, err):
err.filename = filename
on_error(err)
else:
if self.is_dir(follow_symlinks=False):
onexc = None
import shutil
shutil.rmtree(str(self), ignore_errors, onexc=onexc)
if on_error:
def onexc(func, filename, err):
err.filename = filename
on_error(err)
import shutil
shutil.rmtree(str(self), onexc=onexc)
else:
try:
self.unlink()
except OSError as err:
if not ignore_errors:
if on_error:
on_error(err)
else:
raise


def rename(self, target):
"""
Expand Down
74 changes: 35 additions & 39 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
if hasattr(os, 'geteuid'):
root_in_posix = (os.geteuid() == 0)

rmtree_use_fd_functions = (
delete_use_fd_functions = (
{os.open, os.stat, os.unlink, os.rmdir} <= os.supports_dir_fd and
os.listdir in os.supports_fd and os.stat in os.supports_follow_symlinks)

Expand Down Expand Up @@ -862,8 +862,8 @@ def test_group_no_follow_symlinks(self):
self.assertEqual(expected_gid, gid_2)
self.assertEqual(expected_name, link.group(follow_symlinks=False))

def test_rmtree_uses_safe_fd_version_if_available(self):
if rmtree_use_fd_functions:
def test_delete_uses_safe_fd_version_if_available(self):
if delete_use_fd_functions:
d = self.cls(self.base, 'a')
d.mkdir()
try:
Expand All @@ -876,16 +876,16 @@ def _raiser(*args, **kwargs):
raise Called

os.open = _raiser
self.assertRaises(Called, d.rmtree)
self.assertRaises(Called, d.delete)
finally:
os.open = real_open

@unittest.skipIf(sys.platform[:6] == 'cygwin',
"This test can't be run on Cygwin (issue #1071513).")
@os_helper.skip_if_dac_override
@os_helper.skip_unless_working_chmod
def test_rmtree_unwritable(self):
tmp = self.cls(self.base, 'rmtree')
def test_delete_unwritable(self):
tmp = self.cls(self.base, 'delete')
tmp.mkdir()
child_file_path = tmp / 'a'
child_dir_path = tmp / 'b'
Expand All @@ -902,7 +902,7 @@ def test_rmtree_unwritable(self):
tmp.chmod(new_mode)

errors = []
tmp.rmtree(on_error=errors.append)
tmp.delete(on_error=errors.append)
# Test whether onerror has actually been called.
self.assertEqual(len(errors), 3)
finally:
Expand All @@ -911,9 +911,9 @@ def test_rmtree_unwritable(self):
child_dir_path.chmod(old_child_dir_mode)

@needs_windows
def test_rmtree_inner_junction(self):
def test_delete_inner_junction(self):
import _winapi
tmp = self.cls(self.base, 'rmtree')
tmp = self.cls(self.base, 'delete')
tmp.mkdir()
dir1 = tmp / 'dir1'
dir2 = dir1 / 'dir2'
Expand All @@ -929,15 +929,15 @@ def test_rmtree_inner_junction(self):
link3 = dir1 / 'link3'
_winapi.CreateJunction(str(file1), str(link3))
# make sure junctions are removed but not followed
dir1.rmtree()
dir1.delete()
self.assertFalse(dir1.exists())
self.assertTrue(dir3.exists())
self.assertTrue(file1.exists())

@needs_windows
def test_rmtree_outer_junction(self):
def test_delete_outer_junction(self):
import _winapi
tmp = self.cls(self.base, 'rmtree')
tmp = self.cls(self.base, 'delete')
tmp.mkdir()
try:
src = tmp / 'cheese'
Expand All @@ -946,41 +946,41 @@ def test_rmtree_outer_junction(self):
spam = src / 'spam'
spam.write_text('')
_winapi.CreateJunction(str(src), str(dst))
self.assertRaises(OSError, dst.rmtree)
dst.rmtree(ignore_errors=True)
self.assertRaises(OSError, dst.delete)
dst.delete(ignore_errors=True)
finally:
tmp.rmtree(ignore_errors=True)
tmp.delete(ignore_errors=True)

@needs_windows
def test_rmtree_outer_junction_on_error(self):
def test_delete_outer_junction_on_error(self):
import _winapi
tmp = self.cls(self.base, 'rmtree')
tmp = self.cls(self.base, 'delete')
tmp.mkdir()
dir_ = tmp / 'dir'
dir_.mkdir()
link = tmp / 'link'
_winapi.CreateJunction(str(dir_), str(link))
try:
self.assertRaises(OSError, link.rmtree)
self.assertRaises(OSError, link.delete)
self.assertTrue(dir_.exists())
self.assertTrue(link.exists(follow_symlinks=False))
errors = []

def on_error(error):
errors.append(error)

link.rmtree(on_error=on_error)
link.delete(on_error=on_error)
self.assertEqual(len(errors), 1)
self.assertIsInstance(errors[0], OSError)
self.assertEqual(errors[0].filename, str(link))
finally:
os.unlink(str(link))

@unittest.skipUnless(rmtree_use_fd_functions, "requires safe rmtree")
def test_rmtree_fails_on_close(self):
@unittest.skipUnless(delete_use_fd_functions, "requires safe delete")
def test_delete_fails_on_close(self):
# Test that the error handler is called for failed os.close() and that
# os.close() is only called once for a file descriptor.
tmp = self.cls(self.base, 'rmtree')
tmp = self.cls(self.base, 'delete')
tmp.mkdir()
dir1 = tmp / 'dir1'
dir1.mkdir()
Expand All @@ -996,15 +996,15 @@ def close(fd):
close_count = 0
with swap_attr(os, 'close', close) as orig_close:
with self.assertRaises(OSError):
dir1.rmtree()
dir1.delete()
self.assertTrue(dir2.is_dir())
self.assertEqual(close_count, 2)

close_count = 0
errors = []

with swap_attr(os, 'close', close) as orig_close:
dir1.rmtree(on_error=errors.append)
dir1.delete(on_error=errors.append)
self.assertEqual(len(errors), 2)
self.assertEqual(errors[0].filename, str(dir2))
self.assertEqual(errors[1].filename, str(dir1))
Expand All @@ -1013,27 +1013,23 @@ def close(fd):
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
@unittest.skipIf(sys.platform == "vxworks",
"fifo requires special path on VxWorks")
def test_rmtree_on_named_pipe(self):
def test_delete_on_named_pipe(self):
p = self.cls(self.base, 'pipe')
os.mkfifo(p)
try:
with self.assertRaises(NotADirectoryError):
p.rmtree()
self.assertTrue(p.exists())
finally:
p.unlink()
p.delete()
self.assertFalse(p.exists())

p = self.cls(self.base, 'dir')
p.mkdir()
os.mkfifo(p / 'mypipe')
p.rmtree()
p.delete()
self.assertFalse(p.exists())

@unittest.skipIf(sys.platform[:6] == 'cygwin',
"This test can't be run on Cygwin (issue #1071513).")
@os_helper.skip_if_dac_override
@os_helper.skip_unless_working_chmod
def test_rmtree_deleted_race_condition(self):
def test_delete_deleted_race_condition(self):
# bpo-37260
#
# Test that a file or a directory deleted after it is enumerated
Expand All @@ -1057,7 +1053,7 @@ def on_error(exc):
if p != keep:
p.unlink()

tmp = self.cls(self.base, 'rmtree')
tmp = self.cls(self.base, 'delete')
tmp.mkdir()
paths = [tmp] + [tmp / f'child{i}' for i in range(6)]
dirs = paths[1::2]
Expand All @@ -1075,21 +1071,21 @@ def on_error(exc):
path.chmod(new_mode)

try:
tmp.rmtree(on_error=on_error)
tmp.delete(on_error=on_error)
except:
# Test failed, so cleanup artifacts.
for path, mode in zip(paths, old_modes):
try:
path.chmod(mode)
except OSError:
pass
tmp.rmtree()
tmp.delete()
raise

def test_rmtree_does_not_choke_on_failing_lstat(self):
def test_delete_does_not_choke_on_failing_lstat(self):
try:
orig_lstat = os.lstat
tmp = self.cls(self.base, 'rmtree')
tmp = self.cls(self.base, 'delete')

def raiser(fn, *args, **kwargs):
if fn != str(tmp):
Expand All @@ -1102,7 +1098,7 @@ def raiser(fn, *args, **kwargs):
tmp.mkdir()
foo = tmp / 'foo'
foo.write_text('')
tmp.rmtree()
tmp.delete()
finally:
os.lstat = orig_lstat

Expand Down
Loading