Skip to content

GH-73991: Rework pathlib.Path.rmtree() into delete() #122368

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 7, 2024
49 changes: 28 additions & 21 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1636,7 +1636,7 @@ Copying, renaming and deleting
.. method:: Path.unlink(missing_ok=False)

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

If *missing_ok* is false (the default), :exc:`FileNotFoundError` is
raised if the path does not exist.
Expand All @@ -1650,33 +1650,40 @@ Copying, renaming and deleting

.. method:: Path.rmdir()

Remove this directory. The directory must be empty.
Remove this directory. The directory must be empty; use
:meth:`Path.delete` to remove a non-empty directory.


.. 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.
Delete this file or directory. If this path refers to a non-empty
directory, its files and sub-directories are deleted recursively.

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
*on_error*, it will be called each time an exception is raised. If neither
*ignore_errors* nor *on_error* are supplied, exceptions are propagated to
the caller.
If *ignore_errors* is true, errors resulting from failed deletions will be
ignored. If *ignore_errors* is false or omitted, and a callable is given as
the optional *on_error* argument, it will be called with one argument of
type :exc:`OSError` each time an exception is raised. The callable can
handle the error to continue the deletion process or re-raise it to stop.
Note that the filename is available as the :attr:`~OSError.filename`
attribute of the exception object. If neither *ignore_errors* nor
*on_error* are supplied, exceptions are propagated to the caller.

.. 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
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.

If the optional argument *on_error* is specified, it should be a callable;
it will be called with one argument of type :exc:`OSError`. The
callable can handle the error to continue the deletion process or re-raise
it to stop. Note that the filename is available as the :attr:`~OSError.filename`
attribute of the exception object.
When deleting non-empty directories on platforms that lack the necessary
file descriptor-based functions, the :meth:`~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. Applications
can use the :data:`~Path.delete.avoids_symlink_attacks` method attribute
to determine whether the implementation is immune to this attack.

.. attribute:: delete.avoids_symlink_attacks

Indicates whether the current platform and implementation provides a
symlink attack resistant version of :meth:`~Path.delete`. Currently
this is only true for platforms supporting fd-based directory access
functions.

.. versionadded:: 3.14

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
29 changes: 15 additions & 14 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -919,30 +919,26 @@ 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.
Delete this file or directory (including all sub-directories).

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 *ignore_errors* is true, exceptions raised from scanning the
filesystem 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 ignore_errors:
def on_error(err):
pass
elif on_error is None:
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 self.is_dir(follow_symlinks=False):
results = self.walk(
on_error=on_error,
top_down=False, # Bottom-up so we rmdir() empty directories.
top_down=False, # So we rmdir() empty directories.
follow_symlinks=False)
for dirpath, dirnames, filenames in results:
for name in filenames:
Expand All @@ -955,10 +951,15 @@ def on_error(err):
dirpath.joinpath(name).rmdir()
except OSError as err:
on_error(err)
self.rmdir()
delete_self = self.rmdir
else:
delete_self = self.unlink
try:
delete_self()
except OSError as err:
err.filename = str(self)
on_error(err)
delete.avoids_symlink_attacks = False

def owner(self, *, follow_symlinks=True):
"""
Expand Down
39 changes: 25 additions & 14 deletions Lib/pathlib/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import operator
import os
import posixpath
import shutil
import sys
from glob import _StringGlobber
from itertools import chain
Expand Down Expand Up @@ -830,24 +831,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.
Delete this file or directory (including all sub-directories).

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 *ignore_errors* is true, exceptions raised from scanning the
filesystem 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)
shutil.rmtree(str(self), ignore_errors, onexc=onexc)
else:
try:
self.unlink()
except OSError as err:
if not ignore_errors:
if on_error:
on_error(err)
else:
raise

delete.avoids_symlink_attacks = shutil.rmtree.avoids_symlink_attacks

def rename(self, target):
"""
Expand Down
Loading
Loading