Skip to content

Commit d319af6

Browse files
authored
Merge pull request #3444 from Zac-HD/annotate-internals
Skip uninformative locations in explain mode, misc internal cleanups
2 parents 45b6484 + 90c4b7b commit d319af6

File tree

15 files changed

+124
-129
lines changed

15 files changed

+124
-129
lines changed

hypothesis-python/RELEASE.rst

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
RELEASE_TYPE: patch
2+
3+
This patch fixes some type annotations for Python 3.9 and earlier (:issue:`3397`),
4+
and teaches :ref:`explain mode <phases>` about certain locations it should not
5+
bother reporting (:issue:`3439`).

hypothesis-python/src/hypothesis/core.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -881,8 +881,7 @@ def run_engine(self):
881881
errors_to_report.append((fragments, err))
882882
except BaseException as e:
883883
# If we have anything for explain-mode, this is the time to report.
884-
for line in explanations[falsifying_example.interesting_origin]:
885-
fragments.append(line)
884+
fragments.extend(explanations[falsifying_example.interesting_origin])
886885
errors_to_report.append(
887886
(fragments, e.with_traceback(get_trimmed_traceback()))
888887
)

hypothesis-python/src/hypothesis/internal/compat.py

+13
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,19 @@
2222
BaseExceptionGroup as BaseExceptionGroup,
2323
ExceptionGroup as ExceptionGroup,
2424
)
25+
if typing.TYPE_CHECKING: # pragma: no cover
26+
from typing_extensions import Concatenate as Concatenate, ParamSpec as ParamSpec
27+
else:
28+
try:
29+
from typing import Concatenate as Concatenate, ParamSpec as ParamSpec
30+
except ImportError:
31+
try:
32+
from typing_extensions import (
33+
Concatenate as Concatenate,
34+
ParamSpec as ParamSpec,
35+
)
36+
except ImportError:
37+
Concatenate, ParamSpec = None, None
2538

2639
PYPY = platform.python_implementation() == "PyPy"
2740
WINDOWS = platform.system() == "Windows"

hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py

+20-33
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,8 @@ def __init__(self, engine, initial, predicate, allow_transition):
278278

279279
# We keep track of the current best example on the shrink_target
280280
# attribute.
281-
self.shrink_target = None
282-
self.update_shrink_target(initial)
281+
self.shrink_target = initial
282+
self.clear_change_tracking()
283283
self.shrinks = 0
284284

285285
# We terminate shrinks that seem to have reached their logical
@@ -447,23 +447,15 @@ def s(n):
447447
return "s" if n != 1 else ""
448448

449449
total_deleted = self.initial_size - len(self.shrink_target.buffer)
450-
451-
self.debug("---------------------")
452-
self.debug("Shrink pass profiling")
453-
self.debug("---------------------")
454-
self.debug("")
455450
calls = self.engine.call_count - self.initial_calls
451+
456452
self.debug(
457-
"Shrinking made a total of %d call%s "
458-
"of which %d shrank. This deleted %d byte%s out of %d."
459-
% (
460-
calls,
461-
s(calls),
462-
self.shrinks,
463-
total_deleted,
464-
s(total_deleted),
465-
self.initial_size,
466-
)
453+
"---------------------\n"
454+
"Shrink pass profiling\n"
455+
"---------------------\n\n"
456+
f"Shrinking made a total of {calls} call{s(calls)} of which "
457+
f"{self.shrinks} shrank. This deleted {total_deleted} bytes out "
458+
f"of {self.initial_size}."
467459
)
468460
for useful in [True, False]:
469461
self.debug("")
@@ -828,22 +820,17 @@ def __changed_blocks(self):
828820

829821
def update_shrink_target(self, new_target):
830822
assert isinstance(new_target, ConjectureResult)
831-
if self.shrink_target is not None:
832-
self.shrinks += 1
833-
# If we are just taking a long time to shrink we don't want to
834-
# trigger this heuristic, so whenever we shrink successfully
835-
# we give ourselves a bit of breathing room to make sure we
836-
# would find a shrink that took that long to find the next time.
837-
# The case where we're taking a long time but making steady
838-
# progress is handled by `finish_shrinking_deadline` in engine.py
839-
self.max_stall = max(
840-
self.max_stall, (self.calls - self.calls_at_last_shrink) * 2
841-
)
842-
self.calls_at_last_shrink = self.calls
843-
else:
844-
self.__all_changed_blocks = set()
845-
self.__last_checked_changed_at = new_target
846-
823+
self.shrinks += 1
824+
# If we are just taking a long time to shrink we don't want to
825+
# trigger this heuristic, so whenever we shrink successfully
826+
# we give ourselves a bit of breathing room to make sure we
827+
# would find a shrink that took that long to find the next time.
828+
# The case where we're taking a long time but making steady
829+
# progress is handled by `finish_shrinking_deadline` in engine.py
830+
self.max_stall = max(
831+
self.max_stall, (self.calls - self.calls_at_last_shrink) * 2
832+
)
833+
self.calls_at_last_shrink = self.calls
847834
self.shrink_target = new_target
848835
self.__derived_values = {}
849836

