pathlib: fix Python 3.12 rmtree(onerror=...) deprecation

Fixes #10890
Ref: https://docs.python.org/3.12/library/shutil.html#shutil.rmtree
This commit is contained in:
Ran Benita 2023-04-11 12:48:51 +03:00
parent 22524046cf
commit 1b196fbeaf
3 changed files with 32 additions and 13 deletions

View File

@ -0,0 +1 @@
Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`.

View File

@ -6,6 +6,7 @@ import itertools
import os import os
import shutil import shutil
import sys import sys
import types
import uuid import uuid
import warnings import warnings
from enum import Enum from enum import Enum
@ -28,6 +29,8 @@ from typing import Iterable
from typing import Iterator from typing import Iterator
from typing import Optional from typing import Optional
from typing import Set from typing import Set
from typing import Tuple
from typing import Type
from typing import TypeVar from typing import TypeVar
from typing import Union from typing import Union
@ -63,21 +66,33 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath:
return path.joinpath(".lock") return path.joinpath(".lock")
def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool: def on_rm_rf_error(
func,
path: str,
excinfo: Union[
BaseException,
Tuple[Type[BaseException], BaseException, Optional[types.TracebackType]],
],
*,
start_path: Path,
) -> bool:
"""Handle known read-only errors during rmtree. """Handle known read-only errors during rmtree.
The returned value is used only by our own tests. The returned value is used only by our own tests.
""" """
exctype, excvalue = exc[:2] if isinstance(excinfo, BaseException):
exc = excinfo
else:
exc = excinfo[1]
# Another process removed the file in the middle of the "rm_rf" (xdist for example). # Another process removed the file in the middle of the "rm_rf" (xdist for example).
# More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018 # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018
if isinstance(excvalue, FileNotFoundError): if isinstance(exc, FileNotFoundError):
return False return False
if not isinstance(excvalue, PermissionError): if not isinstance(exc, PermissionError):
warnings.warn( warnings.warn(
PytestWarning(f"(rm_rf) error removing {path}\n{exctype}: {excvalue}") PytestWarning(f"(rm_rf) error removing {path}\n{type(exc)}: {exc}")
) )
return False return False
@ -86,7 +101,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool:
warnings.warn( warnings.warn(
PytestWarning( PytestWarning(
"(rm_rf) unknown function {} when removing {}:\n{}: {}".format( "(rm_rf) unknown function {} when removing {}:\n{}: {}".format(
func, path, exctype, excvalue func, path, type(exc), exc
) )
) )
) )
@ -149,7 +164,10 @@ def rm_rf(path: Path) -> None:
are read-only.""" are read-only."""
path = ensure_extended_length_path(path) path = ensure_extended_length_path(path)
onerror = partial(on_rm_rf_error, start_path=path) onerror = partial(on_rm_rf_error, start_path=path)
shutil.rmtree(str(path), onerror=onerror) if sys.version_info >= (3, 12):
shutil.rmtree(str(path), onexc=onerror)
else:
shutil.rmtree(str(path), onerror=onerror)
def find_prefixed(root: Path, prefix: str) -> Iterator[Path]: def find_prefixed(root: Path, prefix: str) -> Iterator[Path]:

View File

@ -512,20 +512,20 @@ class TestRmRf:
# unknown exception # unknown exception
with pytest.warns(pytest.PytestWarning): with pytest.warns(pytest.PytestWarning):
exc_info1 = (None, RuntimeError(), None) exc_info1 = (RuntimeError, RuntimeError(), None)
on_rm_rf_error(os.unlink, str(fn), exc_info1, start_path=tmp_path) on_rm_rf_error(os.unlink, str(fn), exc_info1, start_path=tmp_path)
assert fn.is_file() assert fn.is_file()
# we ignore FileNotFoundError # we ignore FileNotFoundError
exc_info2 = (None, FileNotFoundError(), None) exc_info2 = (FileNotFoundError, FileNotFoundError(), None)
assert not on_rm_rf_error(None, str(fn), exc_info2, start_path=tmp_path) assert not on_rm_rf_error(None, str(fn), exc_info2, start_path=tmp_path)
# unknown function # unknown function
with pytest.warns( with pytest.warns(
pytest.PytestWarning, pytest.PytestWarning,
match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\nNone: ", match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\n<class 'PermissionError'>: ",
): ):
exc_info3 = (None, PermissionError(), None) exc_info3 = (PermissionError, PermissionError(), None)
on_rm_rf_error(None, str(fn), exc_info3, start_path=tmp_path) on_rm_rf_error(None, str(fn), exc_info3, start_path=tmp_path)
assert fn.is_file() assert fn.is_file()
@ -533,12 +533,12 @@ class TestRmRf:
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.simplefilter("ignore") warnings.simplefilter("ignore")
with pytest.warns(None) as warninfo: # type: ignore[call-overload] with pytest.warns(None) as warninfo: # type: ignore[call-overload]
exc_info4 = (None, PermissionError(), None) exc_info4 = PermissionError()
on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path) on_rm_rf_error(os.open, str(fn), exc_info4, start_path=tmp_path)
assert fn.is_file() assert fn.is_file()
assert not [x.message for x in warninfo] assert not [x.message for x in warninfo]
exc_info5 = (None, PermissionError(), None) exc_info5 = PermissionError()
on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path) on_rm_rf_error(os.unlink, str(fn), exc_info5, start_path=tmp_path)
assert not fn.is_file() assert not fn.is_file()