Skip to content

Commit

Permalink
include tracemalloc message in unraisablehook failures
Browse files Browse the repository at this point in the history
  • Loading branch information
graingert committed Nov 13, 2024
1 parent 194f9ec commit c78f84b
Showing 1 changed file with 56 additions and 17 deletions.
73 changes: 56 additions & 17 deletions src/_pytest/unraisableexception.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import traceback
from typing import Callable
from typing import Generator
from typing import NamedTuple
from typing import TYPE_CHECKING
import warnings

Expand All @@ -21,58 +22,87 @@
from exceptiongroup import ExceptionGroup


def _tracemalloc_msg(source: object) -> str:
if source is None:
return ""

Check warning on line 27 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L27

Added line #L27 was not covered by tests

try:
import tracemalloc
except ImportError:
return ""

Check warning on line 32 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L29-L32

Added lines #L29 - L32 were not covered by tests

tb = tracemalloc.get_object_traceback(source)

Check warning on line 34 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L34

Added line #L34 was not covered by tests
if tb is not None:
formatted_tb = "\n".join(tb.format())

Check warning on line 36 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L36

Added line #L36 was not covered by tests
# Use a leading new line to better separate the (large) output
# from the traceback to the previous warning text.
return f"\nObject allocated at:\n{formatted_tb}"

Check warning on line 39 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L39

Added line #L39 was not covered by tests
# No need for a leading new line.
url = "https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings"
return (

Check warning on line 42 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L41-L42

Added lines #L41 - L42 were not covered by tests
"Enable tracemalloc to get traceback where the object was allocated.\n"
f"See {url} for more info."
)


class UnraisableMeta(NamedTuple):
object_repr: str
tracemalloc_tb: str
unraisable: sys.UnraisableHookArgs


def unraisable_exception_runtest_hook() -> Generator[None]:
try:
yield
finally:
collect_unraisable()


_unraisable_exceptions: collections.deque[tuple[str, sys.UnraisableHookArgs]] = (
collections.deque()
)
_unraisable_exceptions: collections.deque[UnraisableMeta] = collections.deque()


def collect_unraisable() -> None:
errors = []
unraisable = None
meta = None
try:
while True:
try:
object_repr, unraisable = _unraisable_exceptions.pop()
meta = _unraisable_exceptions.pop()
except IndexError:
break

if unraisable.err_msg is not None:
err_msg = unraisable.err_msg
if meta.unraisable.err_msg is not None:
err_msg = meta.unraisable.err_msg

Check warning on line 75 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L75

Added line #L75 was not covered by tests
else:
err_msg = "Exception ignored in"
msg = f"{err_msg}: {object_repr}\n\n"
traceback_message = msg + "".join(
msg = f"{err_msg}: {meta.object_repr}"
traceback_message = "\n\n" + "".join(

Check warning on line 79 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L77-L79

Added lines #L77 - L79 were not covered by tests
traceback.format_exception(
unraisable.exc_type,
unraisable.exc_value,
unraisable.exc_traceback,
meta.unraisable.exc_type,
meta.unraisable.exc_value,
meta.unraisable.exc_traceback,
)
)
try:
warnings.warn(

Check warning on line 87 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L86-L87

Added lines #L86 - L87 were not covered by tests
pytest.PytestUnraisableExceptionWarning(traceback_message)
pytest.PytestUnraisableExceptionWarning(
msg + traceback_message + meta.tracemalloc_tb
)
)
except pytest.PytestUnraisableExceptionWarning as e:

Check warning on line 92 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L92

Added line #L92 was not covered by tests
# exceptions have a better way to show the traceback, but
# warnings do not, so hide the traceback from the msg and
# set the cause so the traceback shows up in the right place
e.args = (msg,)
e.__cause__ = unraisable.exc_value
e.args = (msg + meta.tracemalloc_tb,)
e.__cause__ = meta.unraisable.exc_value
errors.append(e)

Check warning on line 98 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L96-L98

Added lines #L96 - L98 were not covered by tests

if len(errors) == 1:
raise errors[0]

Check warning on line 101 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L101

Added line #L101 was not covered by tests
if errors:
raise ExceptionGroup("multiple unraisable exception warnings", errors)

Check warning on line 103 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L103

Added line #L103 was not covered by tests
finally:
del errors, unraisable
del errors, meta


def _cleanup(prev_hook: Callable[[sys.UnraisableHookArgs], object]) -> None:
Expand All @@ -85,7 +115,16 @@ def _cleanup(prev_hook: Callable[[sys.UnraisableHookArgs], object]) -> None:


def unraisable_hook(unraisable: sys.UnraisableHookArgs) -> None:
_unraisable_exceptions.append((repr(unraisable.object), unraisable))
_unraisable_exceptions.append(

Check warning on line 118 in src/_pytest/unraisableexception.py

View check run for this annotation

Codecov / codecov/patch

src/_pytest/unraisableexception.py#L118

Added line #L118 was not covered by tests
UnraisableMeta(
# we need to compute these strings here as they might change after
# the unraisablehook finishes and before the unraisable object is
# collected by a hook
object_repr=repr(unraisable.object),
tracemalloc_tb=_tracemalloc_msg(unraisable.object),
unraisable=unraisable,
)
)


def pytest_configure(config: Config) -> None:
Expand Down

0 comments on commit c78f84b

Please sign in to comment.