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