Skip to content

Commit f9d93e2

Browse files
authored
Merge pull request #3799 from HypothesisWorks/create-pull-request/patch
Update pinned dependencies
2 parents 47c286d + a3d9623 commit f9d93e2

File tree

26 files changed

+158
-177
lines changed

26 files changed

+158
-177
lines changed

hypothesis-python/.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ exclude_lines =
2828
if TYPE_CHECKING:
2929
if sys\.version_info
3030
if "[\w\.]+" in sys\.modules:
31+
if .+ := sys\.modules\.get\("[\w\.]+"\)

hypothesis-python/RELEASE.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
RELEASE_TYPE: minor
2+
3+
This release adds an optional ``payload`` argument to :func:`hypothesis.event`,
4+
so that you can clearly express the difference between the label and the value
5+
of an observation. :ref:`statistics` will still summarize it as a string, but
6+
future observability options can preserve the distinction.

hypothesis-python/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ def local_file(name):
117117
"Programming Language :: Python :: 3.9",
118118
"Programming Language :: Python :: 3.10",
119119
"Programming Language :: Python :: 3.11",
120+
"Programming Language :: Python :: 3.12",
120121
"Programming Language :: Python :: Implementation :: CPython",
121122
"Programming Language :: Python :: Implementation :: PyPy",
122123
"Topic :: Education :: Testing",

hypothesis-python/src/hypothesis/control.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import math
1212
from collections import defaultdict
1313
from typing import NoReturn, Union
14+
from weakref import WeakKeyDictionary
1415

1516
from hypothesis import Verbosity, settings
1617
from hypothesis._settings import note_deprecation
@@ -168,18 +169,38 @@ def note(value: str) -> None:
168169
report(value)
169170

170171

