1# This file is part of Hypothesis, which may be found at
2# https://github.com/HypothesisWorks/hypothesis/
3#
4# Copyright the Hypothesis Authors.
5# Individual contributors are listed in AUTHORS.rst and the git log.
6#
7# This Source Code Form is subject to the terms of the Mozilla Public License,
8# v. 2.0. If a copy of the MPL was not distributed with this file, You can
9# obtain one at https://mozilla.org/MPL/2.0/.
10
11import copy
12import math
13from collections.abc import Iterable
14from typing import Any, Callable, Optional, Union, overload
15
16from hypothesis import strategies as st
17from hypothesis.errors import InvalidArgument
18from hypothesis.internal.conjecture import utils as cu
19from hypothesis.internal.conjecture.data import ConjectureData
20from hypothesis.internal.conjecture.engine import BUFFER_SIZE
21from hypothesis.internal.conjecture.junkdrawer import LazySequenceCopy
22from hypothesis.internal.conjecture.utils import combine_labels
23from hypothesis.internal.filtering import get_integer_predicate_bounds
24from hypothesis.internal.reflection import is_identity_function
25from hypothesis.strategies._internal.strategies import (
26 T3,
27 T4,
28 T5,
29 Ex,
30 FilteredStrategy,
31 RecurT,
32 SampledFromStrategy,
33 SearchStrategy,
34 T,
35 check_strategy,
36 filter_not_satisfied,
37)
38from hypothesis.strategies._internal.utils import cacheable, defines_strategy
39from hypothesis.utils.conventions import UniqueIdentifier
40
41
42class TupleStrategy(SearchStrategy[tuple[Ex, ...]]):
43 """A strategy responsible for fixed length tuples based on heterogeneous
44 strategies for each of their elements."""
45
46 def __init__(self, strategies: Iterable[SearchStrategy[Any]]):
47 super().__init__()
48 self.element_strategies = tuple(strategies)
49
50 def do_validate(self) -> None:
51 for s in self.element_strategies:
52 s.validate()
53
54 def calc_label(self) -> int:
55 return combine_labels(
56 self.class_label, *(s.label for s in self.element_strategies)
57 )
58
59 def __repr__(self) -> str:
60 tuple_string = ", ".join(map(repr, self.element_strategies))
61 return f"TupleStrategy(({tuple_string}))"
62
63 def calc_has_reusable_values(self, recur: RecurT) -> bool:
64 return all(recur(e) for e in self.element_strategies)
65
66 def do_draw(self, data: ConjectureData) -> tuple[Ex, ...]:
67 return tuple(data.draw(e) for e in self.element_strategies)
68
69 def calc_is_empty(self, recur: RecurT) -> bool:
70 return any(recur(e) for e in self.element_strategies)
71
72
73@overload
74def tuples() -> SearchStrategy[tuple[()]]: # pragma: no cover
75 ...
76
77
78@overload
79def tuples(__a1: SearchStrategy[Ex]) -> SearchStrategy[tuple[Ex]]: # pragma: no cover
80 ...
81
82
83@overload
84def tuples(
85 __a1: SearchStrategy[Ex], __a2: SearchStrategy[T]
86) -> SearchStrategy[tuple[Ex, T]]: # pragma: no cover
87 ...
88
89
90@overload
91def tuples(
92 __a1: SearchStrategy[Ex], __a2: SearchStrategy[T], __a3: SearchStrategy[T3]
93) -> SearchStrategy[tuple[Ex, T, T3]]: # pragma: no cover
94 ...
95
96
97@overload
98def tuples(
99 __a1: SearchStrategy[Ex],
100 __a2: SearchStrategy[T],
101 __a3: SearchStrategy[T3],
102 __a4: SearchStrategy[T4],
103) -> SearchStrategy[tuple[Ex, T, T3, T4]]: # pragma: no cover
104 ...
105
106
107@overload
108def tuples(
109 __a1: SearchStrategy[Ex],
110 __a2: SearchStrategy[T],
111 __a3: SearchStrategy[T3],
112 __a4: SearchStrategy[T4],
113 __a5: SearchStrategy[T5],
114) -> SearchStrategy[tuple[Ex, T, T3, T4, T5]]: # pragma: no cover
115 ...
116
117
118@overload
119def tuples(
120 *args: SearchStrategy[Any],
121) -> SearchStrategy[tuple[Any, ...]]: # pragma: no cover
122 ...
123
124
125@cacheable
126@defines_strategy()
127def tuples(*args: SearchStrategy[Any]) -> SearchStrategy[tuple[Any, ...]]:
128 """Return a strategy which generates a tuple of the same length as args by
129 generating the value at index i from args[i].
130
131 e.g. tuples(integers(), integers()) would generate a tuple of length
132 two with both values an integer.
133
134 Examples from this strategy shrink by shrinking their component parts.
135 """
136 for arg in args:
137 check_strategy(arg)
138
139 return TupleStrategy(args)
140
141
142class ListStrategy(SearchStrategy[list[Ex]]):
143 """A strategy for lists which takes a strategy for its elements and the
144 allowed lengths, and generates lists with the correct size and contents."""
145
146 _nonempty_filters: tuple[Callable[[Any], Any], ...] = (bool, len, tuple, list)
147
148 def __init__(
149 self,
150 elements: SearchStrategy[Ex],
151 min_size: int = 0,
152 max_size: Optional[Union[float, int]] = math.inf,
153 ):
154 super().__init__()
155 self.min_size = min_size or 0
156 self.max_size = max_size if max_size is not None else math.inf
157 assert 0 <= self.min_size <= self.max_size
158 self.average_size = min(
159 max(self.min_size * 2, self.min_size + 5),
160 0.5 * (self.min_size + self.max_size),
161 )
162 self.element_strategy = elements
163 if min_size > BUFFER_SIZE:
164 raise InvalidArgument(
165 f"{self!r} can never generate an example, because min_size is larger "
166 "than Hypothesis supports. Including it is at best slowing down your "
167 "tests for no benefit; at worst making them fail (maybe flakily) with "
168 "a HealthCheck error."
169 )
170
171 def calc_label(self) -> int:
172 return combine_labels(self.class_label, self.element_strategy.label)
173
174 def do_validate(self) -> None:
175 self.element_strategy.validate()
176 if self.is_empty:
177 raise InvalidArgument(
178 "Cannot create non-empty lists with elements drawn from "
179 f"strategy {self.element_strategy!r} because it has no values."
180 )
181 if self.element_strategy.is_empty and 0 < self.max_size < float("inf"):
182 raise InvalidArgument(
183 f"Cannot create a collection of max_size={self.max_size!r}, "
184 "because no elements can be drawn from the element strategy "
185 f"{self.element_strategy!r}"
186 )
187
188 def calc_is_empty(self, recur: RecurT) -> bool:
189 if self.min_size == 0:
190 return False
191 return recur(self.element_strategy)
192
193 def do_draw(self, data: ConjectureData) -> list[Ex]:
194 if self.element_strategy.is_empty:
195 assert self.min_size == 0
196 return []
197
198 elements = cu.many(
199 data,
200 min_size=self.min_size,
201 max_size=self.max_size,
202 average_size=self.average_size,
203 )
204 result = []
205 while elements.more():
206 result.append(data.draw(self.element_strategy))
207 return result
208
209 def __repr__(self) -> str:
210 return (
211 f"{self.__class__.__name__}({self.element_strategy!r}, "
212 f"min_size={self.min_size:_}, max_size={self.max_size:_})"
213 )
214
215 def filter(self, condition: Callable[[list[Ex]], Any]) -> SearchStrategy[list[Ex]]:
216 if condition in self._nonempty_filters or is_identity_function(condition):
217 assert self.max_size >= 1, "Always-empty is special cased in st.lists()"
218 if self.min_size >= 1:
219 return self
220 new = copy.copy(self)
221 new.min_size = 1
222 return new
223
224 constraints, pred = get_integer_predicate_bounds(condition)
225 if constraints.get("len") and (
226 "min_value" in constraints or "max_value" in constraints
227 ):
228 new = copy.copy(self)
229 new.min_size = max(
230 self.min_size, constraints.get("min_value", self.min_size)
231 )
232 new.max_size = min(
233 self.max_size, constraints.get("max_value", self.max_size)
234 )
235 # Unsatisfiable filters are easiest to understand without rewriting.
236 if new.min_size > new.max_size:
237 return SearchStrategy.filter(self, condition)
238 # Recompute average size; this is cheaper than making it into a property.
239 new.average_size = min(
240 max(new.min_size * 2, new.min_size + 5),
241 0.5 * (new.min_size + new.max_size),
242 )
243 if pred is None:
244 return new
245 return SearchStrategy.filter(new, condition)
246
247 return SearchStrategy.filter(self, condition)
248
249
250class UniqueListStrategy(ListStrategy[Ex]):
251 def __init__(
252 self,
253 elements: SearchStrategy[Ex],
254 min_size: int,
255 max_size: Optional[Union[float, int]],
256 # TODO: keys are guaranteed to be Hashable, not just Any, but this makes
257 # other things harder to type
258 keys: tuple[Callable[[Ex], Any], ...],
259 tuple_suffixes: Optional[SearchStrategy[tuple[Ex, ...]]],
260 ):
261 super().__init__(elements, min_size, max_size)
262 self.keys = keys
263 self.tuple_suffixes = tuple_suffixes
264
265 def do_draw(self, data: ConjectureData) -> list[Ex]:
266 if self.element_strategy.is_empty:
267 assert self.min_size == 0
268 return []
269
270 elements = cu.many(
271 data,
272 min_size=self.min_size,
273 max_size=self.max_size,
274 average_size=self.average_size,
275 )
276 seen_sets: tuple[set[Ex], ...] = tuple(set() for _ in self.keys)
277 # actually list[Ex], but if self.tuple_suffixes is present then Ex is a
278 # tuple[T, ...] because self.element_strategy is a TuplesStrategy, and
279 # appending a concrete tuple to `result: list[Ex]` makes mypy unhappy
280 # without knowing that Ex = tuple.
281 result: list[Any] = []
282
283 # We construct a filtered strategy here rather than using a check-and-reject
284 # approach because some strategies have special logic for generation under a
285 # filter, and FilteredStrategy can consolidate multiple filters.
286 def not_yet_in_unique_list(val: Ex) -> bool: # type: ignore # covariant type param
287 return all(key(val) not in seen for key, seen in zip(self.keys, seen_sets))
288
289 filtered = FilteredStrategy(
290 self.element_strategy, conditions=(not_yet_in_unique_list,)
291 )
292 while elements.more():
293 value = filtered.do_filtered_draw(data)
294 if value is filter_not_satisfied:
295 elements.reject(f"Aborted test because unable to satisfy {filtered!r}")
296 else:
297 assert not isinstance(value, UniqueIdentifier)
298 for key, seen in zip(self.keys, seen_sets):
299 seen.add(key(value))
300 if self.tuple_suffixes is not None:
301 value = (value, *data.draw(self.tuple_suffixes)) # type: ignore
302 result.append(value)
303 assert self.max_size >= len(result) >= self.min_size
304 return result
305
306
307class UniqueSampledListStrategy(UniqueListStrategy):
308 def do_draw(self, data: ConjectureData) -> list[Ex]:
309 assert isinstance(self.element_strategy, SampledFromStrategy)
310
311 should_draw = cu.many(
312 data,
313 min_size=self.min_size,
314 max_size=self.max_size,
315 average_size=self.average_size,
316 )
317 seen_sets: tuple[set[Ex], ...] = tuple(set() for _ in self.keys)
318 result: list[Any] = []
319
320 remaining = LazySequenceCopy(self.element_strategy.elements)
321
322 while remaining and should_draw.more():
323 j = data.draw_integer(0, len(remaining) - 1)
324 value = self.element_strategy._transform(remaining.pop(j))
325 if value is not filter_not_satisfied and all(
326 key(value) not in seen for key, seen in zip(self.keys, seen_sets)
327 ):
328 for key, seen in zip(self.keys, seen_sets):
329 seen.add(key(value))
330 if self.tuple_suffixes is not None:
331 value = (value, *data.draw(self.tuple_suffixes))
332 result.append(value)
333 else:
334 should_draw.reject(
335 "UniqueSampledListStrategy filter not satisfied or value already seen"
336 )
337 assert self.max_size >= len(result) >= self.min_size
338 return result
339
340
341class FixedDictStrategy(SearchStrategy[dict[Any, Any]]):
342 """A strategy which produces dicts with a fixed set of keys, given a
343 strategy for each of their equivalent values.
344
345 e.g. {'foo' : some_int_strategy} would generate dicts with the single
346 key 'foo' mapping to some integer.
347 """
348
349 def __init__(
350 self,
351 mapping: dict[Any, SearchStrategy[Any]],
352 *,
353 optional: Optional[dict[Any, SearchStrategy[Any]]],
354 ):
355 super().__init__()
356 dict_type = type(mapping)
357 self.mapping = mapping
358 keys = tuple(mapping.keys())
359 self.fixed = st.tuples(*[mapping[k] for k in keys]).map(
360 lambda value: dict_type(zip(keys, value))
361 )
362 self.optional = optional
363
364 def do_draw(self, data: ConjectureData) -> dict[Any, Any]:
365 value = data.draw(self.fixed)
366 if self.optional is None:
367 return value
368
369 remaining = [k for k, v in self.optional.items() if not v.is_empty]
370 should_draw = cu.many(
371 data, min_size=0, max_size=len(remaining), average_size=len(remaining) / 2
372 )
373 while should_draw.more():
374 j = data.draw_integer(0, len(remaining) - 1)
375 remaining[-1], remaining[j] = remaining[j], remaining[-1]
376 key = remaining.pop()
377 value[key] = data.draw(self.optional[key])
378 return value
379
380 def calc_is_empty(self, recur: RecurT) -> bool:
381 return recur(self.fixed)
382
383 def __repr__(self) -> str:
384 if self.optional is not None:
385 return f"fixed_dictionaries({self.mapping!r}, optional={self.optional!r})"
386 return f"fixed_dictionaries({self.mapping!r})"