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