Skip to content

Commit 9116d12

Browse files
authored
Merge pull request #4384 from tybug/realize-for-failure
Add `for_failure: bool = False` to `provider.realize`
2 parents 6f4c1f8 + 24c7a97 commit 9116d12

File tree

6 files changed

+70
-18
lines changed

6 files changed

+70
-18
lines changed

hypothesis-python/RELEASE.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
RELEASE_TYPE: patch
2+
3+
Add a ``for_failure: bool = False`` parameter to ``provider.realize`` in :ref:`alternative backends <alternative-backends>`, so that symbolic-based backends can increase their timeouts when realizing failures, which are more important than regular examples.

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

+5-6
Original file line numberDiff line numberDiff line change
@@ -850,10 +850,9 @@ def draw_float(
850850
*,
851851
allow_nan: bool = True,
852852
smallest_nonzero_magnitude: float = SMALLEST_SUBNORMAL,
853-
# TODO: consider supporting these float widths at the IR level in the
854-
# future.
853+
# TODO: consider supporting these float widths at the choice sequence
854+
# level in the future.
855855
# width: Literal[16, 32, 64] = 64,
856-
# exclude_min and exclude_max handled higher up,
857856
forced: Optional[float] = None,
858857
observe: bool = True,
859858
) -> float:
@@ -1006,12 +1005,12 @@ def _pop_choice(
10061005
bytes: "bytes",
10071006
}[type(choice)]
10081007
# If we're trying to:
1009-
# * draw a different ir type at the same location
1010-
# * draw the same ir type with a different constraints, which does not permit
1008+
# * draw a different choice type at the same location
1009+
# * draw the same choice type with a different constraints, which does not permit
10111010
# the current value
10121011
#
10131012
# then we call this a misalignment, because the choice sequence has
1014-
# slipped from what we expected at some point. An easy misalignment is
1013+
# changed from what we expected at some point. An easy misalignment is
10151014
#
10161015
# one_of(integers(0, 100), integers(101, 200))
10171016
#

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

+26-6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# obtain one at https://mozilla.org/MPL/2.0/.
1010

1111
import importlib
12+
import inspect
1213
import math
1314
import textwrap
1415
import time
@@ -23,7 +24,7 @@
2324
import attr
2425

2526
from hypothesis import HealthCheck, Phase, Verbosity, settings as Settings
26-
from hypothesis._settings import local_settings
27+
from hypothesis._settings import local_settings, note_deprecation
2728
from hypothesis.database import ExampleDatabase, choices_from_bytes, choices_to_bytes
2829
from hypothesis.errors import (
2930
BackendCannotProceed,
@@ -213,9 +214,25 @@ def kill_branch(self) -> NoReturn:
213214
raise ContainsDiscard
214215

215216

216-
def realize_choices(data: ConjectureData) -> None:
217+
def realize_choices(data: ConjectureData, *, for_failure: bool) -> None:
218+
# backwards-compatibility with backends without for_failure, can remove
219+
# in a few months
220+
kwargs = {}
221+
if for_failure:
222+
if "for_failure" in inspect.signature(data.provider.realize).parameters:
223+
kwargs["for_failure"] = True
224+
else:
225+
note_deprecation(
226+
f"{type(data.provider).__qualname__}.realize does not have the "
227+
"for_failure parameter. This will be an error in future versions "
228+
"of Hypothesis. (If you installed this backend from a separate "
229+
"package, upgrading that package may help).",
230+
has_codemod=False,
231+
since="RELEASEDAY",
232+
)
233+
217234
for node in data.nodes:
218-
value = data.provider.realize(node.value)
235+
value = data.provider.realize(node.value, **kwargs)
219236
expected_type = {
220237
"string": str,
221238
"float": float,
@@ -231,7 +248,10 @@ def realize_choices(data: ConjectureData) -> None:
231248

232249
constraints = cast(
233250
ChoiceConstraintsT,
234-
{k: data.provider.realize(v) for k, v in node.constraints.items()},
251+
{
252+
k: data.provider.realize(v, **kwargs)
253+
for k, v in node.constraints.items()
254+
},
235255
)
236256
node.value = value
237257
node.constraints = constraints
@@ -497,7 +517,7 @@ def test_function(self, data: ConjectureData) -> None:
497517
except BaseException:
498518
data.freeze()
499519
if self.settings.backend != "hypothesis":
500-
realize_choices(data)
520+
realize_choices(data, for_failure=True)
501521
self.save_choices(data.choices)
502522
raise
503523
finally:
@@ -517,7 +537,7 @@ def test_function(self, data: ConjectureData) -> None:
517537
}
518538
self.stats_per_test_case.append(call_stats)
519539
if self.settings.backend != "hypothesis":
520-
realize_choices(data)
540+
realize_choices(data, for_failure=data.status is Status.INTERESTING)
521541

522542
self._cache(data)
523543
if data.misaligned_at is not None: # pragma: no branch # coverage bug?

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

+9-2
Original file line numberDiff line numberDiff line change
@@ -317,15 +317,22 @@ def __init__(self, conjecturedata: Optional["ConjectureData"], /) -> None:
317317
def per_test_case_context_manager(self):
318318
return contextlib.nullcontext()
319319

320-
def realize(self, value: T) -> T:
320+
def realize(self, value: T, *, for_failure: bool = False) -> T:
321321
"""
322322
Called whenever hypothesis requires a concrete (non-symbolic) value from
323323
a potentially symbolic value. Hypothesis will not check that `value` is
324324
symbolic before calling `realize`, so you should handle the case where
325325
`value` is non-symbolic.
326326
327327
The returned value should be non-symbolic. If you cannot provide a value,
328-
raise hypothesis.errors.BackendCannotProceed("discard_test_case")
328+
raise hypothesis.errors.BackendCannotProceed("discard_test_case").
329+
330+
If for_failure is True, the value is associated with a failing example.
331+
In this case, the backend should spend substantially more effort when
332+
attempting to realize the value, since it is important to avoid discarding
333+
failing examples. Backends may still raise BackendCannotProceed when
334+
for_failure is True, if realization is truly impossible or if realization
335+
takes significantly longer than expected (say, 5 minutes).
329336
"""
330337
return value
331338

hypothesis-python/tests/conjecture/test_alt_backend.py

+26-4
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@
4747
from hypothesis.internal.intervalsets import IntervalSet
4848

4949
from tests.common.debug import minimal
50-
from tests.common.utils import capture_observations, capture_out
50+
from tests.common.utils import (
51+
capture_observations,
52+
capture_out,
53+
checks_deprecated_behaviour,
54+
)
5155
from tests.conjecture.common import nodes
5256

5357

@@ -377,7 +381,7 @@ def test_function(n):
377381

378382

379383
class BadRealizeProvider(TrivialProvider):
380-
def realize(self, value):
384+
def realize(self, value, *, for_failure=False):
381385
return None
382386

383387

@@ -399,7 +403,7 @@ def test_function(n):
399403
class RealizeProvider(TrivialProvider):
400404
avoid_realization = True
401405

402-
def realize(self, value):
406+
def realize(self, value, *, for_failure=False):
403407
if isinstance(value, int):
404408
return 42
405409
return value
@@ -473,7 +477,7 @@ def observe_information_messages(self, *, lifetime):
473477
yield {"type": "alert", "title": "Trivial alert", "content": "message here"}
474478
yield {"type": "info", "title": "trivial-data", "content": {"k2": "v2"}}
475479

476-
def realize(self, value):
480+
def realize(self, value, *, for_failure=False):
477481
# Get coverage of the can't-realize path for observability outputs
478482
raise BackendCannotProceed
479483

@@ -665,3 +669,21 @@ def f(n):
665669
# full message as of writing: "backend='soundness_test' claimed to
666670
# verify this test passes - please send them a bug report!"
667671
assert all("backend" not in note for note in e.value.__notes__)
672+
673+
674+
class NoForFailureProvider(TrivialProvider):
675+
def realize(self, value):
676+
return value
677+
678+
679+
@checks_deprecated_behaviour
680+
def test_realize_without_for_failure():
681+
with temp_register_backend("no_for_failure", NoForFailureProvider):
682+
683+
@given(st.integers())
684+
@settings(backend="no_for_failure", database=None)
685+
def f(n):
686+
assert n != 1
687+
688+
with pytest.raises(AssertionError):
689+
f()

pytest.ini

+1
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,4 @@ filterwarnings =
2424
default:`np\.object` is a deprecated alias for the builtin `object`:DeprecationWarning
2525
# pytest-cov can't see into subprocesses; we'll see <100% covered if this is an issue
2626
ignore:Module hypothesis.* was previously imported, but not measured
27+
ignore:CrosshairPrimitiveProvider.realize does not have the for_failure parameter

0 commit comments

Comments
 (0)