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 # New in Python 3.12, so not taken by our coverage job
271 elif method == "binomialvariate": # pragma: no cover
272 result = self.__data.draw_integer(0, kwargs["n"])
273 elif method == "choice":
274 seq = kwargs["seq"]
275 result = self.__data.draw_integer(0, len(seq) - 1)
276 elif method == "choices":
277 k = kwargs["k"]
278 result = self.__data.draw(
279 lists(
280 integers(0, len(kwargs["population"]) - 1),
281 min_size=k,
282 max_size=k,
283 )
284 )
285 elif method == "sample":
286 k = kwargs["k"]
287 seq = kwargs["population"]
288
289 if k > len(seq) or k < 0:
290 raise ValueError(
291 f"Sample size {k} not in expected range 0 <= k <= {len(seq)}"
292 )
293
294 if k == 0:
295 result = []
296 else:
297 result = self.__data.draw(
298 lists(
299 sampled_from(range(len(seq))),
300 min_size=k,
301 max_size=k,
302 unique=True,
303 )
304 )
305
306 elif method == "getrandbits":
307 result = self.__data.draw_integer(0, 2 ** kwargs["n"] - 1)
308 elif method == "triangular":
309 low = normalize_zero(kwargs["low"])
310 high = normalize_zero(kwargs["high"])
311 mode = normalize_zero(kwargs["mode"])
312 if mode is None:
313 result = self.__data.draw(floats(low, high))
314 elif self.__data.draw_boolean(0.5):
315 result = self.__data.draw(floats(mode, high))
316 else:
317 result = self.__data.draw(floats(low, mode))
318 elif method in ("paretovariate", "expovariate", "lognormvariate"):
319 result = self.__data.draw(floats(min_value=0.0))
320 elif method == "shuffle":
321 result = self.__data.draw(permutations(range(len(kwargs["x"]))))
322 elif method == "randbytes":
323 n = int(kwargs["n"])
324 result = self.__data.draw_bytes(min_size=n, max_size=n)
325 else:
326 raise NotImplementedError(method)
327
328 new_state = RandomState()
329 self.__state.next_states[key] = (result, new_state)
330 self.__state = new_state
331
332 return self.__convert_result(method, kwargs, result)
333
334 def seed(self, seed):
335 self.__state = state_for_seed(self.__data, seed)
336
337 def getstate(self):
338 if self.__state.state_id is not None:
339 return self.__state.state_id
340
341 if self.__data.states_for_ids is None:
342 self.__data.states_for_ids = {}
343 states_for_ids = self.__data.states_for_ids
344 self.__state.state_id = len(states_for_ids)
345 states_for_ids[self.__state.state_id] = self.__state
346
347 return self.__state.state_id
348
349 def setstate(self, state):
350 self.__state = self.__data.states_for_ids[state]
351
352
353DUMMY_RANDOM = Random(0)
354
355
356def convert_kwargs(name, kwargs):
357 kwargs = dict(kwargs)
358
359 signature = sig_of(name)
360 params = signature.parameters
361
362 bound = signature.bind(DUMMY_RANDOM, **kwargs)
363 bound.apply_defaults()
364
365 for k in list(kwargs):
366 if (
367 kwargs[k] is params[k].default
368 or params[k].kind != inspect.Parameter.KEYWORD_ONLY
369 ):
370 kwargs.pop(k)
371
372 arg_names = list(params)[1:]
373
374 args = []
375
376 for a in arg_names:
377 if params[a].kind == inspect.Parameter.KEYWORD_ONLY:
378 break
379 args.append(bound.arguments[a])
380 kwargs.pop(a, None)
381
382 while args:
383 name = arg_names[len(args) - 1]
384 if args[-1] is params[name].default:
385 args.pop()
386 else:
387 break
388
389 return (args, kwargs)
390
391
392class TrueRandom(HypothesisRandom):
393 def __init__(self, seed, note_method_calls):
394 super().__init__(note_method_calls=note_method_calls)
395 self.__seed = seed
396 self.__random = Random(seed)
397
398 def _hypothesis_do_random(self, method, kwargs):
399 fn = getattr(self.__random, method)
400 try:
401 return fn(**kwargs)
402 except TypeError:
403 pass
404 args, kwargs = convert_kwargs(method, kwargs)
405 return fn(*args, **kwargs)
406
407 def __copy__(self) -> "TrueRandom":
408 result = TrueRandom(
409 seed=self.__seed,
410 note_method_calls=self._note_method_calls,
411 )
412 result.setstate(self.getstate())
413 return result
414
415 def __repr__(self) -> str:
416 return f"Random({self.__seed!r})"
417
418 def seed(self, seed):
419 self.__random.seed(seed)
420 self.__seed = seed
421
422 def getstate(self):
423 return self.__random.getstate()
424
425 def setstate(self, state):
426 self.__random.setstate(state)
427
428
429class RandomStrategy(SearchStrategy[HypothesisRandom]):
430 def __init__(self, *, note_method_calls: bool, use_true_random: bool) -> None:
431 super().__init__()
432 self.__note_method_calls = note_method_calls
433 self.__use_true_random = use_true_random
434
435 def do_draw(self, data: ConjectureData) -> HypothesisRandom:
436 if self.__use_true_random:
437 seed = data.draw_integer(0, 2**64 - 1)
438 return TrueRandom(seed=seed, note_method_calls=self.__note_method_calls)
439 else:
440 return ArtificialRandom(
441 note_method_calls=self.__note_method_calls, data=data
442 )