8000 GH-73991: Add `pathlib.Path.rmtree()` (#119060) · python/cpython@094375b · GitHub
[go: up one dir, main page]

Skip to content

Commit 094375b

Browse files
barneygalepicnixz
andauthored
GH-73991: Add pathlib.Path.rmtree() (#119060)
Add a `Path.rmtree()` method that removes an entire directory tree, like `shutil.rmtree()`. The signature of the optional *on_error* argument matches the `Path.walk()` argument of the same name, but differs from the *onexc* and *onerror* arguments to `shutil.rmtree()`. Consistency within pathlib is probably more important. In the private pathlib ABCs, we add an implementation based on `walk()`. Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
1 parent 8db5f48 commit 094375b

File tree

7 files changed

+448
-5
lines changed

7 files changed

+448
-5
lines changed

Doc/library/pathlib.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1645,6 +1645,34 @@ Copying, renaming and deleting
16451645
Remove this directory. The directory must be empty.
16461646

16471647

1648+
.. method:: Path.rmtree(ignore_errors=False, on_error=None)
1649+
1650+
Recursively delete this entire directory tree. The path must not refer to a symlink.
1651+
1652+
If *ignore_errors* is true, errors resulting from failed removals will be
1653+
ignored. If *ignore_errors* is false or omitted, and a function is given to
1654+
*on_error*, it will be called each time an exception is raised. If neither
1655+
*ignore_errors* nor *on_error* are supplied, exceptions are propagated to
1656+
the caller.
1657+
1658+
.. note::
1659+
1660+
On platforms that support the necessary fd-based functions, a symlink
1661+
attack-resistant version of :meth:`~Path.rmtree` is used by default. On
1662+
other platforms, the :func:`~Path.rmtree` implementation is susceptible
1663+
to a symlink attack: given proper timing and circumstances, attackers
1664+
can manipulate symlinks on the filesystem to delete files they would not
1665+
be able to access otherwise.
1666+
1667+
If the optional argument *on_error* is specified, it should be a callable;
1668+
it will be called with one argument of type :exc:`OSError`. The
1669+
callable can handle the error to continue the deletion process or re-raise
1670+
it to stop. Note that the filename is available as the :attr:`~OSError.filename`
1671+
attribute of the exception object.
1672+
1673+
.. versionadded:: 3.14
1674+
1675+
16481676
Permissions and ownership
16491677
^^^^^^^^^^^^^^^^^^^^^^^^^
16501678

Doc/whatsnew/3.14.rst

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,15 @@ os
118118
pathlib
119119
-------
120120

121-
* Add :meth:`pathlib.Path.copy`, which copies the content of one file to
122-
another, like :func:`shutil.copyfile`.
123-
(Contributed by Barney Gale in :gh:`73991`.)
124-
* Add :meth:`pathlib.Path.copytree`, which copies one directory tree to
125-
another.
121+
* Add methods to :class:`pathlib.Path` to recursively copy or remove files:
122+
123+
* :meth:`~pathlib.Path.copy` copies the content of one file to another, like
124+
:func:`shutil.copyfile`.
125+
* :meth:`~pathlib.Path.copytree` copies one directory tree to another, like
126+
:func:`shutil.copytree`.
127+
* :meth:`~pathlib.Path.rmtree` recursively removes a directory tree, like
128+
:func:`shutil.rmtree`.
129+
126130
(Contributed by Barney Gale in :gh:`73991`.)
127131

128132
pdb

Lib/pathlib/_abc.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,47 @@ def rmdir(self):
915915
"""
916916
raise UnsupportedOperation(self._unsupported_msg('rmdir()'))
917917

918+
def rmtree(self, ignore_errors=False, on_error=None):
919+
"""
920+
Recursively delete this directory tree.
921+
922+
If *ignore_errors* is true, exceptions raised from scanning the tree
923+
and removing files and directories are ignored. Otherwise, if
924+
*on_error* is set, it will be called to handle the error. If neither
925+
*ignore_errors* nor *on_error* are set, exceptions are propagated to
926+
the caller.
927+
"""
928+
if ignore_errors:
929+
def on_error(err):
930+
pass
931+
elif on_error is None:
932+
def on_error(err):
933+
raise err
934+
try:
935+
if self.is_symlink():
936+
raise OSError("Cannot call rmtree on a symbolic link")
937+
elif self.is_junction():
938+
raise OSError("Cannot call rmtree on a junction")
939+
results = self.walk(
940+
on_error=on_error,
941+
top_down=False, # Bottom-up so we rmdir() empty directories.
942+
follow_symlinks=False)
943+
for dirpath, dirnames, filenames in results:
944+
for name in filenames:
945+
try:
946+
dirpath.joinpath(name).unlink()
947+
except OSError as err:
948+
on_error(err)
949+
for name in dirnames:
950+
try:
951+
dirpath.joinpath(name).rmdir()
952+
except OSError as err:
953+
on_error(err)
954+
self.rmdir()
955+
except OSError as err:
956+
err.filename = str(self)
957+
on_error(err)
958+
918959
def owner(self, *, follow_symlinks=True):
919960
"""
920961
Return the login name of the file owner.

Lib/pathlib/_local.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -830,6 +830,25 @@ def rmdir(self):
830830
"""
831831
os.rmdir(self)
832832

833+
def rmtree(self, ignore_errors=False, on_error=None):
834+
"""
835+
Recursively delete this directory tree.
836+
837+
If *ignore_errors* is true, exceptions raised from scanning the tree
838+
and removing files and directories are ignored. Otherwise, if
839+
*on_error* is set, it will be called to handle the error. If neither
840+
*ignore_errors* nor *on_error* are set, exceptions are propagated to
841+
the caller.
842+
"""
843+
if on_error:
844+
def onexc(func, filename, err):
845+
err.filename = filename
846+
on_error(err)
847+
else:
848+
onexc = None
849+
import shutil
850+
shutil.rmtree(str(self), ignore_errors, onexc=onexc)
851+
833852
def rename(self, target):
834853
"""
835854
Rename this path to the target path.

0 commit comments

Comments
 (0)
0