Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/threadexception.py: 88%

42 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1import threading 

2import traceback 

3import warnings 

4from types import TracebackType 

5from typing import Any 

6from typing import Callable 

7from typing import Generator 

8from typing import Optional 

9from typing import Type 

10 

11import pytest 

12 

13 

14# Copied from cpython/Lib/test/support/threading_helper.py, with modifications. 

15class catch_threading_exception: 

16 """Context manager catching threading.Thread exception using 

17 threading.excepthook. 

18 

19 Storing exc_value using a custom hook can create a reference cycle. The 

20 reference cycle is broken explicitly when the context manager exits. 

21 

22 Storing thread using a custom hook can resurrect it if it is set to an 

23 object which is being finalized. Exiting the context manager clears the 

24 stored object. 

25 

26 Usage: 

27 with threading_helper.catch_threading_exception() as cm: 

28 # code spawning a thread which raises an exception 

29 ... 

30 # check the thread exception: use cm.args 

31 ... 

32 # cm.args attribute no longer exists at this point 

33 # (to break a reference cycle) 

34 """ 

35 

36 def __init__(self) -> None: 

37 self.args: Optional["threading.ExceptHookArgs"] = None 

38 self._old_hook: Optional[Callable[["threading.ExceptHookArgs"], Any]] = None 

39 

40 def _hook(self, args: "threading.ExceptHookArgs") -> None: 

41 self.args = args 

42 

43 def __enter__(self) -> "catch_threading_exception": 

44 self._old_hook = threading.excepthook 

45 threading.excepthook = self._hook 

46 return self 

47 

48 def __exit__( 

49 self, 

50 exc_type: Optional[Type[BaseException]], 

51 exc_val: Optional[BaseException], 

52 exc_tb: Optional[TracebackType], 

53 ) -> None: 

54 assert self._old_hook is not None 

55 threading.excepthook = self._old_hook 

56 self._old_hook = None 

57 del self.args 

58 

59 

60def thread_exception_runtest_hook() -> Generator[None, None, None]: 

61 with catch_threading_exception() as cm: 

62 yield 

63 if cm.args: 

64 thread_name = "<unknown>" if cm.args.thread is None else cm.args.thread.name 

65 msg = f"Exception in thread {thread_name}\n\n" 

66 msg += "".join( 

67 traceback.format_exception( 

68 cm.args.exc_type, 

69 cm.args.exc_value, 

70 cm.args.exc_traceback, 

71 ) 

72 ) 

73 warnings.warn(pytest.PytestUnhandledThreadExceptionWarning(msg)) 

74 

75 

76@pytest.hookimpl(hookwrapper=True, trylast=True) 

77def pytest_runtest_setup() -> Generator[None, None, None]: 

78 yield from thread_exception_runtest_hook() 

79 

80 

81@pytest.hookimpl(hookwrapper=True, tryfirst=True) 

82def pytest_runtest_call() -> Generator[None, None, None]: 

83 yield from thread_exception_runtest_hook() 

84 

85 

86@pytest.hookimpl(hookwrapper=True, tryfirst=True) 

87def pytest_runtest_teardown() -> Generator[None, None, None]: 

88 yield from thread_exception_runtest_hook()