Skip to content

Commit 7706606

Browse files
committed
narrow the severity and cases for alternate API use suggestion; close #3819
Revised per suggestion here: #3820 (comment)
1 parent 4eb0b50 commit 7706606

File tree

6 files changed

+128
-17
lines changed

6 files changed

+128
-17
lines changed

hypothesis-python/RELEASE.rst

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
RELEASE_TYPE: patch
22

3-
This patch causes a warning to be issued when :func:`~hypothesis.strategies.sampled_from` is given a nonempty collection of all strategy values as the argument to its `strategies` parameter (:issue:`3819``).
3+
This patch causes a [note to be included](https://peps.python.org/pep-0678/) when :func:`~hypothesis.strategies.sampled_from` is given a nonempty collection of all strategy values _and_ the `given`-decorated test fails with a `TypeError` (:issue:`3819``).
44
This is because such a call is suggestive of intent to instead use :func:`~hypothesis.strategies.one_of`.
5-
This closes

hypothesis-python/src/hypothesis/core.py

+14
Original file line numberDiff line numberDiff line change
@@ -1585,6 +1585,20 @@ def wrapped_test(*arguments, **kwargs):
15851585
if isinstance(e, BaseExceptionGroup)
15861586
else get_trimmed_traceback()
15871587
)
1588+
if (
1589+
isinstance(e, TypeError)
1590+
and "SearchStrategy" in str(e)
1591+
and any(
1592+
getattr(
1593+
s, "sampling_is_from_a_collection_of_strategies", False
1594+
)
1595+
for s in given_kwargs.values()
1596+
)
1597+
):
1598+
add_note(
1599+
e,
1600+
"sample_from was given a collection of strategies; was one_of intended?",
1601+
)
15881602
raise the_error_hypothesis_found
15891603

15901604
if not (ran_explicit_examples or state.ever_executed):

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

-5
Original file line numberDiff line numberDiff line change
@@ -214,11 +214,6 @@ def sampled_from(
214214
"so maybe you tried to write an enum as if it was a dataclass?"
215215
)
216216
raise InvalidArgument("Cannot sample from a length-zero sequence.")
217-
elif all(isinstance(x, SearchStrategy) for x in values):
218-
warnings.warn(
219-
"sample_from was given a collection of strategies; was one_of intended?",
220-
stacklevel=1,
221-
)
222217
if len(values) == 1:
223218
return just(values[0])
224219
try:

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

+3
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,9 @@ def __init__(self, elements, repr_=None, transformations=()):
477477
super().__init__()
478478
self.elements = cu.check_sample(elements, "sampled_from")
479479
assert self.elements
480+
self.sampling_is_from_a_collection_of_strategies = all(
481+
isinstance(x, SearchStrategy) for x in self.elements
482+
)
480483
self.repr_ = repr_
481484
self._transformations = transformations
482485

hypothesis-python/tests/cover/test_lookup.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,8 @@ class Baz(Foo):
311311

312312

