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