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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy