Skip to content

Commit 5af0549

Browse files
committed
handle bool/float cases correctly in choice_key
1 parent 3356b67 commit 5af0549

File tree

4 files changed

+37
-6
lines changed

4 files changed

+37
-6
lines changed

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ class BooleanKWargs(TypedDict):
6767
IntegerKWargs, FloatKWargs, StringKWargs, BytesKWargs, BooleanKWargs
6868
]
6969
ChoiceNameT: "TypeAlias" = Literal["integer", "string", "boolean", "float", "bytes"]
70+
ChoiceKeyT: "TypeAlias" = Union[
71+
int, str, bytes, tuple[Literal["bool"], bool], tuple[Literal["float"], int]
72+
]
7073

7174

7275
def _size_to_index(size: int, *, alphabet_size: int) -> int:
@@ -456,13 +459,17 @@ def choice_permitted(choice: ChoiceT, kwargs: ChoiceKwargsT) -> bool:
456459
raise NotImplementedError(f"unhandled type {type(choice)} with value {choice}")
457460

458461

459-
def choices_key(choices: Sequence[ChoiceT]) -> tuple[ChoiceT, ...]:
462+
def choices_key(choices: Sequence[ChoiceT]) -> tuple[ChoiceKeyT, ...]:
460463
return tuple(choice_key(choice) for choice in choices)
461464

462465

463-
def choice_key(choice: ChoiceT) -> ChoiceT:
464-
if type(choice) is float:
465-
return float_to_int(choice)
466+
def choice_key(choice: ChoiceT) -> ChoiceKeyT:
467+
if isinstance(choice, float):
468+
# distinguish -0.0/0.0, signaling/nonsignaling nans, etc.
469+
return ("float", float_to_int(choice))
470+
if isinstance(choice, bool):
471+
# avoid choice_key(0) == choice_key(False)
472+
return ("bool", choice)
466473
return choice
467474

468475

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ def __stoppable_test_function(self, data: ConjectureData) -> None:
345345
# correct engine.
346346
raise
347347

348-
def _cache_key(self, choices: Sequence[ChoiceT]) -> tuple[ChoiceT, ...]:
348+
def _cache_key(self, choices: Sequence[ChoiceT]) -> tuple[ChoiceKeyT, ...]:
349349
return choices_key(choices)
350350

351351
def _cache(self, data: ConjectureData) -> None:

hypothesis-python/tests/conjecture/test_ir.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
choice_from_index,
3030
choice_permitted,
3131
choice_to_index,
32+
choices_key,
3233
)
3334
from hypothesis.internal.conjecture.data import (
3435
COLLECTION_DEFAULT_MAX_SIZE,
@@ -884,3 +885,26 @@ def test_draw_directly_explicit():
884885
)
885886
== 10
886887
)
888+
889+
890+
@pytest.mark.parametrize(
891+
"choices1, choices2",
892+
[
893+
[(True,), (1,)],
894+
[(False,), (0,)],
895+
[(0.0,), (-0.0,)],
896+
],
897+
)
898+
def test_choices_key_distinguishes_weird_cases(choices1, choices2):
899+
assert choices_key(choices1) != choices_key(choices2)
900+
901+
902+
@given(st.lists(ir_nodes()), st.lists(ir_nodes()))
903+
def test_choices_key_respects_inequality(nodes1, nodes2):
904+
choices1 = [n.value for n in nodes1]
905+
choices2 = [n.value for n in nodes2]
906+
if choices_key(choices1) != choices_key(choices2):
907+
assert set(choices1) != set(choices2)
908+
909+
# note that the other direction is not necessarily true: {False} == {0},
910+
# but choices_key([False]) != choices_key([0]).

hypothesis-python/tests/cover/test_database_backend.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ def test_background_write_database():
478478
@example(ir("a"))
479479
@example(ir(b"a"))
480480
@example(ir(b"a" * 50))
481-
def test_ir_nodes_rountrips(nodes1):
481+
def test_ir_nodes_roundtrips(nodes1):
482482
s1 = ir_to_bytes([n.value for n in nodes1])
483483
assert isinstance(s1, bytes)
484484
ir2 = ir_from_bytes(s1)

0 commit comments

Comments
 (0)