Skip to content

Commit f6b443b

Browse files
committed
Check shared-strategy compatibility without reference to cache
1 parent 4790666 commit f6b443b

File tree

3 files changed

+44
-26
lines changed

3 files changed

+44
-26
lines changed

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

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
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+
import warnings
1112
from collections.abc import Hashable
1213
from typing import Any, Optional
14+
from weakref import WeakKeyDictionary
1315

16+
from hypothesis.errors import HypothesisWarning
1417
from hypothesis.internal.conjecture.data import ConjectureData
1518
from hypothesis.strategies._internal import SearchStrategy
1619
from hypothesis.strategies._internal.strategies import Ex
@@ -21,6 +24,9 @@ def __init__(self, base: SearchStrategy[Ex], key: Optional[Hashable] = None):
2124
super().__init__()
2225
self.key = key
2326
self.base = base
27+
while isinstance(self.base, SharedStrategy):
28+
# Unwrap nested shares
29+
self.base = self.base.base
2430

2531
def __repr__(self) -> str:
2632
if self.key is not None:
@@ -31,27 +37,30 @@ def __repr__(self) -> str:
3137
# Ideally would be -> Ex, but key collisions with different-typed values are
3238
# possible. See https://github.com/HypothesisWorks/hypothesis/issues/4301.
3339
def do_draw(self, data: ConjectureData) -> Any:
34-
if self.key is None or getattr(self.base, "_is_singleton", False):
35-
strat_label = id(self.base)
36-
else:
37-
# Assume that uncached strategies are distinguishable by their
38-
# label. False negatives (even collisions w/id above) are ok as
39-
# long as they are infrequent.
40-
strat_label = self.base.label
4140
key = self.key or self
4241
if key not in data._shared_strategy_draws:
4342
drawn = data.draw(self.base)
44-
data._shared_strategy_draws[key] = (strat_label, drawn)
43+
data._shared_strategy_draws[key] = (drawn, self)
4544
else:
46-
drawn_strat_label, drawn = data._shared_strategy_draws[key]
47-
# Check disabled pending resolution of #4301
48-
if drawn_strat_label != strat_label: # pragma: no cover
49-
pass
50-
# warnings.warn(
51-
# f"Different strategies are shared under {key=}. This"
52-
# " risks drawing values that are not valid examples for the strategy,"
53-
# " or that have a narrower range than expected.",
54-
# HypothesisWarning,
55-
# stacklevel=1,
56-
# )
45+
drawn, other = data._shared_strategy_draws[key]
46+
47+
if other.base is not self.base:
48+
# Check that the strategies shared under this key are equivalent,
49+
# approximated as having equal `repr`s.
50+
try:
51+
is_equivalent = self._equivalent_to[other]
52+
except (AttributeError, KeyError) as e:
53+
if isinstance(e, AttributeError):
54+
self._equivalent_to = WeakKeyDictionary()
55+
is_equivalent = repr(self.base) == repr(other.base)
56+
self._equivalent_to[other] = is_equivalent
57+
if not is_equivalent:
58+
warnings.warn(
59+
f"Different strategies are shared under {key=}. This"
60+
" risks drawing values that are not valid examples for the strategy,"
61+
" or that have a narrower range than expected."
62+
f" Conflicting strategies: ({self.base!r}, {other.base!r}).",
63+
HypothesisWarning,
64+
stacklevel=1,
65+
)
5766
return drawn

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ def cached_strategy(*args, **kwargs):
6969
else:
7070
result = fn(*args, **kwargs)
7171
if not isinstance(result, SearchStrategy) or result.is_cacheable:
72-
result._is_singleton = True
7372
_STRATEGY_CACHE[cache_key] = result
7473
return result
7574

hypothesis-python/tests/cover/test_direct_strategies.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,6 @@ def test_builds_error_messages(data):
586586
data.draw(st.sampled_from(AnEnum))
587587

588588

589-
@pytest.mark.skip("pending resolution of #4301")
590589
@pytest.mark.parametrize(
591590
"strat_a,strat_b",
592591
[
@@ -606,7 +605,6 @@ def test_builds_error_messages(data):
606605
],
607606
)
608607
def test_incompatible_shared_strategies_warns(strat_a, strat_b):
609-
610608
shared_a = st.shared(strat_a, key="share")
611609
shared_b = st.shared(strat_b, key="share")
612610

@@ -629,16 +627,15 @@ def _composite2(draw):
629627
return draw(st.integers())
630628

631629

632-
@pytest.mark.skip("pending resolution of #4301")
633630
@pytest.mark.parametrize(
634631
"strat_a,strat_b",
635632
[
636633
(st.floats(allow_nan=False), st.floats(allow_nan=False)),
637634
(st.builds(float), st.builds(float)),
638635
(_composite1(), _composite1()),
639636
pytest.param(
640-
st.floats(allow_nan=False),
641-
st.floats(allow_nan=0),
637+
st.floats(allow_nan=False, allow_infinity=False),
638+
st.floats(allow_nan=False, allow_infinity=0),
642639
marks=pytest.mark.xfail(
643640
raises=HypothesisWarning,
644641
strict=True,
@@ -665,7 +662,7 @@ def _composite2(draw):
665662
),
666663
],
667664
)
668-
def test_compatible_shared_strategies_do_not_raise(strat_a, strat_b):
665+
def test_compatible_shared_strategies_do_not_warn(strat_a, strat_b):
669666
shared_a = st.shared(strat_a, key="share")
670667
shared_b = st.shared(strat_b, key="share")
671668

@@ -677,3 +674,16 @@ def test_it(a, b):
677674
with warnings.catch_warnings():
678675
warnings.simplefilter("error", HypothesisWarning)
679676
test_it()
677+
678+
679+
def test_compatible_nested_shared_strategies_do_not_warn():
680+
shared_a = st.shared(st.integers(), key="share")
681+
shared_b = st.shared(st.integers(), key="share")
682+
shared_c = st.shared(shared_a, key="share")
683+
684+
@given(shared_a, shared_b, shared_c)
685+
@settings(max_examples=10, phases=[Phase.generate])
686+
def test_it(a, b, c):
687+
assert a == b == c
688+
689+
test_it()

0 commit comments

Comments
 (0)