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