1# This file is part of Hypothesis, which may be found at
2# https://github.com/HypothesisWorks/hypothesis/
3#
4# Copyright the Hypothesis Authors.
5# Individual contributors are listed in AUTHORS.rst and the git log.
6#
7# This Source Code Form is subject to the terms of the Mozilla Public License,
8# v. 2.0. If a copy of the MPL was not distributed with this file, You can
9# obtain one at https://mozilla.org/MPL/2.0/.
10
11import contextlib
12import os
13import sys
14import textwrap
15import traceback
16from functools import partial
17from inspect import getframeinfo
18from pathlib import Path
19from types import ModuleType, TracebackType
20from typing import Callable, NamedTuple, Optional
21
22import hypothesis
23from hypothesis.errors import _Trimmable
24from hypothesis.internal.compat import BaseExceptionGroup
25from hypothesis.utils.dynamicvariables import DynamicVariable
26
27FILE_CACHE: dict[ModuleType, dict[str, bool]] = {}
28
29
30def belongs_to(package: ModuleType) -> Callable[[str], bool]:
31 if getattr(package, "__file__", None) is None: # pragma: no cover
32 return lambda filepath: False
33
34 assert package.__file__ is not None
35 FILE_CACHE.setdefault(package, {})
36 cache = FILE_CACHE[package]
37 root = Path(package.__file__).resolve().parent
38
39 def accept(filepath: str) -> bool:
40 try:
41 return cache[filepath]
42 except KeyError:
43 pass
44 try:
45 Path(filepath).resolve().relative_to(root)
46 result = True
47 except Exception:
48 result = False
49 cache[filepath] = result
50 return result
51
52 accept.__name__ = f"is_{package.__name__}_file"
53 return accept
54
55
56is_hypothesis_file = belongs_to(hypothesis)
57
58
59def get_trimmed_traceback(
60 exception: Optional[BaseException] = None,
61) -> Optional[TracebackType]:
62 """Return the current traceback, minus any frames added by Hypothesis."""
63 if exception is None:
64 _, exception, tb = sys.exc_info()
65 else:
66 tb = exception.__traceback__
67 # Avoid trimming the traceback if we're in verbose mode, or the error
68 # was raised inside Hypothesis. Additionally, the environment variable
69 # HYPOTHESIS_NO_TRACEBACK_TRIM is respected if nonempty, because verbose
70 # mode is prohibitively slow when debugging strategy recursion errors.
71 assert hypothesis.settings.default is not None
72 if (
73 tb is None
74 or os.environ.get("HYPOTHESIS_NO_TRACEBACK_TRIM")
75 or hypothesis.settings.default.verbosity >= hypothesis.Verbosity.debug
76 or (
77 is_hypothesis_file(traceback.extract_tb(tb)[-1][0])
78 and not isinstance(exception, _Trimmable)
79 )
80 ):
81 return tb
82 while tb.tb_next is not None and (
83 # If the frame is from one of our files, it's been added by Hypothesis.
84 is_hypothesis_file(getframeinfo(tb.tb_frame).filename)
85 # But our `@proxies` decorator overrides the source location,
86 # so we check for an attribute it injects into the frame too.
87 or tb.tb_frame.f_globals.get("__hypothesistracebackhide__") is True
88 ):
89 tb = tb.tb_next
90 return tb
91
92
93class InterestingOrigin(NamedTuple):
94 # The `interesting_origin` is how Hypothesis distinguishes between multiple
95 # failures, for reporting and also to replay from the example database (even
96 # if report_multiple_bugs=False). We traditionally use the exception type and
97 # location, but have extracted this logic in order to see through `except ...:`
98 # blocks and understand the __cause__ (`raise x from y`) or __context__ that
99 # first raised an exception as well as PEP-654 exception groups.
100 exc_type: type[BaseException]
101 filename: Optional[str]
102 lineno: Optional[int]
103 context: "InterestingOrigin | tuple[()]"
104 group_elems: "tuple[InterestingOrigin, ...]"
105
106 def __str__(self) -> str:
107 ctx = ""
108 if self.context:
109 ctx = textwrap.indent(f"\ncontext: {self.context}", prefix=" ")
110 group = ""
111 if self.group_elems:
112 chunks = "\n ".join(str(x) for x in self.group_elems)
113 group = textwrap.indent(f"\nchild exceptions:\n {chunks}", prefix=" ")
114 return f"{self.exc_type.__name__} at {self.filename}:{self.lineno}{ctx}{group}"
115
116 @classmethod
117 def from_exception(
118 cls, exception: BaseException, /, seen: tuple[BaseException, ...] = ()
119 ) -> "InterestingOrigin":
120 filename, lineno = None, None
121 if tb := get_trimmed_traceback(exception):
122 filename, lineno, *_ = traceback.extract_tb(tb)[-1]
123 seen = (*seen, exception)
124 make = partial(cls.from_exception, seen=seen)
125 context: InterestingOrigin | tuple[()] = ()
126 if exception.__context__ is not None and exception.__context__ not in seen:
127 context = make(exception.__context__)
128 return cls(
129 type(exception),
130 filename,
131 lineno,
132 # Note that if __cause__ is set it is always equal to __context__, explicitly
133 # to support introspection when debugging, so we can use that unconditionally.
134 context,
135 # We distinguish exception groups by the inner exceptions, as for __context__
136 (
137 tuple(make(exc) for exc in exception.exceptions if exc not in seen)
138 if isinstance(exception, BaseExceptionGroup)
139 else ()
140 ),
141 )
142
143
144current_pytest_item = DynamicVariable(None)
145
146
147def _get_exceptioninfo():
148 # ExceptionInfo was moved to the top-level namespace in Pytest 7.0
149 if "pytest" in sys.modules:
150 with contextlib.suppress(Exception):
151 # From Pytest 7, __init__ warns on direct calls.
152 return sys.modules["pytest"].ExceptionInfo.from_exc_info
153 if "_pytest._code" in sys.modules: # old versions only
154 with contextlib.suppress(Exception):
155 return sys.modules["_pytest._code"].ExceptionInfo
156 return None # pragma: no cover # coverage tests always use pytest
157
158
159def format_exception(err, tb):
160 # Try using Pytest to match the currently configured traceback style
161 ExceptionInfo = _get_exceptioninfo()
162 if current_pytest_item.value is not None and ExceptionInfo is not None:
163 item = current_pytest_item.value
164 return str(item.repr_failure(ExceptionInfo((type(err), err, tb)))) + "\n"
165
166 # Or use better_exceptions, if that's installed and enabled
167 if "better_exceptions" in sys.modules:
168 better_exceptions = sys.modules["better_exceptions"]
169 if sys.excepthook is better_exceptions.excepthook:
170 return "".join(better_exceptions.format_exception(type(err), err, tb))
171
172 # If all else fails, use the standard-library formatting tools
173 return "".join(traceback.format_exception(type(err), err, tb))