Skip to content

Commit d51a6dc

Browse files
authored
gh-102828: add onexc arg to shutil.rmtree. Deprecate onerror. (#102829)
1 parent 4d1f033 commit d51a6dc

File tree

5 files changed

+256
-56
lines changed

5 files changed

+256
-56
lines changed

Doc/library/shutil.rst

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -292,15 +292,15 @@ Directory and files operations
292292
.. versionadded:: 3.8
293293
The *dirs_exist_ok* parameter.
294294

295-
.. function:: rmtree(path, ignore_errors=False, onerror=None, *, dir_fd=None)
295+
.. function:: rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None)
296296

297297
.. index:: single: directory; deleting
298298

299299
Delete an entire directory tree; *path* must point to a directory (but not a
300300
symbolic link to a directory). If *ignore_errors* is true, errors resulting
301301
from failed removals will be ignored; if false or omitted, such errors are
302-
handled by calling a handler specified by *onerror* or, if that is omitted,
303-
they raise an exception.
302+
handled by calling a handler specified by *onexc* or *onerror* or, if both
303+
are omitted, exceptions are propagated to the caller.
304304

305305
This function can support :ref:`paths relative to directory descriptors
306306
<dir_fd>`.
@@ -315,14 +315,17 @@ Directory and files operations
315315
otherwise. Applications can use the :data:`rmtree.avoids_symlink_attacks`
316316
function attribute to determine which case applies.
317317

318-
If *onerror* is provided, it must be a callable that accepts three
319-
parameters: *function*, *path*, and *excinfo*.
318+
If *onexc* is provided, it must be a callable that accepts three parameters:
319+
*function*, *path*, and *excinfo*.
320320

321321
The first parameter, *function*, is the function which raised the exception;
322322
it depends on the platform and implementation. The second parameter,
323323
*path*, will be the path name passed to *function*. The third parameter,
324-
*excinfo*, will be the exception information returned by
325-
:func:`sys.exc_info`. Exceptions raised by *onerror* will not be caught.
324+
*excinfo*, is the exception that was raised. Exceptions raised by *onexc*
325+
will not be caught.
326+
327+
The deprecated *onerror* is similar to *onexc*, except that the third
328+
parameter it receives is the tuple returned from :func:`sys.exc_info`.
326329

327330
.. audit-event:: shutil.rmtree path,dir_fd shutil.rmtree
328331

@@ -337,6 +340,9 @@ Directory and files operations
337340
.. versionchanged:: 3.11
338341
The *dir_fd* parameter.
339342

343+
.. versionchanged:: 3.12
344+
Added the *onexc* parameter, deprecated *onerror*.
345+
340346
.. attribute:: rmtree.avoids_symlink_attacks
341347

342348
Indicates whether the current platform and implementation provides a
@@ -509,7 +515,7 @@ rmtree example
509515
~~~~~~~~~~~~~~
510516

511517
This example shows how to remove a directory tree on Windows where some
512-
of the files have their read-only bit set. It uses the onerror callback
518+
of the files have their read-only bit set. It uses the onexc callback
513519
to clear the readonly bit and reattempt the remove. Any subsequent failure
514520
will propagate. ::
515521

@@ -521,7 +527,7 @@ will propagate. ::
521527
os.chmod(path, stat.S_IWRITE)
522528
func(path)
523529

524-
shutil.rmtree(directory, onerror=remove_readonly)
530+
shutil.rmtree(directory, onexc=remove_readonly)
525531

526532
.. _archiving-operations:
527533

Doc/whatsnew/3.12.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,11 @@ shutil
337337
of the process to *root_dir* to perform archiving.
338338
(Contributed by Serhiy Storchaka in :gh:`74696`.)
339339

340+
* :func:`shutil.rmtree` now accepts a new argument *onexc* which is an
341+
error handler like *onerror* but which expects an exception instance
342+
rather than a *(typ, val, tb)* triplet. *onerror* is deprecated.
343+
(Contributed by Irit Katriel in :gh:`102828`.)
344+
340345

341346
sqlite3
342347
-------
@@ -498,6 +503,10 @@ Deprecated
498503
fields are deprecated. Use :data:`sys.last_exc` instead.
499504
(Contributed by Irit Katriel in :gh:`102778`.)
500505

506+
* The *onerror* argument of :func:`shutil.rmtree` is deprecated. Use *onexc*
507+
instead. (Contributed by Irit Katriel in :gh:`102828`.)
508+
509+
501510
Pending Removal in Python 3.13
502511
------------------------------
503512

