Skip to content

GH-73991: Add pathlib.Path.rmtree() #119060

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 23 commits into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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
28 changes: 28 additions & 0 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,34 @@ Copying, renaming and deleting
Remove this directory. The directory must be empty.


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

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

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.

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

.. versionadded:: 3.14


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

Expand Down
14 changes: 9 additions & 5 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,15 @@ os
pathlib
-------

* Add :meth:`pathlib.Path.copy`, which copies the content of one file to
another, like :func:`shutil.copyfile`.
(Contributed by Barney Gale in :gh:`73991`.)
* Add :meth:`pathlib.Path.copytree`, which copies one directory tree to
another.
* Add methods to :class:`pathlib.Path` to recursively copy or remove files:

* :meth:`~pathlib.Path.copy` copies the content of one file to another, like
: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`.

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

pdb
Expand Down
41 changes: 41 additions & 0 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,47 @@ def rmdir(self):
"""
raise UnsupportedOperation(self._unsupported_msg('rmdir()'))

def rmtree(self, ignore_errors=False, on_error=None):
"""
Recursively delete this 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 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")
results = self.walk(
on_error=on_error,
top_down=False, # Bottom-up so we rmdir() empty directories.
follow_symlinks=False)
for dirpath, dirnames, filenames in results:
for name in filenames:
try:
dirpath.joinpath(name).unlink()
except OSError as err:
on_error(err)
for name in dirnames:
try:
dirpath.joinpath(name).rmdir()
except OSError as err:
on_error(err)
self.rmdir()
except OSError as err:
err.filename = str(self)
on_error(err)

def owner(self, *, follow_symlinks=True):
"""
Return the login name of the file owner.
Expand Down
19 changes: 19 additions & 0 deletions Lib/pathlib/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,25 @@ def rmdir(self):
"""
os.rmdir(self)

def rmtree(self, ignore_errors=False, on_error=None):
"""
Recursively delete this 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:
onexc = None
import shutil
shutil.rmtree(str(self), ignore_errors, onexc=onexc)

def rename(self, target):
"""
Rename this path to the target path.
Expand Down
Loading
Loading