aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/hypothesis/py3/hypothesis/internal/escalation.py
blob: 9c242ba0c2870177cd7f96454d212a2d9e1a44c3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Copyright the Hypothesis Authors.
# Individual contributors are listed in AUTHORS.rst and the git log.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.

import contextlib
import os
import sys
import textwrap
import traceback
from inspect import getframeinfo
from pathlib import Path
from typing import Dict, NamedTuple, Optional, Type

import hypothesis
from hypothesis.errors import _Trimmable
from hypothesis.internal.compat import BaseExceptionGroup
from hypothesis.utils.dynamicvariables import DynamicVariable


def belongs_to(package):
    if not hasattr(package, "__file__"):  # pragma: no cover
        return lambda filepath: False

    root = Path(package.__file__).resolve().parent
    cache = {str: {}, bytes: {}}

    def accept(filepath):
        ftype = type(filepath)
        try:
            return cache[ftype][filepath]
        except KeyError:
            pass
        try:
            Path(filepath).resolve().relative_to(root)
            result = True
        except Exception:
            result = False
        cache[ftype][filepath] = result
        return result

    accept.__name__ = f"is_{package.__name__}_file"
    return accept


FILE_CACHE: Dict[bytes, bool] = {}


is_hypothesis_file = belongs_to(hypothesis)


def get_trimmed_traceback(exception=None):
    """Return the current traceback, minus any frames added by Hypothesis."""
    if exception is None:
        _, exception, tb = sys.exc_info()
    else:
        tb = exception.__traceback__
    # Avoid trimming the traceback if we're in verbose mode, or the error
    # was raised inside Hypothesis. Additionally, the environment variable
    # HYPOTHESIS_NO_TRACEBACK_TRIM is respected if nonempty, because verbose
    # mode is prohibitively slow when debugging strategy recursion errors.
    if (
        tb is None
        or os.environ.get("HYPOTHESIS_NO_TRACEBACK_TRIM", None)
        or hypothesis.settings.default.verbosity >= hypothesis.Verbosity.debug
        or is_hypothesis_file(traceback.extract_tb(tb)[-1][0])
        and not isinstance(exception, _Trimmable)
    ):
        return tb
    while tb.tb_next is not None and (
        # If the frame is from one of our files, it's been added by Hypothesis.
        is_hypothesis_file(getframeinfo(tb.tb_frame).filename)
        # But our `@proxies` decorator overrides the source location,
        # so we check for an attribute it injects into the frame too.
        or tb.tb_frame.f_globals.get("__hypothesistracebackhide__") is True
    ):
        tb = tb.tb_next
    return tb


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.
    exc_type: Type[BaseException]
    filename: Optional[str]
    lineno: Optional[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
        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 ()
            ),
        )


current_pytest_item = DynamicVariable(None)


def _get_exceptioninfo():
    # ExceptionInfo was moved to the top-level namespace in Pytest 7.0
    if "pytest" in sys.modules:
        with contextlib.suppress(Exception):
            # From Pytest 7, __init__ warns on direct calls.
            return sys.modules["pytest"].ExceptionInfo.from_exc_info
    if "_pytest._code" in sys.modules:  # old versions only
        with contextlib.suppress(Exception):
            return sys.modules["_pytest._code"].ExceptionInfo
    return None  # pragma: no cover  # coverage tests always use pytest


def format_exception(err, tb):
    # Try using Pytest to match the currently configured traceback style
    ExceptionInfo = _get_exceptioninfo()
    if current_pytest_item.value is not None and ExceptionInfo is not None:
        item = current_pytest_item.value
        return str(item.repr_failure(ExceptionInfo((type(err), err, tb)))) + "\n"

    # Or use better_exceptions, if that's installed and enabled
    if "better_exceptions" in sys.modules:
        better_exceptions = sys.modules["better_exceptions"]
        if sys.excepthook is better_exceptions.excepthook:
            return "".join(better_exceptions.format_exception(type(err), err, tb))

    # If all else fails, use the standard-library formatting tools
    return "".join(traceback.format_exception(type(err), err, tb))