Lib/shutil.py

Lines changed: 62 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -575,12 +575,12 @@ def _rmtree_islink(path):
575575
return os.path.islink(path)
576576

577577
# version vulnerable to race conditions
578-
def _rmtree_unsafe(path, onerror):
578+
def _rmtree_unsafe(path, onexc):
579579
try:
580580
with os.scandir(path) as scandir_it:
581581
entries = list(scandir_it)
582-
except OSError:
583-
onerror(os.scandir, path, sys.exc_info())
582+
except OSError as err:
583+
onexc(os.scandir, path, err)
584584
entries = []
585585
for entry in entries:
586586
fullname = entry.path
@@ -596,28 +596,28 @@ def _rmtree_unsafe(path, onerror):
596596
# a directory with a symlink after the call to
597597
# os.scandir or entry.is_dir above.
598598
raise OSError("Cannot call rmtree on a symbolic link")
599-
except OSError:
600-
onerror(os.path.islink, fullname, sys.exc_info())
599+
except OSError as err:
600+
onexc(os.path.islink, fullname, err)
601601
continue
602-
_rmtree_unsafe(fullname, onerror)
602+
_rmtree_unsafe(fullname, onexc)
603603
else:
604604
try:
605605
os.unlink(fullname)
606-
except OSError:
607-
onerror(os.unlink, fullname, sys.exc_info())
606+
except OSError as err:
607+
onexc(os.unlink, fullname, err)
608608
try:
609609
os.rmdir(path)
610-
except OSError:
611-
onerror(os.rmdir, path, sys.exc_info())
610+
except OSError as err:
611+
onexc(os.rmdir, path, err)
612612

613613
# Version using fd-based APIs to protect against races
614-
def _rmtree_safe_fd(topfd, path, onerror):
614+
def _rmtree_safe_fd(topfd, path, onexc):
615615
try:
616616
with os.scandir(topfd) as scandir_it:
617617
entries = list(scandir_it)
618618
except OSError as err:
619619
err.filename = path
620-
onerror(os.scandir, path, sys.exc_info())
620+
onexc(os.scandir, path, err)
621621
return
622622
for entry in entries:
623623
fullname = os.path.join(path, entry.name)
@@ -630,71 +630,89 @@ def _rmtree_safe_fd(topfd, path, onerror):
630630
try:
631631
orig_st = entry.stat(follow_symlinks=False)
632632
is_dir = stat.S_ISDIR(orig_st.st_mode)
633-
except OSError:
634-
onerror(os.lstat, fullname, sys.exc_info())
633+
except OSError as err:
634+
onexc(os.lstat, fullname, err)
635635
continue
636636
if is_dir:
637637
try:
638638
dirfd = os.open(entry.name, os.O_RDONLY, dir_fd=topfd)
639639
dirfd_closed = False
640-
except OSError:
641-
onerror(os.open, fullname, sys.exc_info())
640+
except OSError as err:
641+
onexc(os.open, fullname, err)
642642
else:
643643
try:
644644
if os.path.samestat(orig_st, os.fstat(dirfd)):
645-
_rmtree_safe_fd(dirfd, fullname, onerror)
645+
_rmtree_safe_fd(dirfd, fullname, onexc)
646646
try:
647647
os.close(dirfd)
648648
dirfd_closed = True
649649
os.rmdir(entry.name, dir_fd=topfd)
650-
except OSError:
651-
onerror(os.rmdir, fullname, sys.exc_info())
650+
except OSError as err:
651+
onexc(os.rmdir, fullname, err)
652652
else:
653653
try:
654654
# This can only happen if someone replaces
655655
# a directory with a symlink after the call to
656656
# os.scandir or stat.S_ISDIR above.
657657
raise OSError("Cannot call rmtree on a symbolic "
658658
"link")
659-
except OSError:
660-
onerror(os.path.islink, fullname, sys.exc_info())
659+
except OSError as err:
660+
onexc(os.path.islink, fullname, err)
661661
finally:
662662
if not dirfd_closed:
663663
os.close(dirfd)
664664
else:
665665
try:
666666
os.unlink(entry.name, dir_fd=topfd)
667-
except OSError:
668-
onerror(os.unlink, fullname, sys.exc_info())
667+
except OSError as err:
668+
onexc(os.unlink, fullname, err)
669669

670670
_use_fd_functions = ({os.open, os.stat, os.unlink, os.rmdir} <=
671671
os.supports_dir_fd and
672672
os.scandir in os.supports_fd and
673673
os.stat in os.supports_follow_symlinks)
674674