hypothesis-python/src/hypothesis/internal/reflection.py

-4
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,6 @@ def function_digest(function):
6464
hasher.update(function.__name__.encode())
6565
except AttributeError:
6666
pass
67-
try:
68-
hasher.update(function.__module__.__name__.encode())
69-
except AttributeError:
70-
pass
7167
try:
7268
# We prefer to use the modern signature API, but left this for compatibility.
7369
# While we don't promise stability of the database, there's no advantage to

hypothesis-python/src/hypothesis/internal/scrutineer.py

+25-7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from collections import defaultdict
1313
from functools import lru_cache, reduce
1414
from itertools import groupby
15+
from os import sep
1516
from pathlib import Path
1617

1718
from hypothesis._settings import Phase, Verbosity
@@ -45,6 +46,20 @@ def trace(self, frame, event, arg):
4546
self._previous_location = current_location
4647

4748

49+
UNHELPFUL_LOCATIONS = (
50+
# There's a branch which is only taken when an exception is active while exiting
51+
# a contextmanager; this is probably after the fault has been triggered.
52+
# Similar reasoning applies to a few other standard-library modules: even
53+
# if the fault was later, these still aren't useful locations to report!
54+
f"{sep}contextlib.py",
55+
f"{sep}inspect.py",
56+
f"{sep}re.py",
57+
f"{sep}re{sep}__init__.py", # refactored in Python 3.11
58+
# Quite rarely, the first AFNP line is in Pytest's assertion-rewriting module.
59+
f"{sep}_pytest{sep}assertion{sep}rewrite.py",
60+
)
61+
62+
4863
def get_explaining_locations(traces):
4964
# Traces is a dict[interesting_origin | None, set[frozenset[tuple[str, int]]]]
5065
# Each trace in the set might later become a Counter instead of frozenset.
@@ -84,21 +99,25 @@ def get_explaining_locations(traces):
8499
else:
85100
queue.update(cf_graphs[origin][src] - seen)
86101

87-
return explanations
102+
# The last step is to filter out explanations that we know would be uninformative.
103+
# When this is the first AFNP location, we conclude that Scrutineer missed the
104+
# real divergence (earlier in the trace) and drop that unhelpful explanation.
105+
return {
106+
origin: {loc for loc in afnp_locs if not loc[0].endswith(UNHELPFUL_LOCATIONS)}
107+
for origin, afnp_locs in explanations.items()
108+
}
88109

89110

90111
LIB_DIR = str(Path(sys.executable).parent / "lib")
91112
EXPLANATION_STUB = (
92113
"Explanation:",
93114
" These lines were always and only run by failing examples:",
94115
)
95-
HAD_TRACE = " We didn't try to explain this, because sys.gettrace()="
96116

97117