313313
@pytest.mark.parametrize(
314-
"var,expected,exp_warn",
314+
"var,expected",
315315
[
316-
# Expect a warning exactly when the type constraint/bound should trigger
317-
# passing a strategy to sampled_from.
318316
(typing.TypeVar("V"), object),
319317
(typing.TypeVar("V", bound=int), int),
320318
(typing.TypeVar("V", bound=Foo), (Bar, Baz)),

hypothesis-python/tests/cover/test_sampled_from.py

+109-7
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,113 @@ def test_suggests_elements_instead_of_annotations():
192192
st.sampled_from(AnnotationsInsteadOfElements).example()
193193

194194

195-
@pytest.mark.parametrize("wrap", [list, tuple])
196-
def test_warns_when_given_entirely_strategies_as_elements(wrap):
197-
elements = wrap([st.booleans(), st.decimals(), st.integers(), st.text()])
198-
with pytest.warns(
199-
UserWarning,
200-
match="sample_from was given a collection of strategies; was one_of intended?",
195+
class TestErrorNoteBehavior3819:
196+
elements = (st.booleans(), st.decimals(), st.integers(), st.text())
197+
198+
def execute_expecting_error(test_func):
199+
with pytest.raises(TypeError) as err_ctx:
200+
test_func()
201+
obs_notes = getattr(err_ctx.value, "__notes__", [])
202+
assert obs_notes[-1] == exp_msg
203+
204+
@staticmethod
205+
@given(st.sampled_from(elements))
206+
def args_with_type_error_with_message_substring(_):
207+
raise TypeError("SearchStrategy")
208+
209+
@staticmethod
210+
@given(all_strat_sample=st.sampled_from(elements))
211+
def kwargs_with_type_error_with_message_substring(all_strat_sample):
212+
raise TypeError("SearchStrategy")
213+
214+
@staticmethod
215+
@given(st.sampled_from(elements))
216+
def args_with_type_error_without_message_substring(_):
217+
raise TypeError("Substring not in message!")
218+
219+
@staticmethod
220+
@given(st.sampled_from(elements))
221+
def kwargs_with_type_error_without_message_substring(_):
222+
raise TypeError("Substring not in message!")
223+
224+
@staticmethod
225+
@given(st.sampled_from(elements))
226+
def type_error_but_not_all_strategies_args(_):
227+
raise TypeError("SearchStrategy, but should not trigger note addition!")
228+
229+
@staticmethod
230+
@given(all_strat_sample=st.sampled_from(elements))
231+
def type_error_but_not_all_strategies_kwargs(all_strat_sample):
232+
raise TypeError("SearchStrategy, but should not trigger note addition!")
233+
234+
@staticmethod
235+
@given(st.sampled_from(elements))
236+
def non_type_error_args(_):
237+
raise Exception("SearchStrategy, but should not trigger note addition!")
238+
239+
@staticmethod
240+
@given(all_strat_sample=st.sampled_from(elements))
241+
def non_type_error_kwargs(all_strat_sample):
242+
raise Exception("SearchStrategy, but should not trigger note addition!")
243+
244+
@staticmethod
245+
@given(st.sampled_from(elements))
246+
def args_without_error(_):
247+
return
248+
249+
@staticmethod
250+
@given(all_strat_sample=st.sampled_from(elements))
251+
def kwargs_without_error(all_strat_sample):
252+
return
253+
254+
@pytest.mark.parametrize(
255+
["func_to_call", "exp_err_cls", "should_exp_msg"],
256+
[
257+
(f, TypeError, True)
258+
for f in (
259+
args_with_type_error_with_message_substring,
260+
kwargs_with_type_error_with_message_substring,
261+
)
262+
]
263+
+ [
264+
(f, TypeError, False)
265+
for f in (
266+
args_with_type_error_without_message_substring,
267+
kwargs_with_type_error_without_message_substring,
268+
)
269+
]
270+
+ [
271+
(f, TypeError, False)
272+
for f in (
273+
type_error_but_not_all_strategies_args,
274+
type_error_but_not_all_strategies_kwargs,
275+
)
276+
]
277+
+ [(f, Exception, False) for f in (non_type_error_args, non_type_error_kwargs)]
278+
+ [(f, None, False) for f in (args_without_error, kwargs_without_error)],
279+
)
280+
def test_error_appropriate_error_note_3819(
281+
self, func_to_call, exp_err_cls, should_exp_msg
201282
):
202-
st.sampled_from(elements)
283+
if exp_err_cls is None:
284+
try:
285+
func_to_call()
286+
except BaseException as e:
287+
pytest.fail(f"Expected call to succeed but got error: {e}")
288+
else:
289+
assert True
290+
else:
291+
with pytest.raises(Exception) as err_ctx:
292+
func_to_call()
293+
err = err_ctx.value
294+
assert type(err) is exp_err_cls
295+
notes = getattr(err, "__notes__", [])
296+
msg_in_notes = (
297+
"sample_from was given a collection of strategies; was one_of intended?"
298+
in notes
299+
)
300+
assert (
301+
should_exp_msg
302+
and msg_in_notes
303+
or (not should_exp_msg and not should_exp_msg)
304+
)

0 commit comments

Comments
 (0)