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 random import Random
15from typing import Any
16
17import attr
18
19from hypothesis.control import should_note
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, seed):
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@attr.s(slots=True)
151class RandomState:
152 next_states: dict = attr.ib(factory=dict)
153 state_id: Any = attr.ib(default=None)
154
155
156def state_for_seed(data, seed):
157 try:
158 seeds_to_states = data.seeds_to_states
159 except AttributeError:
160 seeds_to_states = {}
161 data.seeds_to_states = seeds_to_states
162
163 try:
164 state = seeds_to_states[seed]
165 except KeyError:
166 state = RandomState()
167 seeds_to_states[seed] = state
168
169 return state
170
171
172def normalize_zero(f: float) -> float:
173 if f == 0.0:
174 return 0.0
175 else:
176 return f
177
178
179class ArtificialRandom(HypothesisRandom):
180 VERSION = 10**6
181
182 def __init__(self, *, note_method_calls: bool, data: ConjectureData) -> None:
183 super().__init__(note_method_calls=note_method_calls)
184 self.__data = data
185 self.__state = RandomState()
186
187 def __repr__(self) -> str:
188 return "HypothesisRandom(generated data)"
189
190 def __copy__(self) -> "ArtificialRandom":
191 result = ArtificialRandom(
192 note_method_calls=self._note_method_calls,
193 data=self.__data,
194 )
195 result.setstate(self.getstate())
196 return result
197
198 def __convert_result(self, method, kwargs, result):
199 if method == "choice":
200 return kwargs.get("seq")[result]
201 if method in ("choices", "sample"):
202 seq = kwargs["population"]
203 return [seq[i] for i in result]
204 if method == "shuffle":
205 seq = kwargs["x"]
206 original = list(seq)
207 for i, i2 in enumerate(result):
208 seq[i] = original[i2]
209 return None
210 return result
211
212 def _hypothesis_do_random(self, method, kwargs):
213 if method == "choices":
214 key = (method, len(kwargs["population"]), kwargs.get("k"))
215 elif method == "choice":
216 key = (method, len(kwargs["seq"]))
217 elif method == "shuffle":
218 key = (method, len(kwargs["x"]))
219 else:
220 key = (method, *sorted(kwargs))
221
222 try:
223 result, self.__state = self.__state.next_states[key]
224 except KeyError:
225 pass
226 else:
227 return self.__convert_result(method, kwargs, result)
228
229 if method == "_randbelow":
230 result = self.__data.draw_integer(0, kwargs["n"] - 1)
231 elif method == "random":
232 # See https://github.com/HypothesisWorks/hypothesis/issues/4297
233 # for numerics/bounds of "random" and "betavariate"
234 result = self.__data.draw(floats(0, 1, exclude_max=True))
235 elif method == "betavariate":
236 result = self.__data.draw(floats(0, 1))
237 elif method == "uniform":
238 a = normalize_zero(kwargs["a"])
239 b = normalize_zero(kwargs["b"])
240 result = self.__data.draw(floats(a, b))
241 elif method in ("weibullvariate", "gammavariate"):
242 result = self.__data.draw(floats(min_value=0.0, allow_infinity=False))
243 elif method in ("gauss", "normalvariate"):
244 mu = kwargs["mu"]
245 result = mu + self.__data.draw(
246 floats(allow_nan=False, allow_infinity=False)
247 )
248 elif method == "vonmisesvariate":
249 result = self.__data.draw(floats(0, 2 * math.pi))
250 elif method == "randrange":
251 if kwargs["stop"] is None:
252 stop = kwargs["start"]
253 start = 0
254 else:
255 start = kwargs["start"]
256 stop = kwargs["stop"]
257
258 step = kwargs["step"]
259 if start == stop:
260 raise ValueError(f"empty range for randrange({start}, {stop}, {step})")
261
262 if step != 1:
263 endpoint = (stop - start) // step
264 if (start - stop) % step == 0:
265 endpoint -= 1
266
267 i = self.__data.draw_integer(0, endpoint)
268 result = start + i * step
269 else:
270 result = self.__data.draw_integer(start, stop - 1)
271 elif method == "randint":
272 result = self.__data.draw_integer(kwargs["a"], kwargs["b"])
273 # New in Python 3.12, so not taken by our coverage job
274 elif method == "binomialvariate": # pragma: no cover
275 result = self.__data.draw_integer(0, kwargs["n"])
276 elif method == "choice":
277 seq = kwargs["seq"]
278 result = self.__data.draw_integer(0, len(seq) - 1)
279 elif method == "choices":
280 k = kwargs["k"]
281 result = self.__data.draw(
282 lists(
283 integers(0, len(kwargs["population"]) - 1),
284 min_size=k,
285 max_size=k,
286 )
287 )
288 elif method == "sample":
289 k = kwargs["k"]
290 seq = kwargs["population"]
291
292 if k > len(seq) or k < 0:
293 raise ValueError(
294 f"Sample size {k} not in expected range 0 <= k <= {len(seq)}"
295 )
296
297 if k == 0:
298 result = []
299 else:
300 result = self.__data.draw(
301 lists(
302 sampled_from(range(len(seq))),
303 min_size=k,
304 max_size=k,
305 unique=True,
306 )
307 )
308
309 elif method == "getrandbits":
310 result = self.__data.draw_integer(0, 2 ** kwargs["n"] - 1)
311 elif method == "triangular":
312 low = normalize_zero(kwargs["low"])
313 high = normalize_zero(kwargs["high"])
314 mode = normalize_zero(kwargs["mode"])
315 if mode is None:
316 result = self.__data.draw(floats(low, high))
317 elif self.__data.draw_boolean(0.5):
318 result = self.__data.draw(floats(mode, high))
319 else:
320 result = self.__data.draw(floats(low, mode))
321 elif method in ("paretovariate", "expovariate", "lognormvariate"):
322 result = self.__data.draw(floats(min_value=0.0))
323 elif method == "shuffle":
324 result = self.__data.draw(permutations(range(len(kwargs["x"]))))
325 elif method == "randbytes":
326 n = int(kwargs["n"])
327 result = self.__data.draw_bytes(min_size=n, max_size=n)
328 else:
329 raise NotImplementedError(method)
330
331 new_state = RandomState()
332 self.__state.next_states[key] = (result, new_state)
333 self.__state = new_state
334
335 return self.__convert_result(method, kwargs, result)
336
337 def seed(self, seed):
338 self.__state = state_for_seed(self.__data, seed)
339
340 def getstate(self):
341 if self.__state.state_id is not None:
342 return self.__state.state_id
343
344 try:
345 states_for_ids = self.__data.states_for_ids
346 except AttributeError:
347 states_for_ids = {}
348 self.__data.states_for_ids = states_for_ids
349
350 self.__state.state_id = len(states_for_ids)
351 states_for_ids[self.__state.state_id] = self.__state
352
353 return self.__state.state_id
354
355 def setstate(self, state):
356 self.__state = self.__data.states_for_ids[state]
357
358
359DUMMY_RANDOM = Random(0)
360
361
362def convert_kwargs(name, kwargs):
363 kwargs = dict(kwargs)
364
365 signature = sig_of(name)
366 params = signature.parameters
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 params[k].default
374 or params[k].kind != inspect.Parameter.KEYWORD_ONLY
375 ):
376 kwargs.pop(k)
377
378 arg_names = list(params)[1:]
379
380 args = []
381
382 for a in arg_names:
383 if params[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 params[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=note_method_calls)
401 self.__seed = seed
402 self.__random = Random(seed)
403
404 def _hypothesis_do_random(self, method, kwargs):
405 fn = getattr(self.__random, method)
406 try:
407 return fn(**kwargs)
408 except TypeError:
409 pass
410 args, kwargs = convert_kwargs(method, kwargs)
411 return fn(*args, **kwargs)
412
413 def __copy__(self) -> "TrueRandom":
414 result = TrueRandom(
415 seed=self.__seed,
416 note_method_calls=self._note_method_calls,
417 )
418 result.setstate(self.getstate())
419 return result
420
421 def __repr__(self) -> str:
422 return f"Random({self.__seed!r})"
423
424 def seed(self, seed):
425 self.__random.seed(seed)
426 self.__seed = seed
427
428 def getstate(self):
429 return self.__random.getstate()
430
431 def setstate(self, state):
432 self.__random.setstate(state)
433
434
435class RandomStrategy(SearchStrategy[HypothesisRandom]):
436 def __init__(self, *, note_method_calls: bool, use_true_random: bool) -> None:
437 self.__note_method_calls = note_method_calls
438 self.__use_true_random = use_true_random
439
440 def do_draw(self, data: ConjectureData) -> HypothesisRandom:
441 if self.__use_true_random:
442 seed = data.draw_integer(0, 2**64 - 1)
443 return TrueRandom(seed=seed, note_method_calls=self.__note_method_calls)
444 else:
445 return ArtificialRandom(
446 note_method_calls=self.__note_method_calls, data=data
447 )