98118
def make_report(explanations, cap_lines_at=5):
99119
report = defaultdict(list)
100120
for origin, locations in explanations.items():
101-
assert locations # or else we wouldn't have stored the key, above.
102121
report_lines = [
103122
" {}:{}".format(k, ", ".join(map(str, sorted(l for _, l in v))))
104123
for k, v in groupby(locations, lambda kv: kv[0])
@@ -107,15 +126,14 @@ def make_report(explanations, cap_lines_at=5):
107126
if len(report_lines) > cap_lines_at + 1:
108127
msg = " (and {} more with settings.verbosity >= verbose)"
109128
report_lines[cap_lines_at:] = [msg.format(len(report_lines[cap_lines_at:]))]
110-
report[origin] = list(EXPLANATION_STUB) + report_lines
129+
if report_lines: # We might have filtered out every location as uninformative.
130+
report[origin] = list(EXPLANATION_STUB) + report_lines
111131
return report
112132

113133

114134
def explanatory_lines(traces, settings):
115135
if Phase.explain in settings.phases and sys.gettrace() and not traces:
116-
return defaultdict(
117-
lambda: [EXPLANATION_STUB[0], HAD_TRACE + repr(sys.gettrace())]
118-
)
136+
return defaultdict(list)
119137
# Return human-readable report lines summarising the traces
120138
explanations = get_explaining_locations(traces)
121139
max_lines = 5 if settings.verbosity <= Verbosity.normal else 100

hypothesis-python/src/hypothesis/strategies/_internal/core.py

+8-9
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,14 @@
4949
from hypothesis.errors import InvalidArgument, ResolutionFailed
5050
from hypothesis.internal.cathetus import cathetus
5151
from hypothesis.internal.charmap import as_general_categories
52-
from hypothesis.internal.compat import ceil, floor, get_type_hints, is_typed_named_tuple
52+
from hypothesis.internal.compat import (
53+
Concatenate,
54+
ParamSpec,
55+
ceil,
56+
floor,
57+
get_type_hints,
58+
is_typed_named_tuple,
59+
)
5360
from hypothesis.internal.conjecture.utils import (
5461
calc_label_from_cls,
5562
check_sample,
@@ -122,14 +129,6 @@
122129
except ImportError: # < py3.8
123130
Protocol = object # type: ignore[assignment]
124131

125-
try:
126-
from typing import Concatenate, ParamSpec
127-
except ImportError:
128-
try:
129-
from typing_extensions import Concatenate, ParamSpec
130-
except ImportError:
131-
ParamSpec = None # type: ignore
132-
133132

134133
@cacheable
135134
@defines_strategy()

hypothesis-python/tests/common/setup.py

+7-14
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from tempfile import mkdtemp
1313
from warnings import filterwarnings
1414

15-
from hypothesis import Verbosity, settings
15+
from hypothesis import Phase, Verbosity, settings
1616
from hypothesis._settings import not_set
1717
from hypothesis.configuration import set_hypothesis_home_dir
1818
from hypothesis.errors import NonInteractiveExampleWarning
@@ -25,10 +25,7 @@ def run():
2525
filterwarnings("ignore", category=ImportWarning)
2626
filterwarnings("ignore", category=FutureWarning, module="pandas._version")
2727

28-
# Fixed in recent versions but allowed by pytest=3.0.0; see #1630
29-
filterwarnings("ignore", category=DeprecationWarning, module="pluggy")
30-
31-
# See https://github.com/numpy/numpy/pull/432
28+
# See https://github.com/numpy/numpy/pull/432; still a thing as of 2022.
3229
filterwarnings("ignore", message="numpy.dtype size changed")
3330
filterwarnings("ignore", message="numpy.ufunc size changed")
3431

@@ -42,14 +39,6 @@ def run():
4239
category=UserWarning,
4340
)
4441

45-
# Imported by Pandas in version 1.9, but fixed in later versions.
46-
filterwarnings(
47-
"ignore", message="Importing from numpy.testing.decorators is deprecated"
48-
)
49-
filterwarnings(
50-
"ignore", message="Importing from numpy.testing.nosetester is deprecated"
51-
)
52-
5342
# User-facing warning which does not apply to our own tests
5443
filterwarnings("ignore", category=NonInteractiveExampleWarning)
5544

@@ -77,7 +66,11 @@ def run():
7766
)
7867

7968
settings.register_profile(
80-
"default", settings(max_examples=20 if IN_COVERAGE_TESTS else not_set)
69+
"default",
70+
settings(
71+
max_examples=20 if IN_COVERAGE_TESTS else not_set,
72+
phases=list(Phase), # Dogfooding the explain phase
73+
),
8174
)
8275

8376
settings.register_profile("speedy", settings(max_examples=5))

hypothesis-python/tests/cover/test_phases.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import pytest
1212

1313
from hypothesis import Phase, example, given, settings, strategies as st
14+
from hypothesis._settings import all_settings
1415
from hypothesis.database import ExampleDatabase, InMemoryExampleDatabase
1516
from hypothesis.errors import InvalidArgument
1617

@@ -47,7 +48,7 @@ def test_sorts_and_dedupes_phases(arg, expected):
4748

4849

4950
def test_phases_default_to_all_except_explain():
50-
assert settings().phases + (Phase.explain,) == tuple(Phase)
51+
assert all_settings["phases"].default + (Phase.explain,) == tuple(Phase)
5152

5253

5354
def test_does_not_reuse_saved_examples_if_reuse_not_in_phases():

hypothesis-python/tests/cover/test_scrutineer.py

-46
This file was deleted.

hypothesis-python/tests/cover/test_settings.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,7 @@ def __repr__(self):
464464

465465

466466
def test_show_changed():
467-
s = settings(max_examples=999, database=None)
467+
s = settings(max_examples=999, database=None, phases=tuple(Phase)[:-1])
468468
assert s.show_changed() == "database=None, max_examples=999"
469469

470470

0 commit comments

Comments
 (0)