171-
def event(value: str) -> None:
172-
"""Record an event that occurred this test. Statistics on number of test
172+
def event(value: str, payload: Union[str, int, float] = "") -> None:
173+
"""Record an event that occurred during this test. Statistics on the number of test
173174
runs with each event will be reported at the end if you run Hypothesis in
174175
statistics reporting mode.
175176
176-
Events should be strings or convertible to them.
177+
Event values should be strings or convertible to them. If an optional
178+
payload is given, it will be included in the string for :ref:`statistics`.
177179
"""
178180
context = _current_build_context.value
179181
if context is None:
180182
raise InvalidArgument("Cannot make record events outside of a test")
181183

182-
context.data.note_event(value)
184+
payload = _event_to_string(payload, (str, int, float))
185+
context.data.events[_event_to_string(value)] = payload
186+
187+
188+
_events_to_strings: WeakKeyDictionary = WeakKeyDictionary()
189+
190+
191+
def _event_to_string(event, allowed_types=str):
192+
if isinstance(event, allowed_types):
193+
return event
194+
try:
195+
return _events_to_strings[event]
196+
except (KeyError, TypeError):
197+
pass
198+
result = str(event)
199+
try:
200+
_events_to_strings[event] = result
201+
except TypeError:
202+
pass
203+
return result
183204

184205

185206
def target(observation: Union[int, float], *, label: str = "") -> Union[int, float]:

hypothesis-python/src/hypothesis/core.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,10 @@
8080
from hypothesis.internal.conjecture.shrinker import sort_key
8181
from hypothesis.internal.entropy import deterministic_PRNG
8282
from hypothesis.internal.escalation import (
83+
InterestingOrigin,
8384
current_pytest_item,
8485
escalate_hypothesis_internal_error,
8586
format_exception,
86-
get_interesting_origin,
8787
get_trimmed_traceback,
8888
)
8989
from hypothesis.internal.healthcheck import fail_health_check
@@ -970,7 +970,7 @@ def _execute_once_for_engine(self, data):
970970

971971
self.failed_normally = True
972972

973-
interesting_origin = get_interesting_origin(e)
973+
interesting_origin = InterestingOrigin.from_exception(e)
974974
if trace: # pragma: no cover
975975
# Trace collection is explicitly disabled under coverage.
976976
self.explain_traces[interesting_origin].add(trace)
@@ -1037,7 +1037,9 @@ def run_engine(self):
10371037
info = falsifying_example.extra_information
10381038
fragments = []
10391039

1040-
ran_example = ConjectureData.for_buffer(falsifying_example.buffer)
1040+
ran_example = runner.new_conjecture_data_for_buffer(
1041+
falsifying_example.buffer
1042+
)
10411043
ran_example.slice_comments = falsifying_example.slice_comments
10421044
assert info.__expected_exception is not None
10431045
try:

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

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
Callable,
2121
Dict,
2222
FrozenSet,
23-
Hashable,
2423
Iterable,
2524
Iterator,
2625
List,
@@ -1367,7 +1366,7 @@ def __init__(
13671366
self.testcounter = global_test_counter
13681367
global_test_counter += 1
13691368
self.start_time = time.perf_counter()
1370-
self.events: "Union[Set[Hashable], FrozenSet[Hashable]]" = set()
1369+
self.events: Dict[str, Union[str, int, float]] = {}
13711370
self.forced_indices: "Set[int]" = set()
13721371
self.interesting_origin: Optional[InterestingOrigin] = None
13731372
self.draw_times: "List[float]" = []
@@ -1615,10 +1614,6 @@ def stop_example(self, *, discard: bool = False) -> None:
16151614

16161615
self.observer.kill_branch()
16171616

1618-
def note_event(self, event: Hashable) -> None:
1619-
assert isinstance(self.events, set)
1620-
self.events.add(event)
1621-
16221617
@property
16231618
def examples(self) -> Examples:
16241619
assert self.frozen
@@ -1643,7 +1638,6 @@ def freeze(self) -> None:
16431638
self.frozen = True
16441639

16451640
self.buffer = bytes(self.buffer)
1646-
self.events = frozenset(self.events)
16471641
self.observer.conclude_test(self.status, self.interesting_origin)
16481642

16491643
def draw_bits(self, n: int, *, forced: Optional[int] = None) -> int:
@@ -1729,7 +1723,7 @@ def mark_interesting(
17291723

17301724
def mark_invalid(self, why: Optional[str] = None) -> NoReturn:
17311725
if why is not None:
1732-
self.note_event(why)
1726+
self.events["invalid because"] = why
17331727
self.conclude_test(Status.INVALID)
17341728

17351729
def mark_overrun(self) -> NoReturn:

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

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from datetime import timedelta
1616
from enum import Enum
1717
from random import Random, getrandbits
18-
from weakref import WeakKeyDictionary
1918

2019
import attr
2120

@@ -101,8 +100,6 @@ def __init__(
101100
self.statistics = {}
102101
self.stats_per_test_case = []
103102

104-
self.events_to_strings = WeakKeyDictionary()
105-
106103
self.interesting_examples = {}
107104
# We use call_count because there may be few possible valid_examples.
108105
self.first_bug_found_at = None
@@ -209,7 +206,9 @@ def test_function(self, data):
209206
"status": data.status.name.lower(),
210207
"runtime": data.finish_time - data.start_time,
211208
"drawtime": math.fsum(data.draw_times),
212-
"events": sorted({self.event_to_string(e) for e in data.events}),
209+
"events": sorted(
210+
k if v == "" else f"{k}: {v}" for k, v in data.events.items()
211+
),
213212
}
214213
self.stats_per_test_case.append(call_stats)
215214
self.__data_cache[data.buffer] = data.as_result()
@@ -1055,20 +1054,6 @@ def kill_branch(self):
10551054
self.__data_cache[buffer] = result
10561055
return result
10571056

1058-
def event_to_string(self, event):
1059-
if isinstance(event, str):
1060-
return event
1061-
try:
1062-
return self.events_to_strings[event]
1063-
except (KeyError, TypeError):
1064-
pass
1065-
result = str(event)
1066-
try:
1067-
self.events_to_strings[event] = result
1068-
except TypeError:
1069-
pass
1070-
return result
1071-
10721057
def passing_buffers(self, prefix=b""):
10731058
"""Return a collection of bytestrings which cause the test to pass.
10741059

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

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
import contextlib
1212
import os
1313
import sys
14+
import textwrap
1415
import traceback
1516
from inspect import getframeinfo
1617
from pathlib import Path
17-
from typing import Dict
18+
from typing import Dict, NamedTuple, Optional, Type
1819

