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 RecurT,
31 SampledFromStrategy,
32 SearchStrategy,
33 T,
34 check_strategy,
35 filter_not_satisfied,
36)
37from hypothesis.strategies._internal.utils import cacheable, defines_strategy
38from hypothesis.utils.conventions import UniqueIdentifier
39
40
41class TupleStrategy(SearchStrategy[tuple[Ex, ...]]):
42 """A strategy responsible for fixed length tuples based on heterogeneous
43 strategies for each of their elements."""
44
45 def __init__(self, strategies: Iterable[SearchStrategy[Any]]):
46 super().__init__()
47 self.element_strategies = tuple(strategies)
48
49 def do_validate(self) -> None:
50 for s in self.element_strategies:
51 s.validate()
52
53 def calc_label(self) -> int:
54 return combine_labels(
55 self.class_label, *(s.label for s in self.element_strategies)
56 )
57
58 def __repr__(self) -> str:
59 tuple_string = ", ".join(map(repr, self.element_strategies))
60 return f"TupleStrategy(({tuple_string}))"
61
62 def calc_has_reusable_values(self, recur: RecurT) -> bool:
63 return all(recur(e) for e in self.element_strategies)
64
65 def do_draw(self, data: ConjectureData) -> tuple[Ex, ...]:
66 return tuple(data.draw(e) for e in self.element_strategies)
67
68 def calc_is_empty(self, recur: RecurT) -> bool:
69 return any(recur(e) for e in self.element_strategies)
70
71
72@overload
73def tuples() -> SearchStrategy[tuple[()]]: # pragma: no cover
74 ...
75
76
77@overload
78def tuples(__a1: SearchStrategy[Ex]) -> SearchStrategy[tuple[Ex]]: # pragma: no cover
79 ...
80
81
82@overload
83def tuples(
84 __a1: SearchStrategy[Ex], __a2: SearchStrategy[T]
85) -> SearchStrategy[tuple[Ex, T]]: # pragma: no cover
86 ...
87
88
89@overload
90def tuples(
91 __a1: SearchStrategy[Ex], __a2: SearchStrategy[T], __a3: SearchStrategy[T3]
92) -> SearchStrategy[tuple[Ex, T, T3]]: # pragma: no cover
93 ...
94
95
96@overload
97def tuples(
98 __a1: SearchStrategy[Ex],
99 __a2: SearchStrategy[T],
100 __a3: SearchStrategy[T3],
101 __a4: SearchStrategy[T4],
102) -> SearchStrategy[tuple[Ex, T, T3, T4]]: # pragma: no cover
103 ...
104
105
106@overload
107def tuples(
108 __a1: SearchStrategy[Ex],
109 __a2: SearchStrategy[T],
110 __a3: SearchStrategy[T3],
111 __a4: SearchStrategy[T4],
112 __a5: SearchStrategy[T5],
113) -> SearchStrategy[tuple[Ex, T, T3, T4, T5]]: # pragma: no cover
114 ...
115
116
117@overload
118def tuples(
119 *args: SearchStrategy[Any],
120) -> SearchStrategy[tuple[Any, ...]]: # pragma: no cover
121 ...
122
123
124@cacheable
125@defines_strategy()
126def tuples(*args: SearchStrategy[Any]) -> SearchStrategy[tuple[Any, ...]]:
127 """Return a strategy which generates a tuple of the same length as args by
128 generating the value at index i from args[i].
129
130 e.g. tuples(integers(), integers()) would generate a tuple of length
131 two with both values an integer.
132
133 Examples from this strategy shrink by shrinking their component parts.
134 """
135 for arg in args:
136 check_strategy(arg)
137
138 return TupleStrategy(args)
139
140
141class ListStrategy(SearchStrategy[list[Ex]]):
142 """A strategy for lists which takes a strategy for its elements and the
143 allowed lengths, and generates lists with the correct size and contents."""
144
145 _nonempty_filters: tuple[Callable[[Any], Any], ...] = (bool, len, tuple, list)
146
147 def __init__(
148 self,
149 elements: SearchStrategy[Ex],
150 min_size: int = 0,
151 max_size: Optional[Union[float, int]] = math.inf,
152 ):
153 super().__init__()
154 self.min_size = min_size or 0
155 self.max_size = max_size if max_size is not None else math.inf
156 assert 0 <= self.min_size <= self.max_size
157 self.average_size = min(
158 max(self.min_size * 2, self.min_size + 5),
159 0.5 * (self.min_size + self.max_size),
160 )
161 self.element_strategy = elements
162 if min_size > BUFFER_SIZE:
163 raise InvalidArgument(
164 f"{self!r} can never generate an example, because min_size is larger "
165 "than Hypothesis supports. Including it is at best slowing down your "
166 "tests for no benefit; at worst making them fail (maybe flakily) with "
167 "a HealthCheck error."
168 )
169
170 def calc_label(self) -> int:
171 return combine_labels(self.class_label, self.element_strategy.label)
172
173 def do_validate(self) -> None:
174 self.element_strategy.validate()
175 if self.is_empty:
176 raise InvalidArgument(
177 "Cannot create non-empty lists with elements drawn from "
178 f"strategy {self.element_strategy!r} because it has no values."
179 )
180 if self.element_strategy.is_empty and 0 < self.max_size < float("inf"):
181 raise InvalidArgument(
182 f"Cannot create a collection of max_size={self.max_size!r}, "
183 "because no elements can be drawn from the element strategy "
184 f"{self.element_strategy!r}"
185 )
186
187 def calc_is_empty(self, recur: RecurT) -> bool:
188 if self.min_size == 0:
189 return False
190 else:
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 = self.element_strategy._filter_for_filtered_draw(
290 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 dict_type = type(mapping)
356 self.mapping = mapping
357 keys = tuple(mapping.keys())
358 self.fixed = st.tuples(*[mapping[k] for k in keys]).map(
359 lambda value: dict_type(zip(keys, value))
360 )
361 self.optional = optional
362
363 def do_draw(self, data: ConjectureData) -> dict[Any, Any]:
364 value = data.draw(self.fixed)
365 if self.optional is None:
366 return value
367
368 remaining = [k for k, v in self.optional.items() if not v.is_empty]
369 should_draw = cu.many(
370 data, min_size=0, max_size=len(remaining), average_size=len(remaining) / 2
371 )
372 while should_draw.more():
373 j = data.draw_integer(0, len(remaining) - 1)
374 remaining[-1], remaining[j] = remaining[j], remaining[-1]
375 key = remaining.pop()
376 value[key] = data.draw(self.optional[key])
377 return value
378
379 def calc_is_empty(self, recur: RecurT) -> bool:
380 return recur(self.fixed)
381
382 def __repr__(self) -> str:
383 if self.optional is not None:
384 return f"fixed_dictionaries({self.mapping!r}, optional={self.optional!r})"
385 return f"fixed_dictionaries({self.mapping!r})"