# 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 ( DeadlineExceeded, HypothesisException, StopTest, UnsatisfiedAssumption, _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 PREVENT_ESCALATION = os.getenv("HYPOTHESIS_DO_NOT_ESCALATE") == "true" FILE_CACHE: Dict[bytes, bool] = {} is_hypothesis_file = belongs_to(hypothesis) HYPOTHESIS_CONTROL_EXCEPTIONS = (DeadlineExceeded, StopTest, UnsatisfiedAssumption) def escalate_hypothesis_internal_error(): if PREVENT_ESCALATION: return _, e, tb = sys.exc_info() if getattr(e, "hypothesis_internal_never_escalate", False): return filepath = None if tb is None else traceback.extract_tb(tb)[-1][0] if is_hypothesis_file(filepath) and not isinstance( e, (HypothesisException, *HYPOTHESIS_CONTROL_EXCEPTIONS) ): raise 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))