mirror of
https://github.com/python/cpython.git
synced 2025-07-24 03:35:53 +00:00
GH-73991: Rework pathlib.Path.rmtree()
into delete()
(#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.)
This commit is contained in:
parent
b5e142ba7c
commit
98dba73010
7 changed files with 139 additions and 149 deletions
|
@ -1636,7 +1636,7 @@ Copying, renaming and deleting
|
||||||
.. method:: Path.unlink(missing_ok=False)
|
.. method:: Path.unlink(missing_ok=False)
|
||||||
|
|
||||||
Remove this file or symbolic link. If the path points to a directory,
|
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
|
If *missing_ok* is false (the default), :exc:`FileNotFoundError` is
|
||||||
raised if the path does not exist.
|
raised if the path does not exist.
|
||||||
|
@ -1650,33 +1650,40 @@ Copying, renaming and deleting
|
||||||
|
|
||||||
.. method:: Path.rmdir()
|
.. 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
|
If *ignore_errors* is true, errors resulting from failed deletions will be
|
||||||
ignored. If *ignore_errors* is false or omitted, and a function is given to
|
ignored. If *ignore_errors* is false or omitted, and a callable is given as
|
||||||
*on_error*, it will be called each time an exception is raised. If neither
|
the optional *on_error* argument, it will be called with one argument of
|
||||||
*ignore_errors* nor *on_error* are supplied, exceptions are propagated to
|
type :exc:`OSError` each time an exception is raised. The callable can
|
||||||
the caller.
|
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::
|
.. note::
|
||||||
|
|
||||||
On platforms that support the necessary fd-based functions, a symlink
|
When deleting non-empty directories on platforms that lack the necessary
|
||||||
attack-resistant version of :meth:`~Path.rmtree` is used by default. On
|
file descriptor-based functions, the :meth:`~Path.delete` implementation
|
||||||
other platforms, the :func:`~Path.rmtree` implementation is susceptible
|
is susceptible to a symlink attack: given proper timing and
|
||||||
to a symlink attack: given proper timing and circumstances, attackers
|
circumstances, attackers can manipulate symlinks on the filesystem to
|
||||||
can manipulate symlinks on the filesystem to delete files they would not
|
delete files they would not be able to access otherwise. Applications
|
||||||
be able to access otherwise.
|
can use the :data:`~Path.delete.avoids_symlink_attacks` method attribute
|
||||||
|
to determine whether the implementation is immune to this attack.
|
||||||
|
|
||||||
If the optional argument *on_error* is specified, it should be a callable;
|
.. attribute:: delete.avoids_symlink_attacks
|
||||||
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
|
Indicates whether the current platform and implementation provides a
|
||||||
it to stop. Note that the filename is available as the :attr:`~OSError.filename`
|
symlink attack resistant version of :meth:`~Path.delete`. Currently
|
||||||
attribute of the exception object.
|
this is only true for platforms supporting fd-based directory access
|
||||||
|
functions.
|
||||||
|
|
||||||
.. versionadded:: 3.14
|
.. versionadded:: 3.14
|
||||||
|
|
||||||
|
|
|
@ -141,8 +141,7 @@ pathlib
|
||||||
:func:`shutil.copyfile`.
|
:func:`shutil.copyfile`.
|
||||||
* :meth:`~pathlib.Path.copytree` copies one directory tree to another, like
|
* :meth:`~pathlib.Path.copytree` copies one directory tree to another, like
|
||||||
:func:`shutil.copytree`.
|
:func:`shutil.copytree`.
|
||||||
* :meth:`~pathlib.Path.rmtree` recursively removes a directory tree, like
|
* :meth:`~pathlib.Path.delete` removes a file or directory tree.
|
||||||
:func:`shutil.rmtree`.
|
|
||||||
|
|
||||||
(Contributed by Barney Gale in :gh:`73991`.)
|
(Contributed by Barney Gale in :gh:`73991`.)
|
||||||
|
|
||||||
|
|
|
@ -919,15 +919,15 @@ class PathBase(PurePathBase):
|
||||||
"""
|
"""
|
||||||
raise UnsupportedOperation(self._unsupported_msg('rmdir()'))
|
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
|
If *ignore_errors* is true, exceptions raised from scanning the
|
||||||
and removing files and directories are ignored. Otherwise, if
|
filesystem and removing files and directories are ignored. Otherwise,
|
||||||
*on_error* is set, it will be called to handle the error. If neither
|
if *on_error* is set, it will be called to handle the error. If
|
||||||
*ignore_errors* nor *on_error* are set, exceptions are propagated to
|
neither *ignore_errors* nor *on_error* are set, exceptions are
|
||||||
the caller.
|
propagated to the caller.
|
||||||
"""
|
"""
|
||||||
if ignore_errors:
|
if ignore_errors:
|
||||||
def on_error(err):
|
def on_error(err):
|
||||||
|
@ -935,14 +935,10 @@ class PathBase(PurePathBase):
|
||||||
elif on_error is None:
|
elif on_error is None:
|
||||||
def on_error(err):
|
def on_error(err):
|
||||||
raise err
|
raise err
|
||||||
try:
|
if self.is_dir(follow_symlinks=False):
|
||||||
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(
|
results = self.walk(
|
||||||
on_error=on_error,
|
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)
|
follow_symlinks=False)
|
||||||
for dirpath, dirnames, filenames in results:
|
for dirpath, dirnames, filenames in results:
|
||||||
for name in filenames:
|
for name in filenames:
|
||||||
|
@ -955,10 +951,15 @@ class PathBase(PurePathBase):
|
||||||
dirpath.joinpath(name).rmdir()
|
dirpath.joinpath(name).rmdir()
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
on_error(err)
|
on_error(err)
|
||||||
self.rmdir()
|
delete_self = self.rmdir
|
||||||
|
else:
|
||||||
|
delete_self = self.unlink
|
||||||
|
try:
|
||||||
|
delete_self()
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
err.filename = str(self)
|
err.filename = str(self)
|
||||||
on_error(err)
|
on_error(err)
|
||||||
|
delete.avoids_symlink_attacks = False
|
||||||
|
|
||||||
def owner(self, *, follow_symlinks=True):
|
def owner(self, *, follow_symlinks=True):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,6 +3,7 @@ import ntpath
|
||||||
import operator
|
import operator
|
||||||
import os
|
import os
|
||||||
import posixpath
|
import posixpath
|
||||||
|
import shutil
|
||||||
import sys
|
import sys
|
||||||
from glob import _StringGlobber
|
from glob import _StringGlobber
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
@ -830,24 +831,34 @@ class Path(PathBase, PurePath):
|
||||||
"""
|
"""
|
||||||
os.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
|
If *ignore_errors* is true, exceptions raised from scanning the
|
||||||
and removing files and directories are ignored. Otherwise, if
|
filesystem and removing files and directories are ignored. Otherwise,
|
||||||
*on_error* is set, it will be called to handle the error. If neither
|
if *on_error* is set, it will be called to handle the error. If
|
||||||
*ignore_errors* nor *on_error* are set, exceptions are propagated to
|
neither *ignore_errors* nor *on_error* are set, exceptions are
|
||||||
the caller.
|
propagated to the caller.
|
||||||
"""
|
"""
|
||||||
if on_error:
|
if self.is_dir(follow_symlinks=False):
|
||||||
def onexc(func, filename, err):
|
|
||||||
err.filename = filename
|
|
||||||
on_error(err)
|
|
||||||
else:
|
|
||||||
onexc = None
|
onexc = None
|
||||||
import shutil
|
if on_error:
|
||||||
shutil.rmtree(str(self), ignore_errors, onexc=onexc)
|
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):
|
def rename(self, target):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -32,7 +32,7 @@ root_in_posix = False
|
||||||
if hasattr(os, 'geteuid'):
|
if hasattr(os, 'geteuid'):
|
||||||
root_in_posix = (os.geteuid() == 0)
|
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.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)
|
os.listdir in os.supports_fd and os.stat in os.supports_follow_symlinks)
|
||||||
|
|
||||||
|
@ -862,8 +862,9 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
self.assertEqual(expected_gid, gid_2)
|
self.assertEqual(expected_gid, gid_2)
|
||||||
self.assertEqual(expected_name, link.group(follow_symlinks=False))
|
self.assertEqual(expected_name, link.group(follow_symlinks=False))
|
||||||
|
|
||||||
def test_rmtree_uses_safe_fd_version_if_available(self):
|
def test_delete_uses_safe_fd_version_if_available(self):
|
||||||
if rmtree_use_fd_functions:
|
if delete_use_fd_functions:
|
||||||
|
self.assertTrue(self.cls.delete.avoids_symlink_attacks)
|
||||||
d = self.cls(self.base, 'a')
|
d = self.cls(self.base, 'a')
|
||||||
d.mkdir()
|
d.mkdir()
|
||||||
try:
|
try:
|
||||||
|
@ -876,16 +877,18 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
raise Called
|
raise Called
|
||||||
|
|
||||||
os.open = _raiser
|
os.open = _raiser
|
||||||
self.assertRaises(Called, d.rmtree)
|
self.assertRaises(Called, d.delete)
|
||||||
finally:
|
finally:
|
||||||
os.open = real_open
|
os.open = real_open
|
||||||
|
else:
|
||||||
|
self.assertFalse(self.cls.delete.avoids_symlink_attacks)
|
||||||
|
|
||||||
@unittest.skipIf(sys.platform[:6] == 'cygwin',
|
@unittest.skipIf(sys.platform[:6] == 'cygwin',
|
||||||
"This test can't be run on Cygwin (issue #1071513).")
|
"This test can't be run on Cygwin (issue #1071513).")
|
||||||
@os_helper.skip_if_dac_override
|
@os_helper.skip_if_dac_override
|
||||||
@os_helper.skip_unless_working_chmod
|
@os_helper.skip_unless_working_chmod
|
||||||
def test_rmtree_unwritable(self):
|
def test_delete_unwritable(self):
|
||||||
tmp = self.cls(self.base, 'rmtree')
|
tmp = self.cls(self.base, 'delete')
|
||||||
tmp.mkdir()
|
tmp.mkdir()
|
||||||
child_file_path = tmp / 'a'
|
child_file_path = tmp / 'a'
|
||||||
child_dir_path = tmp / 'b'
|
child_dir_path = tmp / 'b'
|
||||||
|
@ -902,7 +905,7 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
tmp.chmod(new_mode)
|
tmp.chmod(new_mode)
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
tmp.rmtree(on_error=errors.append)
|
tmp.delete(on_error=errors.append)
|
||||||
# Test whether onerror has actually been called.
|
# Test whether onerror has actually been called.
|
||||||
self.assertEqual(len(errors), 3)
|
self.assertEqual(len(errors), 3)
|
||||||
finally:
|
finally:
|
||||||
|
@ -911,9 +914,9 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
child_dir_path.chmod(old_child_dir_mode)
|
child_dir_path.chmod(old_child_dir_mode)
|
||||||
|
|
||||||
@needs_windows
|
@needs_windows
|
||||||
def test_rmtree_inner_junction(self):
|
def test_delete_inner_junction(self):
|
||||||
import _winapi
|
import _winapi
|
||||||
tmp = self.cls(self.base, 'rmtree')
|
tmp = self.cls(self.base, 'delete')
|
||||||
tmp.mkdir()
|
tmp.mkdir()
|
||||||
dir1 = tmp / 'dir1'
|
dir1 = tmp / 'dir1'
|
||||||
dir2 = dir1 / 'dir2'
|
dir2 = dir1 / 'dir2'
|
||||||
|
@ -929,15 +932,15 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
link3 = dir1 / 'link3'
|
link3 = dir1 / 'link3'
|
||||||
_winapi.CreateJunction(str(file1), str(link3))
|
_winapi.CreateJunction(str(file1), str(link3))
|
||||||
# make sure junctions are removed but not followed
|
# make sure junctions are removed but not followed
|
||||||
dir1.rmtree()
|
dir1.delete()
|
||||||
self.assertFalse(dir1.exists())
|
self.assertFalse(dir1.exists())
|
||||||
self.assertTrue(dir3.exists())
|
self.assertTrue(dir3.exists())
|
||||||
self.assertTrue(file1.exists())
|
self.assertTrue(file1.exists())
|
||||||
|
|
||||||
@needs_windows
|
@needs_windows
|
||||||
def test_rmtree_outer_junction(self):
|
def test_delete_outer_junction(self):
|
||||||
import _winapi
|
import _winapi
|
||||||
tmp = self.cls(self.base, 'rmtree')
|
tmp = self.cls(self.base, 'delete')
|
||||||
tmp.mkdir()
|
tmp.mkdir()
|
||||||
try:
|
try:
|
||||||
src = tmp / 'cheese'
|
src = tmp / 'cheese'
|
||||||
|
@ -946,22 +949,22 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
spam = src / 'spam'
|
spam = src / 'spam'
|
||||||
spam.write_text('')
|
spam.write_text('')
|
||||||
_winapi.CreateJunction(str(src), str(dst))
|
_winapi.CreateJunction(str(src), str(dst))
|
||||||
self.assertRaises(OSError, dst.rmtree)
|
self.assertRaises(OSError, dst.delete)
|
||||||
dst.rmtree(ignore_errors=True)
|
dst.delete(ignore_errors=True)
|
||||||
finally:
|
finally:
|
||||||
tmp.rmtree(ignore_errors=True)
|
tmp.delete(ignore_errors=True)
|
||||||
|
|
||||||
@needs_windows
|
@needs_windows
|
||||||
def test_rmtree_outer_junction_on_error(self):
|
def test_delete_outer_junction_on_error(self):
|
||||||
import _winapi
|
import _winapi
|
||||||
tmp = self.cls(self.base, 'rmtree')
|
tmp = self.cls(self.base, 'delete')
|
||||||
tmp.mkdir()
|
tmp.mkdir()
|
||||||
dir_ = tmp / 'dir'
|
dir_ = tmp / 'dir'
|
||||||
dir_.mkdir()
|
dir_.mkdir()
|
||||||
link = tmp / 'link'
|
link = tmp / 'link'
|
||||||
_winapi.CreateJunction(str(dir_), str(link))
|
_winapi.CreateJunction(str(dir_), str(link))
|
||||||
try:
|
try:
|
||||||
self.assertRaises(OSError, link.rmtree)
|
self.assertRaises(OSError, link.delete)
|
||||||
self.assertTrue(dir_.exists())
|
self.assertTrue(dir_.exists())
|
||||||
self.assertTrue(link.exists(follow_symlinks=False))
|
self.assertTrue(link.exists(follow_symlinks=False))
|
||||||
errors = []
|
errors = []
|
||||||
|
@ -969,18 +972,18 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
def on_error(error):
|
def on_error(error):
|
||||||
errors.append(error)
|
errors.append(error)
|
||||||
|
|
||||||
link.rmtree(on_error=on_error)
|
link.delete(on_error=on_error)
|
||||||
self.assertEqual(len(errors), 1)
|
self.assertEqual(len(errors), 1)
|
||||||
self.assertIsInstance(errors[0], OSError)
|
self.assertIsInstance(errors[0], OSError)
|
||||||
self.assertEqual(errors[0].filename, str(link))
|
self.assertEqual(errors[0].filename, str(link))
|
||||||
finally:
|
finally:
|
||||||
os.unlink(str(link))
|
os.unlink(str(link))
|
||||||
|
|
||||||
@unittest.skipUnless(rmtree_use_fd_functions, "requires safe rmtree")
|
@unittest.skipUnless(delete_use_fd_functions, "requires safe delete")
|
||||||
def test_rmtree_fails_on_close(self):
|
def test_delete_fails_on_close(self):
|
||||||
# Test that the error handler is called for failed os.close() and that
|
# Test that the error handler is called for failed os.close() and that
|
||||||
# os.close() is only called once for a file descriptor.
|
# os.close() is only called once for a file descriptor.
|
||||||
tmp = self.cls(self.base, 'rmtree')
|
tmp = self.cls(self.base, 'delete')
|
||||||
tmp.mkdir()
|
tmp.mkdir()
|
||||||
dir1 = tmp / 'dir1'
|
dir1 = tmp / 'dir1'
|
||||||
dir1.mkdir()
|
dir1.mkdir()
|
||||||
|
@ -996,7 +999,7 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
close_count = 0
|
close_count = 0
|
||||||
with swap_attr(os, 'close', close) as orig_close:
|
with swap_attr(os, 'close', close) as orig_close:
|
||||||
with self.assertRaises(OSError):
|
with self.assertRaises(OSError):
|
||||||
dir1.rmtree()
|
dir1.delete()
|
||||||
self.assertTrue(dir2.is_dir())
|
self.assertTrue(dir2.is_dir())
|
||||||
self.assertEqual(close_count, 2)
|
self.assertEqual(close_count, 2)
|
||||||
|
|
||||||
|
@ -1004,7 +1007,7 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
with swap_attr(os, 'close', close) as orig_close:
|
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(len(errors), 2)
|
||||||
self.assertEqual(errors[0].filename, str(dir2))
|
self.assertEqual(errors[0].filename, str(dir2))
|
||||||
self.assertEqual(errors[1].filename, str(dir1))
|
self.assertEqual(errors[1].filename, str(dir1))
|
||||||
|
@ -1013,27 +1016,23 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
|
@unittest.skipUnless(hasattr(os, "mkfifo"), 'requires os.mkfifo()')
|
||||||
@unittest.skipIf(sys.platform == "vxworks",
|
@unittest.skipIf(sys.platform == "vxworks",
|
||||||
"fifo requires special path on 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')
|
p = self.cls(self.base, 'pipe')
|
||||||
os.mkfifo(p)
|
os.mkfifo(p)
|
||||||
try:
|
p.delete()
|
||||||
with self.assertRaises(NotADirectoryError):
|
self.assertFalse(p.exists())
|
||||||
p.rmtree()
|
|
||||||
self.assertTrue(p.exists())
|
|
||||||
finally:
|
|
||||||
p.unlink()
|
|
||||||
|
|
||||||
p = self.cls(self.base, 'dir')
|
p = self.cls(self.base, 'dir')
|
||||||
p.mkdir()
|
p.mkdir()
|
||||||
os.mkfifo(p / 'mypipe')
|
os.mkfifo(p / 'mypipe')
|
||||||
p.rmtree()
|
p.delete()
|
||||||
self.assertFalse(p.exists())
|
self.assertFalse(p.exists())
|
||||||
|
|
||||||
@unittest.skipIf(sys.platform[:6] == 'cygwin',
|
@unittest.skipIf(sys.platform[:6] == 'cygwin',
|
||||||
"This test can't be run on Cygwin (issue #1071513).")
|
"This test can't be run on Cygwin (issue #1071513).")
|
||||||
@os_helper.skip_if_dac_override
|
@os_helper.skip_if_dac_override
|
||||||
@os_helper.skip_unless_working_chmod
|
@os_helper.skip_unless_working_chmod
|
||||||
def test_rmtree_deleted_race_condition(self):
|
def test_delete_deleted_race_condition(self):
|
||||||
# bpo-37260
|
# bpo-37260
|
||||||
#
|
#
|
||||||
# Test that a file or a directory deleted after it is enumerated
|
# Test that a file or a directory deleted after it is enumerated
|
||||||
|
@ -1057,7 +1056,7 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
if p != keep:
|
if p != keep:
|
||||||
p.unlink()
|
p.unlink()
|
||||||
|
|
||||||
tmp = self.cls(self.base, 'rmtree')
|
tmp = self.cls(self.base, 'delete')
|
||||||
tmp.mkdir()
|
tmp.mkdir()
|
||||||
paths = [tmp] + [tmp / f'child{i}' for i in range(6)]
|
paths = [tmp] + [tmp / f'child{i}' for i in range(6)]
|
||||||
dirs = paths[1::2]
|
dirs = paths[1::2]
|
||||||
|
@ -1075,7 +1074,7 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
path.chmod(new_mode)
|
path.chmod(new_mode)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tmp.rmtree(on_error=on_error)
|
tmp.delete(on_error=on_error)
|
||||||
except:
|
except:
|
||||||
# Test failed, so cleanup artifacts.
|
# Test failed, so cleanup artifacts.
|
||||||
for path, mode in zip(paths, old_modes):
|
for path, mode in zip(paths, old_modes):
|
||||||
|
@ -1083,13 +1082,13 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
path.chmod(mode)
|
path.chmod(mode)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
tmp.rmtree()
|
tmp.delete()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def test_rmtree_does_not_choke_on_failing_lstat(self):
|
def test_delete_does_not_choke_on_failing_lstat(self):
|
||||||
try:
|
try:
|
||||||
orig_lstat = os.lstat
|
orig_lstat = os.lstat
|
||||||
tmp = self.cls(self.base, 'rmtree')
|
tmp = self.cls(self.base, 'delete')
|
||||||
|
|
||||||
def raiser(fn, *args, **kwargs):
|
def raiser(fn, *args, **kwargs):
|
||||||
if fn != str(tmp):
|
if fn != str(tmp):
|
||||||
|
@ -1102,7 +1101,7 @@ class PathTest(test_pathlib_abc.DummyPathTest, PurePathTest):
|
||||||
tmp.mkdir()
|
tmp.mkdir()
|
||||||
foo = tmp / 'foo'
|
foo = tmp / 'foo'
|
||||||
foo.write_text('')
|
foo.write_text('')
|
||||||
tmp.rmtree()
|
tmp.delete()
|
||||||
finally:
|
finally:
|
||||||
os.lstat = orig_lstat
|
os.lstat = orig_lstat
|
||||||
|
|
||||||
|
|
|
@ -2641,85 +2641,43 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
self.assertFileNotFound(p.stat)
|
self.assertFileNotFound(p.stat)
|
||||||
self.assertFileNotFound(p.unlink)
|
self.assertFileNotFound(p.unlink)
|
||||||
|
|
||||||
def test_rmtree(self):
|
def test_delete_file(self):
|
||||||
|
p = self.cls(self.base) / 'fileA'
|
||||||
|
p.delete()
|
||||||
|
self.assertFileNotFound(p.stat)
|
||||||
|
self.assertFileNotFound(p.unlink)
|
||||||
|
|
||||||
|
def test_delete_dir(self):
|
||||||
base = self.cls(self.base)
|
base = self.cls(self.base)
|
||||||
base.joinpath('dirA').rmtree()
|
base.joinpath('dirA').delete()
|
||||||
self.assertRaises(FileNotFoundError, base.joinpath('dirA').stat)
|
self.assertRaises(FileNotFoundError, base.joinpath('dirA').stat)
|
||||||
self.assertRaises(FileNotFoundError, base.joinpath('dirA', 'linkC').lstat)
|
self.assertRaises(FileNotFoundError, base.joinpath('dirA', 'linkC').lstat)
|
||||||
base.joinpath('dirB').rmtree()
|
base.joinpath('dirB').delete()
|
||||||
self.assertRaises(FileNotFoundError, base.joinpath('dirB').stat)
|
self.assertRaises(FileNotFoundError, base.joinpath('dirB').stat)
|
||||||
self.assertRaises(FileNotFoundError, base.joinpath('dirB', 'fileB').stat)
|
self.assertRaises(FileNotFoundError, base.joinpath('dirB', 'fileB').stat)
|
||||||
self.assertRaises(FileNotFoundError, base.joinpath('dirB', 'linkD').lstat)
|
self.assertRaises(FileNotFoundError, base.joinpath('dirB', 'linkD').lstat)
|
||||||
base.joinpath('dirC').rmtree()
|
base.joinpath('dirC').delete()
|
||||||
self.assertRaises(FileNotFoundError, base.joinpath('dirC').stat)
|
self.assertRaises(FileNotFoundError, base.joinpath('dirC').stat)
|
||||||
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'dirD').stat)
|
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'dirD').stat)
|
||||||
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'dirD', 'fileD').stat)
|
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'dirD', 'fileD').stat)
|
||||||
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'fileC').stat)
|
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'fileC').stat)
|
||||||
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'novel.txt').stat)
|
self.assertRaises(FileNotFoundError, base.joinpath('dirC', 'novel.txt').stat)
|
||||||
|
|
||||||
def test_rmtree_errors(self):
|
|
||||||
tmp = self.cls(self.base, 'rmtree')
|
|
||||||
tmp.mkdir()
|
|
||||||
# filename is guaranteed not to exist
|
|
||||||
filename = tmp / 'foo'
|
|
||||||
self.assertRaises(FileNotFoundError, filename.rmtree)
|
|
||||||
# test that ignore_errors option is honored
|
|
||||||
filename.rmtree(ignore_errors=True)
|
|
||||||
|
|
||||||
# existing file
|
|
||||||
filename = tmp / "tstfile"
|
|
||||||
filename.write_text("")
|
|
||||||
with self.assertRaises(NotADirectoryError) as cm:
|
|
||||||
filename.rmtree()
|
|
||||||
self.assertEqual(cm.exception.filename, str(filename))
|
|
||||||
self.assertTrue(filename.exists())
|
|
||||||
# test that ignore_errors option is honored
|
|
||||||
filename.rmtree(ignore_errors=True)
|
|
||||||
self.assertTrue(filename.exists())
|
|
||||||
|
|
||||||
def test_rmtree_on_error(self):
|
|
||||||
tmp = self.cls(self.base, 'rmtree')
|
|
||||||
tmp.mkdir()
|
|
||||||
filename = tmp / "tstfile"
|
|
||||||
filename.write_text("")
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
def on_error(error):
|
|
||||||
errors.append(error)
|
|
||||||
|
|
||||||
filename.rmtree(on_error=on_error)
|
|
||||||
self.assertEqual(len(errors), 2)
|
|
||||||
# First from scandir()
|
|
||||||
self.assertIsInstance(errors[0], NotADirectoryError)
|
|
||||||
self.assertEqual(errors[0].filename, str(filename))
|
|
||||||
# Then from munlink()
|
|
||||||
self.assertIsInstance(errors[1], NotADirectoryError)
|
|
||||||
self.assertEqual(errors[1].filename, str(filename))
|
|
||||||
|
|
||||||
@needs_symlinks
|
@needs_symlinks
|
||||||
def test_rmtree_outer_symlink(self):
|
def test_delete_symlink(self):
|
||||||
tmp = self.cls(self.base, 'rmtree')
|
tmp = self.cls(self.base, 'delete')
|
||||||
tmp.mkdir()
|
tmp.mkdir()
|
||||||
dir_ = tmp / 'dir'
|
dir_ = tmp / 'dir'
|
||||||
dir_.mkdir()
|
dir_.mkdir()
|
||||||
link = tmp / 'link'
|
link = tmp / 'link'
|
||||||
link.symlink_to(dir_)
|
link.symlink_to(dir_)
|
||||||
self.assertRaises(OSError, link.rmtree)
|
link.delete()
|
||||||
self.assertTrue(dir_.exists())
|
self.assertTrue(dir_.exists())
|
||||||
self.assertTrue(link.exists(follow_symlinks=False))
|
self.assertFalse(link.exists(follow_symlinks=False))
|
||||||
errors = []
|
|
||||||
|
|
||||||
def on_error(error):
|
|
||||||
errors.append(error)
|
|
||||||
|
|
||||||
link.rmtree(on_error=on_error)
|
|
||||||
self.assertEqual(len(errors), 1)
|
|
||||||
self.assertIsInstance(errors[0], OSError)
|
|
||||||
self.assertEqual(errors[0].filename, str(link))
|
|
||||||
|
|
||||||
@needs_symlinks
|
@needs_symlinks
|
||||||
def test_rmtree_inner_symlink(self):
|
def test_delete_inner_symlink(self):
|
||||||
tmp = self.cls(self.base, 'rmtree')
|
tmp = self.cls(self.base, 'delete')
|
||||||
tmp.mkdir()
|
tmp.mkdir()
|
||||||
dir1 = tmp / 'dir1'
|
dir1 = tmp / 'dir1'
|
||||||
dir2 = dir1 / 'dir2'
|
dir2 = dir1 / 'dir2'
|
||||||
|
@ -2735,11 +2693,26 @@ class DummyPathTest(DummyPurePathTest):
|
||||||
link3 = dir1 / 'link3'
|
link3 = dir1 / 'link3'
|
||||||
link3.symlink_to(file1)
|
link3.symlink_to(file1)
|
||||||
# make sure symlinks are removed but not followed
|
# make sure symlinks are removed but not followed
|
||||||
dir1.rmtree()
|
dir1.delete()
|
||||||
self.assertFalse(dir1.exists())
|
self.assertFalse(dir1.exists())
|
||||||
self.assertTrue(dir3.exists())
|
self.assertTrue(dir3.exists())
|
||||||
self.assertTrue(file1.exists())
|
self.assertTrue(file1.exists())
|
||||||
|
|
||||||
|
def test_delete_missing(self):
|
||||||
|
tmp = self.cls(self.base, 'delete')
|
||||||
|
tmp.mkdir()
|
||||||
|
# filename is guaranteed not to exist
|
||||||
|
filename = tmp / 'foo'
|
||||||
|
self.assertRaises(FileNotFoundError, filename.delete)
|
||||||
|
# test that ignore_errors option is honored
|
||||||
|
filename.delete(ignore_errors=True)
|
||||||
|
# test on_error
|
||||||
|
errors = []
|
||||||
|
filename.delete(on_error=errors.append)
|
||||||
|
self.assertEqual(len(errors), 1)
|
||||||
|
self.assertIsInstance(errors[0], FileNotFoundError)
|
||||||
|
self.assertEqual(errors[0].filename, str(filename))
|
||||||
|
|
||||||
def setUpWalk(self):
|
def setUpWalk(self):
|
||||||
# Build:
|
# Build:
|
||||||
# TESTFN/
|
# TESTFN/
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Add :meth:`pathlib.Path.rmtree`, which recursively removes a directory.
|
Add :meth:`pathlib.Path.delete`, which recursively removes a file or directory.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue