Skip to content

Commit 70eda0f

Browse files
authored
Merge pull request #4536 from Liam-DeVoe/provider-conformance-realization
Copy `avoid_realization` in `run_conformance_test`
2 parents 275d496 + 2c062f8 commit 70eda0f

File tree

13 files changed

+189
-153
lines changed

13 files changed

+189
-153
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,8 @@ jobs:
227227
- alt-nocover
228228
- alt-rest
229229
exclude:
230-
- { os: macos-latest, python-architecture: "x86" }
231-
- { python-version: "3.13", python-architecture: "x86" }
230+
- { os: macos-latest, python-architecture: "x86" }
231+
- { python-version: "3.13", python-architecture: "x86" }
232232
- { python-version: "3.11", task: nocover }
233233
- { python-version: "3.11", task: rest }
234234
- { python-version: "3.13", task: alt-nocover }

hypothesis-python/RELEASE.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
RELEASE_TYPE: patch
2+
3+
Fixes our bundled |run_conformance_test| not respecting |PrimitiveProvider.avoid_realization|.

hypothesis-python/docs/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ Hypothesis is the property-based testing library for Python. With Hypothesis, yo
5151
5252
@given(st.lists(st.integers() | st.floats()))
5353
def test_sort_correct(lst):
54-
# lst is a random list of numbers
54+
# hypothesis generates random lists of numbers to test
5555
assert my_sort(lst) == sorted(lst)
5656
5757
test_sort_correct()

hypothesis-python/docs/prolog.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,10 @@
137137
.. |PrimitiveProvider.add_observability_callback| replace:: :data:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.add_observability_callback`
138138
.. |PrimitiveProvider.span_start| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.span_start`
139139
.. |PrimitiveProvider.span_end| replace:: :func:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.span_end`
140+
.. |PrimitiveProvider.avoid_realization| replace:: :data:`~hypothesis.internal.conjecture.providers.PrimitiveProvider.avoid_realization`
140141

141142
.. |AVAILABLE_PROVIDERS| replace:: :data:`~hypothesis.internal.conjecture.providers.AVAILABLE_PROVIDERS`
143+
.. |run_conformance_test| replace:: :func:`~hypothesis.internal.conjecture.provider_conformance.run_conformance_test`
142144

143145
.. |add_observability_callback| replace:: :data:`~hypothesis.internal.observability.add_observability_callback`
144146
.. |remove_observability_callback| replace:: :data:`~hypothesis.internal.observability.remove_observability_callback`

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

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -117,24 +117,7 @@ def __setitem__(self, key: K, value: V) -> None:
117117
raise ValueError(
118118
"Cannot increase size of cache where all keys have been pinned."
119119
) from None
120-
try:
121-
del self.keys_to_indices[evicted.key]
122-
except KeyError: # pragma: no cover
123-
# This can't happen, but happens nevertheless with
124-
# id(key1) == id(key2)
125-
# but
126-
# hash(key1) != hash(key2)
127-
# (see https://github.com/HypothesisWorks/hypothesis/issues/4442)
128-
# Rebuild keys_to_indices to match data.
129-
self.keys_to_indices.clear()
130-
self.keys_to_indices.update(
131-
{
132-
entry.key: i
133-
for i, entry in enumerate(self.data)
134-
if entry is not evicted
135-
}
136-
)
137-
assert len(self.keys_to_indices) == len(self.data) - 1
120+
del self.keys_to_indices[evicted.key]
138121
i = 0
139122
self.data[0] = entry
140123
else:

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,11 @@ class RunIsComplete(Exception):
170170

171171

172172
def _get_provider(backend: str) -> Union[type, PrimitiveProvider]:
173-
module_name, class_name = AVAILABLE_PROVIDERS[backend].rsplit(".", 1)
174-
provider_cls = getattr(importlib.import_module(module_name), class_name)
173+
provider_cls = AVAILABLE_PROVIDERS[backend]
174+
if isinstance(provider_cls, str):
175+
module_name, class_name = provider_cls.rsplit(".", 1)
176+
provider_cls = getattr(importlib.import_module(module_name), class_name)
177+
175178
if provider_cls.lifetime == "test_function":
176179
return provider_cls(None)
177180
elif provider_cls.lifetime == "test_case":

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

Lines changed: 128 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
from hypothesis.internal.conjecture.data import ConjectureData
3030
from hypothesis.internal.conjecture.providers import (
3131
COLLECTION_DEFAULT_MAX_SIZE,
32+
HypothesisProvider,
3233
PrimitiveProvider,
34+
with_register_backend,
3335
)
3436
from hypothesis.internal.floats import SMALLEST_SUBNORMAL, sign_aware_lte
3537
from hypothesis.internal.intervalsets import IntervalSet
@@ -369,119 +371,132 @@ def test_conformance():
369371
treat those exceptions as fatal errors.
370372
"""
371373

372-
@Settings(settings, suppress_health_check=[HealthCheck.too_slow])
373-
class ProviderConformanceTest(RuleBasedStateMachine):
374-
def __init__(self):
375-
super().__init__()
376-
377-
@initialize(random=st.randoms())
378-
def setup(self, random):
379-
if Provider.lifetime == "test_case":
380-
data = ConjectureData(random=random, provider=Provider)
381-
self.provider = data.provider
382-
else:
383-
self.provider = Provider(None)
384-
385-
self.context_manager = self.provider.per_test_case_context_manager()
386-
self.context_manager.__enter__()
387-
self.frozen = False
388-
389-
def _draw(self, choice_type, constraints):
390-
del constraints["forced"]
391-
draw_func = getattr(self.provider, f"draw_{choice_type}")
392-
393-
try:
394-
choice = draw_func(**constraints)
395-
note(f"drew {choice_type} {choice}")
396-
expected_type = {
397-
"integer": int,
398-
"float": float,
399-
"bytes": bytes,
400-
"string": str,
401-
"boolean": bool,
402-
}[choice_type]
403-
assert isinstance(choice, expected_type)
404-
assert choice_permitted(choice, constraints)
405-
except context_manager_exceptions as e:
406-
note(f"caught exception {type(e)} in context_manager_exceptions: {e}")
374+
class CopiesRealizationProvider(HypothesisProvider):
375+
avoid_realization = Provider.avoid_realization
376+
377+
with with_register_backend("copies_realization", CopiesRealizationProvider):
378+
379+
@Settings(
380+
settings,
381+
suppress_health_check=[HealthCheck.too_slow],
382+
backend="copies_realization",
383+
)
384+
class ProviderConformanceTest(RuleBasedStateMachine):
385+
def __init__(self):
386+
super().__init__()
387+
388+
@initialize(random=st.randoms())
389+
def setup(self, random):
390+
if Provider.lifetime == "test_case":
391+
data = ConjectureData(random=random, provider=Provider)
392+
self.provider = data.provider
393+
else:
394+
self.provider = Provider(None)
395+
396+
self.context_manager = self.provider.per_test_case_context_manager()
397+
self.context_manager.__enter__()
398+
self.frozen = False
399+
400+
def _draw(self, choice_type, constraints):
401+
del constraints["forced"]
402+
draw_func = getattr(self.provider, f"draw_{choice_type}")
403+
407404
try:
408-
self.context_manager.__exit__(type(e), e, None)
409-
except BackendCannotProceed:
410-
self.frozen = True
411-
return None
412-
413-
return choice
414-
415-
@precondition(lambda self: not self.frozen)
416-
@rule(constraints=integer_constraints())
417-
def draw_integer(self, constraints):
418-
self._draw("integer", constraints)
419-
420-
@precondition(lambda self: not self.frozen)
421-
@rule(constraints=float_constraints())
422-
def draw_float(self, constraints):
423-
self._draw("float", constraints)
424-
425-
@precondition(lambda self: not self.frozen)
426-
@rule(constraints=bytes_constraints())
427-
def draw_bytes(self, constraints):
428-
self._draw("bytes", constraints)
429-
430-
@precondition(lambda self: not self.frozen)
431-
@rule(constraints=string_constraints())
432-
def draw_string(self, constraints):
433-
self._draw("string", constraints)
434-
435-
@precondition(lambda self: not self.frozen)
436-
@rule(constraints=boolean_constraints())
437-
def draw_boolean(self, constraints):
438-
self._draw("boolean", constraints)
439-
440-
@precondition(lambda self: not self.frozen)
441-
@rule(label=st.integers())
442-
def span_start(self, label):
443-
self.provider.span_start(label)
444-
445-
@precondition(lambda self: not self.frozen)
446-
@rule(discard=st.booleans())
447-
def span_end(self, discard):
448-
self.provider.span_end(discard)
449-
450-
@precondition(lambda self: not self.frozen)
451-
@rule()
452-
def freeze(self):
453-
# phase-transition, mimicking data.freeze() at the end of a test case.
454-
self.frozen = True
455-
self.context_manager.__exit__(None, None, None)
456-
457-
@precondition(lambda self: self.frozen)
458-
@rule(value=_realize_objects)
459-
def realize(self, value):
460-
# filter out nans and weirder things
461-
try:
462-
assume(value == value)
463-
except Exception:
464-
# e.g. value = Decimal('-sNaN')
465-
assume(False)
466-
467-
# if `value` is non-symbolic, the provider should return it as-is.
468-
assert self.provider.realize(value) == value
469-
470-
@precondition(lambda self: self.frozen)
471-
@rule()
472-
def observe_test_case(self):
473-
observations = self.provider.observe_test_case()
474-
assert isinstance(observations, dict)
475-
476-
@precondition(lambda self: self.frozen)
477-
@rule(lifetime=st.sampled_from(["test_function", "test_case"]))
478-
def observe_information_messages(self, lifetime):
479-
observations = self.provider.observe_information_messages(lifetime=lifetime)
480-
for observation in observations:
481-
assert isinstance(observation, dict)
482-
483-
def teardown(self):
484-
if not self.frozen:
405+
choice = draw_func(**constraints)
406+
note(f"drew {choice_type} {choice}")
407+
expected_type = {
408+
"integer": int,
409+
"float": float,
410+
"bytes": bytes,
411+
"string": str,
412+
"boolean": bool,
413+
}[choice_type]
414+
assert isinstance(choice, expected_type)
415+
assert choice_permitted(choice, constraints)
416+
except context_manager_exceptions as e:
417+
note(
418+
f"caught exception {type(e)} in context_manager_exceptions: {e}"
419+
)
420+
try:
421+
self.context_manager.__exit__(type(e), e, None)
422+
except BackendCannotProceed:
423+
self.frozen = True
424+
return None
425+
426+
return choice
427+
428+
@precondition(lambda self: not self.frozen)
429+
@rule(constraints=integer_constraints())
430+
def draw_integer(self, constraints):
431+
self._draw("integer", constraints)
432+
433+
@precondition(lambda self: not self.frozen)
434+
@rule(constraints=float_constraints())
435+
def draw_float(self, constraints):
436+
self._draw("float", constraints)
437+
438+
@precondition(lambda self: not self.frozen)
439+
@rule(constraints=bytes_constraints())
440+
def draw_bytes(self, constraints):
441+
self._draw("bytes", constraints)
442+
443+
@precondition(lambda self: not self.frozen)
444+
@rule(constraints=string_constraints())
445+
def draw_string(self, constraints):
446+
self._draw("string", constraints)
447+
448+
@precondition(lambda self: not self.frozen)
449+
@rule(constraints=boolean_constraints())
450+
def draw_boolean(self, constraints):
451+
self._draw("boolean", constraints)
452+
453+
@precondition(lambda self: not self.frozen)
454+
@rule(label=st.integers())
455+
def span_start(self, label):
456+
self.provider.span_start(label)
457+
458+
@precondition(lambda self: not self.frozen)
459+
@rule(discard=st.booleans())
460+
def span_end(self, discard):
461+
self.provider.span_end(discard)
462+
463+
@precondition(lambda self: not self.frozen)
464+
@rule()
465+
def freeze(self):
466+
# phase-transition, mimicking data.freeze() at the end of a test case.
467+
self.frozen = True
485468
self.context_manager.__exit__(None, None, None)
486469

487-
ProviderConformanceTest.TestCase().runTest()
470+
@precondition(lambda self: self.frozen)
471+
@rule(value=_realize_objects)
472+
def realize(self, value):
473+
# filter out nans and weirder things
474+
try:
475+
assume(value == value)
476+
except Exception:
477+
# e.g. value = Decimal('-sNaN')
478+
assume(False)
479+
480+
# if `value` is non-symbolic, the provider should return it as-is.
481+
assert self.provider.realize(value) == value
482+
483+
@precondition(lambda self: self.frozen)
484+
@rule()
485+
def observe_test_case(self):
486+
observations = self.provider.observe_test_case()
487+
assert isinstance(observations, dict)
488+
489+
@precondition(lambda self: self.frozen)
490+
@rule(lifetime=st.sampled_from(["test_function", "test_case"]))
491+
def observe_information_messages(self, lifetime):
492+
observations = self.provider.observe_information_messages(
493+
lifetime=lifetime
494+
)
495+
for observation in observations:
496+
assert isinstance(observation, dict)
497+
498+
def teardown(self):
499+
if not self.frozen:
500+
self.context_manager.__exit__(None, None, None)
501+
502+
ProviderConformanceTest.TestCase().runTest()

0 commit comments

Comments
 (0)