Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
RELEASE_TYPE: patch

This release fixes (:issue:`4681`), which would cause line numbers to frequently be wrong in tracebacks for
errors found by Hypothesis.
19 changes: 11 additions & 8 deletions hypothesis-python/src/hypothesis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,12 @@ def shortlex_key(error):
return result


# Exists solely to insert a line in the traceback where we need to.
def _reraise_exception_group(the_error_hypothesis_found):
__tracebackhide__ = False
raise the_error_hypothesis_found


def _raise_to_user(
errors_to_report, settings, target_lines, trailer="", *, unsound_backend=None
):
Expand Down Expand Up @@ -2248,19 +2254,16 @@ def wrapped_test(*arguments, **kwargs):
f"{pytest_extra_msg}."
)
report(msg)
# The dance here is to avoid showing users long tracebacks
# full of Hypothesis internals they don't care about.
# We have to do this inline, to avoid adding another
# internal stack frame just when we've removed the rest.
#
# Using a variable for our trimmed error ensures that the line
# which will actually appear in tracebacks is as clear as
# possible - "raise the_error_hypothesis_found".
# Trim the traceback to remove hypothesis internals
the_error_hypothesis_found = e.with_traceback(
Comment on lines 2256 to 2258

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree with removing this useful comment (did claude do this?)

None
if isinstance(e, BaseExceptionGroup)
else get_trimmed_traceback()
)
if isinstance(e, BaseExceptionGroup):
# Insert a frame here as otherwise all base frames are
# trimmed which causes pytest problems
_reraise_exception_group(the_error_hypothesis_found)
Comment on lines +2263 to +2266

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather fix this in Pytest than Hypothesis, optionally keeping a version-gated workaround here.

raise the_error_hypothesis_found

if not (ran_explicit_examples or state.ever_executed):
Expand Down
7 changes: 5 additions & 2 deletions hypothesis-python/src/hypothesis/extra/_patching.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ def get_patch_for(
if patch is None:
return None

(before, after) = patch
before, after = patch
return (str(fname), before, after)


Expand All @@ -196,8 +196,11 @@ def _get_patch_for(
strip_via: tuple[str, ...] = (),
namespace: dict[str, Any],
) -> tuple[str, str] | None:
# Follow __wrapped_target to the original function, since the wrapper's
# co_filename may not point to the real source file (see #4681).
source_func = getattr(func, "__wrapped_target", func)
try:
before = inspect.getsource(func)
before = inspect.getsource(source_func)
except Exception: # pragma: no cover
return None

Expand Down
27 changes: 20 additions & 7 deletions hypothesis-python/src/hypothesis/internal/escalation.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,27 @@ def get_trimmed_traceback(
)
):
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(getsourcefile(tb.tb_frame) or getfile(tb.tb_frame))
# 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
):

def _is_hypothesis_frame(frame):
return (
is_hypothesis_file(getsourcefile(frame) or getfile(frame))
or frame.f_globals.get("__hypothesistracebackhide__") is True
)

# Strip leading hypothesis frames
while tb.tb_next is not None and _is_hypothesis_frame(tb.tb_frame):
tb = tb.tb_next

# Strip any remaining hypothesis frames from the middle of the traceback
prev = tb
current = tb.tb_next
while current is not None:
if current.tb_next is not None and _is_hypothesis_frame(current.tb_frame):
prev.tb_next = current.tb_next
else:
prev = current
current = prev.tb_next
Comment on lines +94 to +102

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unlike stripping leading frames, this modifies the traceback object in place and will thus affect anyone else who has a copy for whatever reason. Avoiding that is necessarily more expensive though, so maybe we just leave a comment?


return tb


Expand Down
10 changes: 2 additions & 8 deletions hypothesis-python/src/hypothesis/internal/reflection.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,7 @@ def source_exec_as_module(source: str) -> ModuleType:

def accept({funcname}):
def {name}{signature}:
__tracebackhide__ = True
return {funcname}({invocation})
return {name}
""".lstrip()
Expand Down Expand Up @@ -468,18 +469,11 @@ def impersonate(target):
"""

def accept(f):
# Lie shamelessly about where this code comes from, to hide the hypothesis
# internals from pytest, ipython, and other runtime introspection.
f.__code__ = f.__code__.replace(
co_filename=target.__code__.co_filename,
co_firstlineno=target.__code__.co_firstlineno,
)
f.__name__ = target.__name__
f.__module__ = target.__module__
f.__doc__ = target.__doc__
f.__globals__["__hypothesistracebackhide__"] = True
# But leave an breadcrumb for _describe_lambda to follow, it's
# just confused by the lies above
# Leave a breadcrumb for _describe_lambda and _get_patch_for to follow
f.__wrapped_target = target
return f

Expand Down
2 changes: 1 addition & 1 deletion hypothesis-python/tests/cover/test_traceback_elision.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def simplest_failure(x):
except ValueError as e:
tb = traceback.extract_tb(e.__traceback__)
# Unless in debug mode, Hypothesis adds 1 frame - the least possible!
# (4 frames: this one, simplest_failure, internal frame, assert False)
# (4 frames: this one, simplest_failure, internal frame, raise ValueError)
if verbosity < Verbosity.debug and not env_value:
assert len(tb) == 4
else:
Expand Down
39 changes: 0 additions & 39 deletions hypothesis-python/tests/pytest/test_capture.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

import pytest

from hypothesis._settings import _CI_VARS
from hypothesis.internal.compat import WINDOWS, escape_unicode_characters

pytest_plugins = "pytester"
Expand Down Expand Up @@ -71,44 +70,6 @@ def test_output_emitting_unicode(testdir, monkeypatch):
assert result.ret == 0


def get_line_num(token, lines, skip_n=0):
skipped = 0
for i, line in enumerate(lines):
if token in line:
if skip_n == skipped:
return i
else:
skipped += 1
raise AssertionError(
f"Token {token!r} not found (skipped {skipped} of planned {skip_n} skips)"
)


TRACEBACKHIDE_HEALTHCHECK = """
from hypothesis import given, settings
from hypothesis.strategies import integers
import time
@given(integers().map(lambda x: time.sleep(0.2)))
def test_healthcheck_traceback_is_hidden(x):
pass
"""


def test_healthcheck_traceback_is_hidden(testdir, monkeypatch):
for key in _CI_VARS:
monkeypatch.delenv(key, raising=False)

script = testdir.makepyfile(TRACEBACKHIDE_HEALTHCHECK)
lines = testdir.runpytest(script, "--verbose").stdout.lines
def_token = "__ test_healthcheck_traceback_is_hidden __"
timeout_token = ": FailedHealthCheck"
def_line = get_line_num(def_token, lines)
timeout_line = get_line_num(timeout_token, lines)
# 16 on pytest 8.4.0 combined with py3{9, 10} or 3.13 free-threading (but
# not with 3.13 normal??)
assert timeout_line - def_line in {15, 16}


COMPOSITE_IS_NOT_A_TEST = """
from hypothesis.strategies import composite, none
@composite
Expand Down
9 changes: 9 additions & 0 deletions hypothesis-python/tests/snapshot/__init__.py
Comment thread
Zac-HD marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 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/.
Loading
Loading