diff --git a/changelog/10875.improvement.rst b/changelog/10875.improvement.rst new file mode 100644 index 00000000000..eeaf046350b --- /dev/null +++ b/changelog/10875.improvement.rst @@ -0,0 +1 @@ +Python 3.12 support: fixed ``RuntimeError: TestResult has no addDuration method`` when running ``unittest`` tests. diff --git a/changelog/10890.improvement.rst b/changelog/10890.improvement.rst new file mode 100644 index 00000000000..9c367da31ef --- /dev/null +++ b/changelog/10890.improvement.rst @@ -0,0 +1 @@ +Python 3.12 support: fixed ``shutil.rmtree(onerror=...)`` deprecation warning when using :fixture:`tmp_path`. diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 9f9463d8862..30ed76cf89e 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -6,6 +6,7 @@ import os import shutil import sys +import types import uuid import warnings from enum import Enum @@ -28,6 +29,8 @@ from typing import Iterator from typing import Optional from typing import Set +from typing import Tuple +from typing import Type from typing import TypeVar from typing import Union @@ -63,21 +66,33 @@ def get_lock_path(path: _AnyPurePath) -> _AnyPurePath: 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. 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). # More context: https://github.com/pytest-dev/pytest/issues/5974#issuecomment-543799018 - if isinstance(excvalue, FileNotFoundError): + if isinstance(exc, FileNotFoundError): return False - if not isinstance(excvalue, PermissionError): + if not isinstance(exc, PermissionError): 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 @@ -86,7 +101,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool: warnings.warn( PytestWarning( "(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.""" path = ensure_extended_length_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]: diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index c2df986530c..c660aa75db2 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -298,6 +298,9 @@ def addSuccess(self, testcase: "unittest.TestCase") -> None: def stopTest(self, testcase: "unittest.TestCase") -> None: pass + def addDuration(self, testcase: "unittest.TestCase", elapsed: float) -> None: + pass + def runtest(self) -> None: from _pytest.debugging import maybe_wrap_pytest_function_for_tracing diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index fcb0775dd5f..110a68b278d 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -512,20 +512,20 @@ def test_on_rm_rf_error(self, tmp_path: Path) -> None: # unknown exception 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) assert fn.is_file() # 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) # unknown function with pytest.warns( 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: ", ): - exc_info3 = (None, PermissionError(), None) + exc_info3 = (PermissionError, PermissionError(), None) on_rm_rf_error(None, str(fn), exc_info3, start_path=tmp_path) assert fn.is_file() @@ -533,12 +533,12 @@ def test_on_rm_rf_error(self, tmp_path: Path) -> None: with warnings.catch_warnings(): warnings.simplefilter("ignore") 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) assert fn.is_file() 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) assert not fn.is_file()