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 abc
12import inspect
13import math
14from dataclasses import dataclass, field
15from random import Random
16from typing import Any
17
18from hypothesis.control import should_note
19from hypothesis.errors import InvalidArgument
20from hypothesis.internal.conjecture.data import ConjectureData
21from hypothesis.internal.reflection import define_function_signature
22from hypothesis.reporting import report
23from hypothesis.strategies._internal.core import lists, permutations, sampled_from
24from hypothesis.strategies._internal.numbers import floats, integers
25from hypothesis.strategies._internal.strategies import SearchStrategy
26
27
28class HypothesisRandom(Random, abc.ABC):
29 """A subclass of Random designed to expose the seed it was initially
30 provided with."""
31
32 def __init__(self, *, note_method_calls: bool) -> None:
33 self._note_method_calls = note_method_calls
34
35 def __deepcopy__(self, table):
36 return self.__copy__()
37
38 @abc.abstractmethod
39 def seed(self, a=None, version=2):
40 raise NotImplementedError
41
42 @abc.abstractmethod
43 def getstate(self):
44 raise NotImplementedError
45
46 @abc.abstractmethod
47 def setstate(self, state):
48 raise NotImplementedError
49
50 @abc.abstractmethod
51 def _hypothesis_do_random(self, method, kwargs):
52 raise NotImplementedError
53
54 def _hypothesis_log_random(self, method, kwargs, result):
55 if not (self._note_method_calls and should_note()):
56 return
57
58 args, kwargs = convert_kwargs(method, kwargs)
59 argstr = ", ".join(
60 list(map(repr, args)) + [f"{k}={v!r}" for k, v in kwargs.items()]
61 )
62 report(f"{self!r}.{method}({argstr}) -> {result!r}")
63
64
65RANDOM_METHODS = [
66 name
67 for name in [
68 "_randbelow",
69 "betavariate",
70 "binomialvariate",
71 "choice",
72 "choices",
73 "expovariate",
74 "gammavariate",
75 "gauss",
76 "getrandbits",
77 "lognormvariate",
78 "normalvariate",
79 "paretovariate",
80 "randint",
81 "random",
82 "randrange",
83 "sample",
84 "shuffle",
85 "triangular",
86 "uniform",
87 "vonmisesvariate",
88 "weibullvariate",
89 "randbytes",
90 ]
91 if hasattr(Random, name)
92]
93
94
95# Fake shims to get a good signature
96def getrandbits(self, n: int) -> int: # type: ignore
97 raise NotImplementedError
98
99
100def random(self) -> float: # type: ignore
101 raise NotImplementedError
102
103
104def _randbelow(self, n: int) -> int: # type: ignore
105 raise NotImplementedError
106
107
108STUBS = {f.__name__: f for f in [getrandbits, random, _randbelow]}
109
110
111SIGNATURES: dict[str, inspect.Signature] = {}
112
113
114def sig_of(name):
115 try:
116 return SIGNATURES[name]
117 except KeyError:
118 pass
119
120 target = getattr(Random, name)
121 result = inspect.signature(STUBS.get(name, target))
122 SIGNATURES[name] = result
123 return result
124
125
126def define_copy_method(name):
127 target = getattr(Random, name)
128
129 def implementation(self, **kwargs):
130 result = self._hypothesis_do_random(name, kwargs)
131 self._hypothesis_log_random(name, kwargs, result)
132 return result
133
134 sig = inspect.signature(STUBS.get(name, target))
135
136 result = define_function_signature(target.__name__, target.__doc__, sig)(
137 implementation
138 )
139
140 result.__module__ = __name__
141 result.__qualname__ = "HypothesisRandom." + result.__name__
142
143 setattr(HypothesisRandom, name, result)
144
145
146for r in RANDOM_METHODS:
147 define_copy_method(r)
148
149
150@dataclass(slots=True, frozen=False)
151class RandomState:
152 next_states: dict = field(default_factory=dict)
153 state_id: Any = None
154
155
156def state_for_seed(data, seed):
157 if data.seeds_to_states is None:
158 data.seeds_to_states = {}
159
160 seeds_to_states = data.seeds_to_states
161 try:
162 state = seeds_to_states[seed]
163 except KeyError:
164 state = RandomState()
165 seeds_to_states[seed] = state
166
167 return state
168
169
170def check_seed(a: object) -> None:
171 if a is None:
172 raise InvalidArgument(
173 "Hypothesis-generated Random instances do not support seed() "
174 "without an explicit seed"
175 )
176
177
178def normalize_zero(f: float) -> float:
179 if f == 0.0:
180 return 0.0
181 else:
182 return f
183
184
185class ArtificialRandom(HypothesisRandom):
186 VERSION = 10**6
187
188 def __init__(self, *, note_method_calls: bool, data: ConjectureData) -> None:
189 super().__init__(note_method_calls=note_method_calls)
190 self.__data = data
191 self.__state = RandomState()
192
193 def __repr__(self) -> str:
194 return "HypothesisRandom(generated data)"
195
196 def __copy__(self) -> "ArtificialRandom":
197 result = ArtificialRandom(
198 note_method_calls=self._note_method_calls,
199 data=self.__data,
200 )
201 result.setstate(self.getstate())
202 return result
203
204 def __convert_result(self, method, kwargs, result):
205 if method == "choice":
206 return kwargs.get("seq")[result]
207 if method in ("choices", "sample"):
208 seq = kwargs["population"]
209 return [seq[i] for i in result]
210 if method == "shuffle":
211 seq = kwargs["x"]
212 original = list(seq)
213 for i, i2 in enumerate(result):
214 seq[i] = original[i2]
215 return None
216 return result
217
218 def _hypothesis_do_random(self, method, kwargs):
219 if method == "choices":
220 key = (method, len(kwargs["population"]), kwargs.get("k"))
221 elif method == "choice":
222 key = (method, len(kwargs["seq"]))
223 elif method == "shuffle":
224 key = (method, len(kwargs["x"]))
225 else:
226 key = (method, *sorted(kwargs))
227
228 try:
229 result, self.__state = self.__state.next_states[key]
230 except KeyError:
231 pass
232 else:
233 return self.__convert_result(method, kwargs, result)
234
235 if method == "_randbelow":
236 result = self.__data.draw_integer(0, kwargs["n"] - 1)
237 elif method == "random":
238 # See https://github.com/HypothesisWorks/hypothesis/issues/4297
239 # for numerics/bounds of "random" and "betavariate"
240 result = self.__data.draw(floats(0, 1, exclude_max=True))
241 elif method == "betavariate":
242 result = self.__data.draw(floats(0, 1))
243 elif method == "uniform":
244 a = normalize_zero(kwargs["a"])
245 b = normalize_zero(kwargs["b"])
246 result = self.__data.draw(floats(a, b))
247 elif method in ("weibullvariate", "gammavariate"):
248 result = self.__data.draw(floats(min_value=0.0, allow_infinity=False))
249 elif method in ("gauss", "normalvariate"):
250 mu = kwargs["mu"]
251 result = mu + self.__data.draw(
252 floats(allow_nan=False, allow_infinity=False)
253 )
254 elif method == "vonmisesvariate":
255 result = self.__data.draw(floats(0, 2 * math.pi))
256 elif method == "randrange":
257 if kwargs["stop"] is None:
258 stop = kwargs["start"]
259 start = 0
260 else:
261 start = kwargs["start"]
262 stop = kwargs["stop"]
263
264 step = kwargs["step"]
265 if start == stop:
266 raise ValueError(f"empty range for randrange({start}, {stop}, {step})")
267
268 if step != 1:
269 endpoint = (stop - start) // step
270 if (start - stop) % step == 0:
271 endpoint -= 1
272
273 i = self.__data.draw_integer(0, endpoint)
274 result = start + i * step
275 else:
276 result = self.__data.draw_integer(start, stop - 1)
277 elif method == "randint":
278 result = self.__data.draw_integer(kwargs["a"], kwargs["b"])
279 elif method == "binomialvariate":
280 result = self.__data.draw_integer(0, kwargs["n"])
281 elif method == "choice":
282 seq = kwargs["seq"]
283 result = self.__data.draw_integer(0, len(seq) - 1)
284 elif method == "choices":
285 k = kwargs["k"]
286 result = self.__data.draw(
287 lists(
288 integers(0, len(kwargs["population"]) - 1),
289 min_size=k,
290 max_size=k,
291 )
292 )
293 elif method == "sample":
294 k = kwargs["k"]
295 seq = kwargs["population"]
296
297 if k > len(seq) or k < 0:
298 raise ValueError(
299 f"Sample size {k} not in expected range 0 <= k <= {len(seq)}"
300 )
301
302 if k == 0:
303 result = []
304 else:
305 result = self.__data.draw(
306 lists(
307 sampled_from(range(len(seq))),
308 min_size=k,
309 max_size=k,
310 unique=True,
311 )
312 )
313
314 elif method == "getrandbits":
315 result = self.__data.draw_integer(0, 2 ** kwargs["n"] - 1)
316 elif method == "triangular":
317 low = normalize_zero(kwargs["low"])
318 high = normalize_zero(kwargs["high"])
319 mode = normalize_zero(kwargs["mode"])
320 if mode is None:
321 result = self.__data.draw(floats(low, high))
322 elif self.__data.draw_boolean(0.5):
323 result = self.__data.draw(floats(mode, high))
324 else:
325 result = self.__data.draw(floats(low, mode))
326 elif method in ("paretovariate", "expovariate", "lognormvariate"):
327 result = self.__data.draw(floats(min_value=0.0))
328 elif method == "shuffle":
329 result = self.__data.draw(permutations(range(len(kwargs["x"]))))
330 elif method == "randbytes":
331 n = int(kwargs["n"])
332 result = self.__data.draw_bytes(min_size=n, max_size=n)
333 else:
334 raise NotImplementedError(method)
335
336 new_state = RandomState()
337 self.__state.next_states[key] = (result, new_state)
338 self.__state = new_state
339
340 return self.__convert_result(method, kwargs, result)
341
342 def seed(self, a=None, version=2):
343 check_seed(a)
344 self.__state = state_for_seed(self.__data, a)
345
346 def getstate(self):
347 if self.__state.state_id is not None:
348 return self.__state.state_id
349
350 if self.__data.states_for_ids is None:
351 self.__data.states_for_ids = {}
352 states_for_ids = self.__data.states_for_ids
353 self.__state.state_id = len(states_for_ids)
354 states_for_ids[self.__state.state_id] = self.__state
355
356 return self.__state.state_id
357
358 def setstate(self, state):
359 self.__state = self.__data.states_for_ids[state]
360
361
362DUMMY_RANDOM = Random(0)
363
364
365def convert_kwargs(name, kwargs):
366 kwargs = dict(kwargs)
367
368 signature = sig_of(name)
369 params = signature.parameters
370
371 bound = signature.bind(DUMMY_RANDOM, **kwargs)
372 bound.apply_defaults()
373
374 for k in list(kwargs):
375 if (
376 kwargs[k] is params[k].default
377 or params[k].kind != inspect.Parameter.KEYWORD_ONLY
378 ):
379 kwargs.pop(k)
380
381 arg_names = list(params)[1:]
382
383 args = []
384
385 for a in arg_names:
386 if params[a].kind == inspect.Parameter.KEYWORD_ONLY:
387 break
388 args.append(bound.arguments[a])
389 kwargs.pop(a, None)
390
391 while args:
392 name = arg_names[len(args) - 1]
393 if args[-1] is params[name].default:
394 args.pop()
395 else:
396 break
397
398 return (args, kwargs)
399
400
401class TrueRandom(HypothesisRandom):
402 def __init__(self, seed, note_method_calls):
403 super().__init__(note_method_calls=note_method_calls)
404 self.__seed = seed
405 self.__random = Random(seed)
406
407 def _hypothesis_do_random(self, method, kwargs):
408 fn = getattr(self.__random, method)
409 try:
410 return fn(**kwargs)
411 except TypeError:
412 pass
413 args, kwargs = convert_kwargs(method, kwargs)
414 return fn(*args, **kwargs)
415
416 def __copy__(self) -> "TrueRandom":
417 result = TrueRandom(
418 seed=self.__seed,
419 note_method_calls=self._note_method_calls,
420 )
421 result.setstate(self.getstate())
422 return result
423
424 def __repr__(self) -> str:
425 return f"Random({self.__seed!r})"
426
427 def seed(self, a=None, version=2):
428 check_seed(a)
429 self.__random.seed(a, version)
430 self.__seed = a
431
432 def getstate(self):
433 return self.__random.getstate()
434
435 def setstate(self, state):
436 self.__random.setstate(state)
437
438
439class RandomStrategy(SearchStrategy[HypothesisRandom]):
440 def __init__(self, *, note_method_calls: bool, use_true_random: bool) -> None:
441 super().__init__()
442 self.__note_method_calls = note_method_calls
443 self.__use_true_random = use_true_random
444
445 def do_draw(self, data: ConjectureData) -> HypothesisRandom:
446 if self.__use_true_random:
447 seed = data.draw_integer(0, 2**64 - 1)
448 return TrueRandom(seed=seed, note_method_calls=self.__note_method_calls)
449 else:
450 return ArtificialRandom(
451 note_method_calls=self.__note_method_calls, data=data
452 )