Skip to content

Commit a3d9623

Browse files
committed
Events can have a payload now
1 parent 63c5298 commit a3d9623

15 files changed

Lines changed: 71 additions & 130 deletions

File tree

hypothesis-python/RELEASE.rst

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
RELEASE_TYPE: patch
1+
RELEASE_TYPE: minor
22

3-
This patch updates our vendored `list of top-level domains <https://www.iana.org/domains/root/db>`__,
4-
which is used by the provisional :func:`~hypothesis.provisional.domains` strategy.
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/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/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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import traceback
1616
from inspect import getframeinfo
1717
from pathlib import Path
18-
from typing import Dict, NamedTuple, Type
18+
from typing import Dict, NamedTuple, Optional, Type
1919

2020
import hypothesis
2121
from hypothesis.errors import (
@@ -114,8 +114,8 @@ class InterestingOrigin(NamedTuple):
114114
# blocks and understand the __cause__ (`raise x from y`) or __context__ that
115115
# first raised an exception as well as PEP-654 exception groups.
116116
exc_type: Type[BaseException]
117-
filename: str
118-
lineno: int
117+
filename: Optional[str]
118+
lineno: Optional[int]
119119
context: "InterestingOrigin | tuple[()]"
120120
group_elems: "tuple[InterestingOrigin, ...]"
121121

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.

hypothesis-python/tests/common/debug.py

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,8 @@
88
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
99
# obtain one at https://mozilla.org/MPL/2.0/.
1010

11-
from hypothesis import (
12-
HealthCheck,
13-
Phase,
14-
Verbosity,
15-
given,
16-
settings as Settings,
17-
strategies as st,
18-
)
11+
from hypothesis import HealthCheck, Phase, Verbosity, given, settings as Settings
1912
from hypothesis.errors import Found, NoSuchExample, Unsatisfiable
20-
from hypothesis.internal.conjecture.data import ConjectureData, StopTest
2113
from hypothesis.internal.reflection import get_pretty_function_description
2214

2315
from tests.common.utils import no_shrink
@@ -107,15 +99,3 @@ def assert_examples(s):
10799
assert predicate(s), msg
108100

109101
assert_examples()
110-
111-
112-
def assert_can_trigger_event(strategy, predicate):
113-
def test(buf):
114-
data = ConjectureData.for_buffer(buf)
115-
try:
116-
data.draw(strategy)
117-
except StopTest:
118-
pass
119-
return any(predicate(e) for e in data.events)
120-
121-
find_any(st.binary(), test)

0 commit comments

Comments
 (0)