1920
import hypothesis
2021
from hypothesis.errors import (
@@ -105,32 +106,46 @@ def get_trimmed_traceback(exception=None):
105106
return tb
106107

107108

108-
def get_interesting_origin(exception):
109+
class InterestingOrigin(NamedTuple):
109110
# The `interesting_origin` is how Hypothesis distinguishes between multiple
110111
# failures, for reporting and also to replay from the example database (even
111112
# if report_multiple_bugs=False). We traditionally use the exception type and
112113
# location, but have extracted this logic in order to see through `except ...:`
113114
# blocks and understand the __cause__ (`raise x from y`) or __context__ that
114115
# first raised an exception as well as PEP-654 exception groups.
115-
tb = get_trimmed_traceback(exception)
116-
if tb is None:
116+
exc_type: Type[BaseException]
117+
filename: Optional[str]
118+
lineno: Optional[int]
119+
context: "InterestingOrigin | tuple[()]"
120+
group_elems: "tuple[InterestingOrigin, ...]"
121+
122+
def __str__(self) -> str:
123+
ctx = ""
124+
if self.context:
125+
ctx = textwrap.indent(f"\ncontext: {self.context}", prefix=" ")
126+
group = ""
127+
if self.group_elems:
128+
chunks = "\n ".join(str(x) for x in self.group_elems)
129+
group = textwrap.indent(f"\nchild exceptions:\n {chunks}", prefix=" ")
130+
return f"{self.exc_type.__name__} at {self.filename}:{self.lineno}{ctx}{group}"
131+
132+
@classmethod
133+
def from_exception(cls, exception: BaseException, /) -> "InterestingOrigin":
117134
filename, lineno = None, None
118-
else:
119-
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
120-
return (
121-
type(exception),
122-
filename,
123-
lineno,
124-
# Note that if __cause__ is set it is always equal to __context__, explicitly
125-
# to support introspection when debugging, so we can use that unconditionally.
126-
get_interesting_origin(exception.__context__) if exception.__context__ else (),
127-
# We distinguish exception groups by the inner exceptions, as for __context__
128-
tuple(
129-
map(get_interesting_origin, exception.exceptions)
135+
if tb := get_trimmed_traceback(exception):
136+
filename, lineno, *_ = traceback.extract_tb(tb)[-1]
137+
return cls(
138+
type(exception),
139+
filename,
140+
lineno,
141+
# Note that if __cause__ is set it is always equal to __context__, explicitly
142+
# to support introspection when debugging, so we can use that unconditionally.
143+
cls.from_exception(exception.__context__) if exception.__context__ else (),
144+
# We distinguish exception groups by the inner exceptions, as for __context__
145+
tuple(map(cls.from_exception, exception.exceptions))
130146
if isinstance(exception, BaseExceptionGroup)
131-
else []
132-
),
133-
)
147+
else (),
148+
)
134149

135150

136151
current_pytest_item = DynamicVariable(None)

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

Lines changed: 0 additions & 33 deletions
This file was deleted.

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,10 @@ def draw_naive_datetime_and_combine(self, data, tz):
163163
try:
164164
return replace_tzinfo(dt.datetime(**result), timezone=tz)
165165
except (ValueError, OverflowError):
166-
msg = "Failed to draw a datetime between %r and %r with timezone from %r."
167-
data.mark_invalid(msg % (self.min_value, self.max_value, self.tz_strat))
166+
data.mark_invalid(
167+
f"Failed to draw a datetime between {self.min_value!r} and "
168+
f"{self.max_value!r} with timezone from {self.tz_strat!r}."
169+
)
168170

169171

170172
@defines_strategy(force_reusable_values=True)

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

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from contextlib import contextmanager
1313

1414
from hypothesis.errors import InvalidArgument
15-
from hypothesis.internal.lazyformat import lazyformat
1615
from hypothesis.internal.reflection import get_pretty_function_description
1716
from hypothesis.internal.validation import check_type
1817
from hypothesis.strategies._internal.strategies import (
@@ -112,13 +111,7 @@ def do_draw(self, data):
112111
with self.limited_base.capped(self.max_leaves):
113112
return data.draw(self.strategy)
114113
except LimitReached:
115-
# Workaround for possible coverage bug - this branch is definitely
116-
# covered but for some reason is showing up as not covered.
117-
if count == 0: # pragma: no branch
118-
data.note_event(
119-
lazyformat(
120-
"Draw for %r exceeded max_leaves and had to be retried",
121-
self,
122-
)
123-
)
114+
if count == 0:
115+
msg = f"Draw for {self!r} exceeded max_leaves and had to be retried"
116+
data.events[msg] = ""
124117
count += 1

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

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
combine_labels,
4444
)
4545
from hypothesis.internal.coverage import check_function
46-
from hypothesis.internal.lazyformat import lazyformat
4746
from hypothesis.internal.reflection import (
4847
get_pretty_function_description,
4948
is_identity_function,
@@ -550,7 +549,7 @@ def do_filtered_draw(self, data):
550549
if element is not filter_not_satisfied:
551550
return element
552551
if not known_bad_indices:
553-
FilteredStrategy.note_retried(self, data)
552+
data.events[f"Retried draw from {self!r} to satisfy filter"] = ""
554553
known_bad_indices.add(i)
555554

556555
# If we've tried all the possible elements, give up now.
@@ -940,9 +939,6 @@ def do_draw(self, data: ConjectureData) -> Ex:
940939
data.mark_invalid(f"Aborted test because unable to satisfy {self!r}")
941940
raise NotImplementedError("Unreachable, for Mypy")
942941

943-
def note_retried(self, data):
944-
data.note_event(lazyformat("Retried draw from %r to satisfy filter", self))
945-
946942
def do_filtered_draw(self, data):
947943
for i in range(3):
948944
start_index = data.index
@@ -954,7 +950,7 @@ def do_filtered_draw(self, data):
954950
else:
955951
data.stop_example(discard=True)
956952
if i == 0:
957-
self.note_retried(data)
953+
data.events[f"Retried draw from {self!r} to satisfy filter"] = ""
958954
# This is to guard against the case where we consume no data.
959955
# As long as we consume data, we'll eventually pass or raise.
960956
# But if we don't this could be an infinite loop.

0 commit comments

Comments
 (0)