675-
def rmtree(path, ignore_errors=False, onerror=None, *, dir_fd=None):
675+
def rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None):
676676
"""Recursively delete a directory tree.
677677
678678
If dir_fd is not None, it should be a file descriptor open to a directory;
679679
path will then be relative to that directory.
680680
dir_fd may not be implemented on your platform.
681681
If it is unavailable, using it will raise a NotImplementedError.
682682
683-
If ignore_errors is set, errors are ignored; otherwise, if onerror
684-
is set, it is called to handle the error with arguments (func,
683+
If ignore_errors is set, errors are ignored; otherwise, if onexc or
684+
onerror is set, it is called to handle the error with arguments (func,
685685
path, exc_info) where func is platform and implementation dependent;
686686
path is the argument to that function that caused it to fail; and
687-
exc_info is a tuple returned by sys.exc_info(). If ignore_errors
688-
is false and onerror is None, an exception is raised.
687+
the value of exc_info describes the exception. For onexc it is the
688+
exception instance, and for onerror it is a tuple as returned by
689+
sys.exc_info(). If ignore_errors is false and both onexc and
690+
onerror are None, the exception is reraised.
689691
692+
onerror is deprecated and only remains for backwards compatibility.
693+
If both onerror and onexc are set, onerror is ignored and onexc is used.
690694
"""
691695
sys.audit("shutil.rmtree", path, dir_fd)
692696
if ignore_errors:
693-
def onerror(*args):
697+
def onexc(*args):
694698
pass
695-
elif onerror is None:
696-
def onerror(*args):
699+
elif onerror is None and onexc is None:
700+
def onexc(*args):
697701
raise
702+
elif onexc is None:
703+
if onerror is None:
704+
def onexc(*args):
705+
raise
706+
else:
707+
# delegate to onerror
708+
def onexc(*args):
709+
func, path, exc = args
710+
if exc is None:
711+
exc_info = None, None, None
712+
else:
713+
exc_info = type(exc), exc, exc.__traceback__
714+
return onerror(func, path, exc_info)
715+
698716
if _use_fd_functions:
699717
# While the unsafe rmtree works fine on bytes, the fd based does not.
700718
if isinstance(path, bytes):
@@ -703,30 +721,30 @@ def onerror(*args):
703721
# lstat()/open()/fstat() trick.
704722
try:
705723
orig_st = os.lstat(path, dir_fd=dir_fd)
706-
except Exception:
707-
onerror(os.lstat, path, sys.exc_info())
724+
except Exception as err:
725+
onexc(os.lstat, path, err)
708726
return
709727
try:
710728
fd = os.open(path, os.O_RDONLY, dir_fd=dir_fd)
711729
fd_closed = False
712-
except Exception:
713-
onerror(os.open, path, sys.exc_info())
730+
except Exception as err:
731+
onexc(os.open, path, err)
714732
return
715733
try:
716734
if os.path.samestat(orig_st, os.fstat(fd)):
717-
_rmtree_safe_fd(fd, path, onerror)
735+
_rmtree_safe_fd(fd, path, onexc)
718736
try:
719737
os.close(fd)
720738
fd_closed = True
721739
os.rmdir(path, dir_fd=dir_fd)
722-
except OSError:
723-
onerror(os.rmdir, path, sys.exc_info())
740+
except OSError as err:
741+
onexc(os.rmdir, path, err)
724742
else:
725743
try:
726744
# symlinks to directories are forbidden, see bug #1669
727745
raise OSError("Cannot call rmtree on a symbolic link")
728-
except OSError:
729-
onerror(os.path.islink, path, sys.exc_info())
746+
except OSError as err:
747+
onexc(os.path.islink, path, err)
730748
finally:
731749
if not fd_closed:
732750
os.close(fd)
@@ -737,11 +755,11 @@ def onerror(*args):
737755
if _rmtree_islink(path):
738756
# symlinks to directories are forbidden, see bug #1669
739757
raise OSError("Cannot call rmtree on a symbolic link")
740-
except OSError:
741-
onerror(os.path.islink, path, sys.exc_info())
742-
# can't continue even if onerror hook returns
758+
except OSError as err:
759+
onexc(os.path.islink, path, err)
760+
# can't continue even if onexc hook returns
743761
return
744-
return _rmtree_unsafe(path, onerror)
762+
return _rmtree_unsafe(path, onexc)
745763

746764
# Allow introspection of whether or not the hardening against symlink
747765
# attacks is supported on the current platform

0 commit comments

Comments
 (0)