unraisableexception.py 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import sys
  2. import traceback
  3. import warnings
  4. from types import TracebackType
  5. from typing import Any
  6. from typing import Callable
  7. from typing import Generator
  8. from typing import Optional
  9. from typing import Type
  10. import pytest
  11. # Copied from cpython/Lib/test/support/__init__.py, with modifications.
  12. class catch_unraisable_exception:
  13. """Context manager catching unraisable exception using sys.unraisablehook.
  14. Storing the exception value (cm.unraisable.exc_value) creates a reference
  15. cycle. The reference cycle is broken explicitly when the context manager
  16. exits.
  17. Storing the object (cm.unraisable.object) can resurrect it if it is set to
  18. an object which is being finalized. Exiting the context manager clears the
  19. stored object.
  20. Usage:
  21. with catch_unraisable_exception() as cm:
  22. # code creating an "unraisable exception"
  23. ...
  24. # check the unraisable exception: use cm.unraisable
  25. ...
  26. # cm.unraisable attribute no longer exists at this point
  27. # (to break a reference cycle)
  28. """
  29. def __init__(self) -> None:
  30. self.unraisable: Optional["sys.UnraisableHookArgs"] = None
  31. self._old_hook: Optional[Callable[["sys.UnraisableHookArgs"], Any]] = None
  32. def _hook(self, unraisable: "sys.UnraisableHookArgs") -> None:
  33. # Storing unraisable.object can resurrect an object which is being
  34. # finalized. Storing unraisable.exc_value creates a reference cycle.
  35. self.unraisable = unraisable
  36. def __enter__(self) -> "catch_unraisable_exception":
  37. self._old_hook = sys.unraisablehook
  38. sys.unraisablehook = self._hook
  39. return self
  40. def __exit__(
  41. self,
  42. exc_type: Optional[Type[BaseException]],
  43. exc_val: Optional[BaseException],
  44. exc_tb: Optional[TracebackType],
  45. ) -> None:
  46. assert self._old_hook is not None
  47. sys.unraisablehook = self._old_hook
  48. self._old_hook = None
  49. del self.unraisable
  50. def unraisable_exception_runtest_hook() -> Generator[None, None, None]:
  51. with catch_unraisable_exception() as cm:
  52. yield
  53. if cm.unraisable:
  54. if cm.unraisable.err_msg is not None:
  55. err_msg = cm.unraisable.err_msg
  56. else:
  57. err_msg = "Exception ignored in"
  58. msg = f"{err_msg}: {cm.unraisable.object!r}\n\n"
  59. msg += "".join(
  60. traceback.format_exception(
  61. cm.unraisable.exc_type,
  62. cm.unraisable.exc_value,
  63. cm.unraisable.exc_traceback,
  64. )
  65. )
  66. warnings.warn(pytest.PytestUnraisableExceptionWarning(msg))
  67. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
  68. def pytest_runtest_setup() -> Generator[None, None, None]:
  69. yield from unraisable_exception_runtest_hook()
  70. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
  71. def pytest_runtest_call() -> Generator[None, None, None]:
  72. yield from unraisable_exception_runtest_hook()
  73. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
  74. def pytest_runtest_teardown() -> Generator[None, None, None]:
  75. yield from unraisable_exception_runtest_hook()