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