Skip to content

Commit d06605d

Browse files
barneygaleblhsing
authored andcommitted
pythonGH-73991: Rework pathlib.Path.rmtree() into delete() (python#122368)
Rename `pathlib.Path.rmtree()` to `delete()`, and add support for deleting non-directories. This simplifies the interface for users, and nicely complements the upcoming `move()` and `copy()` methods (which will also accept any type of file.)
1 parent ad03484 commit d06605d

File tree

7 files changed

+140
-150
lines changed

7 files changed

+140
-150
lines changed

Doc/library/pathlib.rst

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1636,7 +1636,7 @@ Copying, renaming and deleting
16361636
.. method:: Path.unlink(missing_ok=False)
16371637

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

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

16511651
.. method:: Path.rmdir()
16521652

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

16551656

1656-
.. method:: Path.rmtree(ignore_errors=False, on_error=None)
1657+
.. method:: Path.delete(ignore_errors=False, on_error=None)
16571658

1658-
Recursively delete this entire directory tree. The path must not refer to a symlink.
1659+
Delete this file or directory. If this path refers to a non-empty
1660+
directory, its files and sub-directories are deleted recursively.
16591661

1660-
If *ignore_errors* is true, errors resulting from failed removals will be
1661-
ignored. If *ignore_errors* is false or omitted, and a function is given to
1662-
*on_error*, it will be called each time an exception is raised. If neither
1663-
*ignore_errors* nor *on_error* are supplied, exceptions are propagated to
1664-
the caller.
1662+
If *ignore_errors* is true, errors resulting from failed deletions will be
1663+
ignored. If *ignore_errors* is false or omitted, and a callable is given as
1664+
the optional *on_error* argument, it will be called with one argument of
1665+
type :exc:`OSError` each time an exception is raised. The callable can
1666+
handle the error to continue the deletion process or re-raise it to stop.
1667+
Note that the filename is available as the :attr:`~OSError.filename`
1668+
attribute of the exception object. If neither *ignore_errors* nor
1669+
*on_error* are supplied, exceptions are propagated to the caller.
16651670

16661671
.. note::
16671672

1668-
On platforms that support the necessary fd-based functions, a symlink
1669-
attack-resistant version of :meth:`~Path.rmtree` is used by default. On
1670-
other platforms, the :func:`~Path.rmtree` implementation is susceptible
1671-
to a symlink attack: given proper timing and circumstances, attackers
1672-
can manipulate symlinks on the filesystem to delete files they would not
1673-
be able to access otherwise.
1674-
1675-
If the optional argument *on_error* is specified, it should be a callable;
1676-
it will be called with one argument of type :exc:`OSError`. The
1677-
callable can handle the error to continue the deletion process or re-raise
1678-
it to stop. Note that the filename is available as the :attr:`~OSError.filename`
1679-
attribute of the exception object.
1673+
When deleting non-empty directories on platforms that lack the necessary
1674+
file descriptor-based functions, the :meth:`~Path.delete` implementation
1675+
is susceptible to a symlink attack: given proper timing and
1676+
circumstances, attackers can manipulate symlinks on the filesystem to
1677+
delete files they would not be able to access otherwise. Applications
1678+
can use the :data:`~Path.delete.avoids_symlink_attacks` method attribute
1679+
to determine whether the implementation is immune to this attack.
1680+
1681+
.. attribute:: delete.avoids_symlink_attacks
1682+
1683+
Indicates whether the current platform and implementation provides a
1684+
symlink attack resistant version of :meth:`~Path.delete`. Currently
1685+
this is only true for platforms supporting fd-based directory access
1686+
functions.
16801687

16811688
.. versionadded:: 3.14
16821689

Doc/whatsnew/3.14.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,7 @@ pathlib
141141
:func:`shutil.copyfile`.
142142
* :meth:`~pathlib.Path.copytree` copies one directory tree to another, like
143143
:func:`shutil.copytree`.
144-
* :meth:`~pathlib.Path.rmtree` recursively removes a directory tree, like
145-
:func:`shutil.rmtree`.
144+
* :meth:`~pathlib.Path.delete` removes a file or directory tree.
146145

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

Lib/pathlib/_abc.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -919,30 +919,26 @@ def rmdir(self):
919919
"""
920920
raise UnsupportedOperation(self._unsupported_msg('rmdir()'))
921921

922-
def rmtree(self, ignore_errors=False, on_error=None):
922+
def delete(self, ignore_errors=False, on_error=None):
923923
"""
924-
Recursively delete this directory tree.
924+
Delete this file or directory (including all sub-directories).
925925
926-
If *ignore_errors* is true, exceptions raised from scanning the tree
927-
and removing files and directories are ignored. Otherwise, if
928-
*on_error* is set, it will be called to handle the error. If neither
929-
*ignore_errors* nor *on_error* are set, exceptions are propagated to
930-
the caller.
926+
If *ignore_errors* is true, exceptions raised from scanning the
927+
filesystem and removing files and directories are ignored. Otherwise,
928+
if *on_error* is set, it will be called to handle the error. If
929+
neither *ignore_errors* nor *on_error* are set, exceptions are
930+
propagated to the caller.
931931
"""
932932
if ignore_errors:
933933
def on_error(err):
934934
pass
935935
elif on_error is None:
936936
def on_error(err):
937937
raise err
938-
try:
939-
if self.is_symlink():
940-
raise OSError("Cannot call rmtree on a symbolic link")
941-
elif self.is_junction():
942-
raise OSError("Cannot call rmtree on a junction")
938+
if self.is_dir(follow_symlinks=False):
943939
results = self.walk(
944940
on_error=on_error,
945-
top_down=False, # Bottom-up so we rmdir() empty directories.
941+
top_down=False, # So we rmdir() empty directories.
946942
follow_symlinks=False)
947943
for dirpath, dirnames, filenames in results:
948944
for name in filenames:
@@ -955,10 +951,15 @@ def on_error(err):
955951
dirpath.joinpath(name).rmdir()
956952
except OSError as err:
957953
on_error(err)
958-
self.rmdir()
954+
delete_self = self.rmdir
955+
else:
956+
delete_self = self.unlink
957+
try:
958+
delete_self()
959959
except OSError as err:
960960
err.filename = str(self)
961961
on_error(err)
962+
delete.avoids_symlink_attacks = False
962963

963964
def owner(self, *, follow_symlinks=True):
964965
"""

Lib/pathlib/_local.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import operator
44
import os
55
import posixpath
6+
import shutil
67
import sys
78
from glob import _StringGlobber
89
from itertools import chain
@@ -830,24 +831,34 @@ def rmdir(self):
830831
"""
831832
os.rmdir(self)
832833

833-
def rmtree(self, ignore_errors=False, on_error=None):
834+
def delete(self, ignore_errors=False, on_error=None):
834835
"""
835-
Recursively delete this directory tree.
836+
Delete this file or directory (including all sub-directories).
836837
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.
838+
If *ignore_errors* is true, exceptions raised from scanning the
839+
filesystem and removing files and directories are ignored. Otherwise,
840+
if *on_error* is set, it will be called to handle the error. If
841+
neither *ignore_errors* nor *on_error* are set, exceptions are
842+
propagated to the caller.
842843
"""
843-
if on_error:
844-
def onexc(func, filename, err):
845-
err.filename = filename
846-
on_error(err)
847-
else:
844+
if self.is_dir(follow_symlinks=False):
848845
onexc = None
849-
import shutil
850-
shutil.rmtree(str(self), ignore_errors, onexc=onexc)
846+
if on_error:
847+
def onexc(func, filename, err):
848+
err.filename = filename
849+
on_error(err)
850+
shutil.rmtree(str(self), ignore_errors, onexc=onexc)
851+
else:
852+
try:
853+
self.unlink()
854+
except OSError as err:
855+
if not ignore_errors:
856+
if on_error:
857+
on_error(err)
858+
else:
859+
raise
860+
861+
delete.avoids_symlink_attacks = shutil.rmtree.avoids_symlink_attacks
851862

852863
def rename(self, target):
853864
"""

0 commit comments

Comments
 (0)