Skip to content

Commit

Permalink
refactor interesting_origin
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Nov 27, 2023
1 parent 3962442 commit 8a67dc7
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 22 deletions.
4 changes: 2 additions & 2 deletions hypothesis-python/src/hypothesis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@
from hypothesis.internal.conjecture.shrinker import sort_key
from hypothesis.internal.entropy import deterministic_PRNG
from hypothesis.internal.escalation import (
InterestingOrigin,
current_pytest_item,
escalate_hypothesis_internal_error,
format_exception,
get_interesting_origin,
get_trimmed_traceback,
)
from hypothesis.internal.healthcheck import fail_health_check
Expand Down Expand Up @@ -970,7 +970,7 @@ def _execute_once_for_engine(self, data):

self.failed_normally = True

interesting_origin = get_interesting_origin(e)
interesting_origin = InterestingOrigin.from_exception(e)
if trace: # pragma: no cover
# Trace collection is explicitly disabled under coverage.
self.explain_traces[interesting_origin].add(trace)
Expand Down
53 changes: 34 additions & 19 deletions hypothesis-python/src/hypothesis/internal/escalation.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
import contextlib
import os
import sys
import textwrap
import traceback
from inspect import getframeinfo
from pathlib import Path
from typing import Dict
from typing import Dict, NamedTuple, Type

import hypothesis
from hypothesis.errors import (
Expand Down Expand Up @@ -105,32 +106,46 @@ def get_trimmed_traceback(exception=None):
return tb


def get_interesting_origin(exception):
class InterestingOrigin(NamedTuple):
# The `interesting_origin` is how Hypothesis distinguishes between multiple
# failures, for reporting and also to replay from the example database (even
# if report_multiple_bugs=False). We traditionally use the exception type and
# location, but have extracted this logic in order to see through `except ...:`
# blocks and understand the __cause__ (`raise x from y`) or __context__ that
# first raised an exception as well as PEP-654 exception groups.
tb = get_trimmed_traceback(exception)
if tb is None:
exc_type: Type[BaseException]
filename: str
lineno: int
context: "InterestingOrigin | tuple[()]"
group_elems: "tuple[InterestingOrigin, ...]"

def __str__(self) -> str:
ctx = ""
if self.context:
ctx = textwrap.indent(f"\ncontext: {self.context}", prefix=" ")
group = ""
if self.group_elems:
chunks = "\n ".join(str(x) for x in self.group_elems)
group = textwrap.indent(f"\nchild exceptions:\n {chunks}", prefix=" ")
return f"{self.exc_type.__name__} at {self.filename}:{self.lineno}{ctx}{group}"

@classmethod
def from_exception(cls, exception: BaseException, /) -> "InterestingOrigin":
filename, lineno = None, None
else:
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
return (
type(exception),
filename,
lineno,
# Note that if __cause__ is set it is always equal to __context__, explicitly
# to support introspection when debugging, so we can use that unconditionally.
get_interesting_origin(exception.__context__) if exception.__context__ else (),
# We distinguish exception groups by the inner exceptions, as for __context__
tuple(
map(get_interesting_origin, exception.exceptions)
if tb := get_trimmed_traceback(exception):
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
return cls(
type(exception),
filename,
lineno,
# Note that if __cause__ is set it is always equal to __context__, explicitly
# to support introspection when debugging, so we can use that unconditionally.
cls.from_exception(exception.__context__) if exception.__context__ else (),
# We distinguish exception groups by the inner exceptions, as for __context__
tuple(map(cls.from_exception, exception.exceptions))
if isinstance(exception, BaseExceptionGroup)
else []
),
)
else (),
)


current_pytest_item = DynamicVariable(None)
Expand Down
20 changes: 19 additions & 1 deletion hypothesis-python/tests/cover/test_escalation.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,22 @@ def test_errors_attribute_error():


def test_handles_null_traceback():
esc.get_interesting_origin(Exception())
esc.InterestingOrigin.from_exception(Exception())


def test_handles_context():
e = ValueError()
e.__context__ = KeyError()
origin = esc.InterestingOrigin.from_exception(e)
assert "ValueError at " in str(origin)
assert " context: " in str(origin)
assert "KeyError at " in str(origin)


def test_handles_groups():
origin = esc.InterestingOrigin.from_exception(
BaseExceptionGroup("message", [ValueError("msg2")])
)
assert "ExceptionGroup at " in str(origin)
assert "child exception" in str(origin)
assert "ValueError at " in str(origin)

0 comments on commit 8a67dc7

Please sign in to comment.