Skip to content

Commit b3146b0

Browse files
authored
Merge pull request #4509 from jobh/fix_shared_strategy_warnings
Fix shared strategy warnings
2 parents 4790666 + af304d9 commit b3146b0

File tree

13 files changed

+128
-57
lines changed

13 files changed

+128
-57
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
RELEASE_TYPE: patch
2+
3+
This patch re-enables the warning for incompatible :func:`~hypothesis.strategies.shared`
4+
strategies that was first enabled in :v:`6.133.0` but disabled in :v:`6.135.15`.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -706,7 +706,7 @@ def __init__(
706706
self._sampled_from_all_strategies_elements_message: Optional[
707707
tuple[str, object]
708708
] = None
709-
self._shared_strategy_draws: dict[Hashable, tuple[int, Any]] = {}
709+
self._shared_strategy_draws: dict[Hashable, tuple[Any, "SearchStrategy"]] = {}
710710
self._shared_data_strategy: Optional[DataObject] = None
711711
self._stateful_repr_parts: Optional[list[Any]] = None
712712
self.states_for_ids: Optional[dict[int, RandomState]] = None

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@
1616
from collections import OrderedDict, abc
1717
from collections.abc import Sequence
1818
from functools import lru_cache
19-
from typing import TYPE_CHECKING, Optional, TypeVar, Union
19+
from types import FunctionType
20+
from typing import TYPE_CHECKING, Callable, Optional, TypeVar, Union
2021

2122
from hypothesis.errors import InvalidArgument
2223
from hypothesis.internal.compat import int_from_bytes
2324
from hypothesis.internal.floats import next_up
25+
from hypothesis.internal.lambda_sources import _function_key
2426

2527
if TYPE_CHECKING:
2628
from hypothesis.internal.conjecture.data import ConjectureData
@@ -34,6 +36,20 @@ def calc_label_from_name(name: str) -> int:
3436
return int_from_bytes(hashed[:8])
3537

3638

39+
def calc_label_from_callable(f: Callable) -> int:
40+
if isinstance(f, FunctionType):
41+
return calc_label_from_hash(_function_key(f, ignore_name=True))
42+
elif isinstance(f, type):
43+
return calc_label_from_cls(f)
44+
else:
45+
# probably an instance defining __call__
46+
try:
47+
return calc_label_from_hash(f)
48+
except Exception:
49+
# not hashable
50+
return calc_label_from_cls(type(f))
51+
52+
3753
def calc_label_from_cls(cls: type) -> int:
3854
return calc_label_from_name(cls.__qualname__)
3955

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def visit_Attribute(self, node):
6565
return attributes
6666

6767

68-
def _function_key(f, *, bounded_size=False):
68+
def _function_key(f, *, bounded_size=False, ignore_name=False):
6969
"""Returns a digest that differentiates functions that have different sources.
7070
7171
Either a function or a code object may be passed. If code object, default
@@ -96,7 +96,7 @@ def _function_key(f, *, bounded_size=False):
9696
code.co_names,
9797
code.co_varnames,
9898
code.co_freevars,
99-
code.co_name,
99+
ignore_name or code.co_name,
100100
)
101101

102102

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,10 @@
8282
)
8383
from hypothesis.internal.conjecture.data import ConjectureData
8484
from hypothesis.internal.conjecture.utils import (
85-
calc_label_from_cls,
85+
calc_label_from_callable,
86+
calc_label_from_name,
8687
check_sample,
88+
combine_labels,
8789
identity,
8890
)
8991
from hypothesis.internal.entropy import get_seeder_and_restorer
@@ -1053,6 +1055,15 @@ def __init__(
10531055
self.args = args
10541056
self.kwargs = kwargs
10551057

1058+
def calc_label(self) -> int:
1059+
return combine_labels(
1060+
self.class_label,
1061+
calc_label_from_callable(self.target),
1062+
*[strat.label for strat in self.args],
1063+
*[calc_label_from_name(k) for k in self.kwargs],
1064+
*[strat.label for strat in self.kwargs.values()],
1065+
)
1066+
10561067
def do_draw(self, data: ConjectureData) -> Ex:
10571068
args = [data.draw(s) for s in self.args]
10581069
kwargs = {k: data.draw(v) for k, v in self.kwargs.items()}
@@ -1875,7 +1886,10 @@ def do_draw(self, data):
18751886
return self.definition(data.draw, *self.args, **self.kwargs)
18761887

18771888
def calc_label(self) -> int:
1878-
return calc_label_from_cls(self.definition)
1889+
return combine_labels(
1890+
self.class_label,
1891+
calc_label_from_callable(self.definition),
1892+
)
18791893

18801894

18811895
class DrawFn(Protocol):

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
from typing import Callable, Generic, TypeVar
1212

1313
from hypothesis.internal.conjecture.data import ConjectureData
14+
from hypothesis.internal.conjecture.utils import (
15+
calc_label_from_callable,
16+
combine_labels,
17+
)
1418
from hypothesis.internal.reflection import get_pretty_function_description
1519
from hypothesis.strategies._internal.strategies import (
1620
RecurT,
@@ -35,6 +39,13 @@ def __init__(
3539
def calc_is_empty(self, recur: RecurT) -> bool:
3640
return recur(self.base)
3741

42+
def calc_label(self) -> int:
43+
return combine_labels(
44+
self.class_label,
45+
self.base.label,
46+
calc_label_from_callable(self.expand),
47+
)
48+
3849
def __repr__(self) -> str:
3950
if not hasattr(self, "_cached_repr"):
4051
self._cached_repr = (

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

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
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
1314

15+
from hypothesis.errors import HypothesisWarning
1416
from hypothesis.internal.conjecture.data import ConjectureData
1517
from hypothesis.strategies._internal import SearchStrategy
1618
from hypothesis.strategies._internal.strategies import Ex
@@ -28,30 +30,27 @@ def __repr__(self) -> str:
2830
else:
2931
return f"shared({self.base!r})"
3032

33+
def calc_label(self) -> int:
34+
return self.base.calc_label()
35+
3136
# Ideally would be -> Ex, but key collisions with different-typed values are
3237
# possible. See https://github.com/HypothesisWorks/hypothesis/issues/4301.
3338
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
4139
key = self.key or self
4240
if key not in data._shared_strategy_draws:
4341
drawn = data.draw(self.base)
44-
data._shared_strategy_draws[key] = (strat_label, drawn)
42+
data._shared_strategy_draws[key] = (drawn, self)
4543
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-
# )
44+
drawn, other = data._shared_strategy_draws[key]
45+
46+
# Check that the strategies shared under this key are equivalent
47+
if self.label != other.label:
48+
warnings.warn(
49+
f"Different strategies are shared under {key=}. This"
50+
" risks drawing values that are not valid examples for the strategy,"
51+
" or that have a narrower range than expected."
52+
f" Conflicting strategies: ({self!r}, {other!r}).",
53+
HypothesisWarning,
54+
stacklevel=1,
55+
)
5756
return drawn

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -702,7 +702,7 @@ def do_draw(self, data: ConjectureData) -> Ex:
702702
isinstance(x, SearchStrategy) for x in self.elements
703703
):
704704
data._sampled_from_all_strategies_elements_message = (
705-
"sample_from was given a collection of strategies: "
705+
"sampled_from was given a collection of strategies: "
706706
"{!r}. Was one_of intended?",
707707
self.elements,
708708
)

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/conjecture/test_utils.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,3 +208,20 @@ def test_samples_from_a_range_directly():
208208

209209
def test_p_continue_to_average_saturates():
210210
assert cu._p_continue_to_avg(1.1, 100) == 100
211+
212+
213+
def test_unhashable_calc_label():
214+
215+
class Unhashable:
216+
def __call__(self):
217+
return None
218+
219+
def __hash__(self):
220+
raise TypeError
221+
222+
c1 = Unhashable()
223+
c2 = Unhashable()
224+
225+
with pytest.raises(TypeError):
226+
assert cu.calc_label_from_hash(c1) == cu.calc_label_from_hash(c2)
227+
assert cu.calc_label_from_callable(c1) == cu.calc_label_from_callable(c2)

0 commit comments

Comments
 (0)