1# This file is part of Hypothesis, which may be found at
2# https://github.com/HypothesisWorks/hypothesis/
3#
4# Most of this work is copyright (C) 2013-2021 David R. MacIver
5# (david@drmaciver.com), but it contains contributions by others. See
6# CONTRIBUTING.rst for a full list of people who may hold copyright, and
7# consult the git log if you need to determine who owns an individual
8# contribution.
9#
10# This Source Code Form is subject to the terms of the Mozilla Public License,
11# v. 2.0. If a copy of the MPL was not distributed with this file, You can
12# obtain one at https://mozilla.org/MPL/2.0/.
13#
14# END HEADER
15
16import inspect
17import math
18from random import Random
19from typing import Dict
20
21import attr
22
23from hypothesis.control import should_note
24from hypothesis.internal.conjecture import utils as cu
25from hypothesis.internal.reflection import define_function_signature
26from hypothesis.reporting import report
27from hypothesis.strategies._internal.core import (
28    binary,
29    lists,
30    permutations,
31    sampled_from,
32)
33from hypothesis.strategies._internal.numbers import floats, integers
34from hypothesis.strategies._internal.strategies import SearchStrategy
35
36
37class HypothesisRandom(Random):
38    """A subclass of Random designed to expose the seed it was initially
39    provided with."""
40
41    def __init__(self, note_method_calls):
42        self.__note_method_calls = note_method_calls
43
44    def __deepcopy__(self, table):
45        return self.__copy__()
46
47    def __repr__(self):
48        raise NotImplementedError()
49
50    def seed(self, seed):
51        raise NotImplementedError()
52
53    def getstate(self):
54        raise NotImplementedError()
55
56    def setstate(self, state):
57        raise NotImplementedError()
58
59    def _hypothesis_log_random(self, method, kwargs, result):
60        if not (self.__note_method_calls and should_note()):
61            return
62
63        args, kwargs = convert_kwargs(method, kwargs)
64        argstr = ", ".join(
65            list(map(repr, args)) + [f"{k}={v!r}" for k, v in kwargs.items()]
66        )
67        report(f"{self!r}.{method}({argstr}) -> {result!r}")
68
69    def _hypothesis_do_random(self, method, kwargs):
70        raise NotImplementedError()
71
72
73RANDOM_METHODS = [
74    name
75    for name in [
76        "_randbelow",
77        "betavariate",
78        "choice",
79        "choices",
80        "expovariate",
81        "gammavariate",
82        "gauss",
83        "getrandbits",
84        "lognormvariate",
85        "normalvariate",
86        "paretovariate",
87        "randint",
88        "random",
89        "randrange",
90        "sample",
91        "shuffle",
92        "triangular",
93        "uniform",
94        "vonmisesvariate",
95        "weibullvariate",
96        "randbytes",
97    ]
98    if hasattr(Random, name)
99]
100
101
102# Fake shims to get a good signature
103def getrandbits(self, n: int) -> int:  # type: ignore
104    raise NotImplementedError()
105
106
107def random(self) -> float:  # type: ignore
108    raise NotImplementedError()
109
110
111def _randbelow(self, n: int) -> int:  # type: ignore
112    raise NotImplementedError()
113
114
115STUBS = {f.__name__: f for f in [getrandbits, random, _randbelow]}
116
117
118SIGNATURES: Dict[str, inspect.Signature] = {}
119
120
121def sig_of(name):
122    try:
123        return SIGNATURES[name]
124    except KeyError:
125        pass
126
127    target = getattr(Random, name)
128    result = inspect.signature(STUBS.get(name, target))
129    SIGNATURES[name] = result
130    return result
131
132
133def define_copy_method(name):
134    target = getattr(Random, name)
135
136    def implementation(self, **kwargs):
137        result = self._hypothesis_do_random(name, kwargs)
138        self._hypothesis_log_random(name, kwargs, result)
139        return result
140
141    spec = inspect.getfullargspec(STUBS.get(name, target))
142
143    result = define_function_signature(target.__name__, target.__doc__, spec)(
144        implementation
145    )
146
147    result.__module__ = __name__
148    result.__qualname__ = "HypothesisRandom." + result.__name__
149
150    setattr(HypothesisRandom, name, result)
151
152
153for r in RANDOM_METHODS:
154    define_copy_method(r)
155
156
157@attr.s(slots=True)
158class RandomState:
159    next_states = attr.ib(default=attr.Factory(dict))
160    state_id = attr.ib(default=None)
161
162
163def state_for_seed(data, seed):
164    try:
165        seeds_to_states = data.seeds_to_states
166    except AttributeError:
167        seeds_to_states = {}
168        data.seeds_to_states = seeds_to_states
169
170    try:
171        state = seeds_to_states[seed]
172    except KeyError:
173        state = RandomState()
174        seeds_to_states[seed] = state
175
176    return state
177
178
179UNIFORM = floats(0, 1)
180
181
182def normalize_zero(f: float) -> float:
183    if f == 0.0:
184        return 0.0
185    else:
186        return f
187
188
189class ArtificialRandom(HypothesisRandom):
190    VERSION = 10 ** 6
191
192    def __init__(self, note_method_calls, data):
193        super().__init__(note_method_calls=note_method_calls)
194        self.__data = data
195        self.__state = RandomState()
196
197    def __repr__(self):
198        return "HypothesisRandom(generated data)"
199
200    def __copy__(self):
201        result = ArtificialRandom(
202            note_method_calls=self._HypothesisRandom__note_method_calls,
203            data=self.__data,
204        )
205        result.setstate(self.getstate())
206        return result
207
208    def __convert_result(self, method, kwargs, result):
209        if method == "choice":
210            return kwargs.get("seq")[result]
211        if method in ("choices", "sample"):
212            seq = kwargs["population"]
213            return [seq[i] for i in result]
214        if method == "shuffle":
215            seq = kwargs["x"]
216            original = list(seq)
217            for i, i2 in enumerate(result):
218                seq[i] = original[i2]
219            return
220        return result
221
222    def _hypothesis_do_random(self, method, kwargs):
223        if method == "choices":
224            key = (method, len(kwargs["population"]), kwargs.get("k"))
225        elif method == "choice":
226            key = (method, len(kwargs["seq"]))
227        elif method == "shuffle":
228            key = (method, len(kwargs["x"]))
229        else:
230            key = (method,) + tuple(sorted(kwargs))
231
232        try:
233            result, self.__state = self.__state.next_states[key]
234        except KeyError:
235            pass
236        else:
237            return self.__convert_result(method, kwargs, result)
238
239        if method == "_randbelow":
240            result = cu.integer_range(self.__data, 0, kwargs["n"] - 1)
241        elif method in ("betavariate", "random"):
242            result = self.__data.draw(UNIFORM)
243        elif method == "uniform":
244            a = normalize_zero(kwargs["a"])
245            b = normalize_zero(kwargs["b"])
246            result = self.__data.draw(floats(a, b))
247        elif method in ("weibullvariate", "gammavariate"):
248            result = self.__data.draw(floats(min_value=0.0, allow_infinity=False))
249        elif method in ("gauss", "normalvariate"):
250            mu = kwargs["mu"]
251            result = mu + self.__data.draw(
252                floats(allow_nan=False, allow_infinity=False)
253            )
254        elif method == "vonmisesvariate":
255            result = self.__data.draw(floats(0, 2 * math.pi))
256        elif method == "randrange":
257            if kwargs["stop"] is None:
258                stop = kwargs["start"]
259                start = 0
260            else:
261                start = kwargs["start"]
262                stop = kwargs["stop"]
263
264            step = kwargs["step"]
265            if start == stop:
266                raise ValueError(f"empty range for randrange({start}, {stop}, {step})")
267
268            if step != 1:
269                endpoint = (stop - start) // step
270                if (start - stop) % step == 0:
271                    endpoint -= 1
272
273                i = cu.integer_range(self.__data, 0, endpoint)
274                result = start + i * step
275            else:
276                result = cu.integer_range(self.__data, start, stop - 1)
277        elif method == "randint":
278            result = cu.integer_range(self.__data, kwargs["a"], kwargs["b"])
279        elif method == "choice":
280            seq = kwargs["seq"]
281            result = cu.integer_range(self.__data, 0, len(seq) - 1)
282        elif method == "choices":
283            k = kwargs["k"]
284            result = self.__data.draw(
285                lists(
286                    integers(0, len(kwargs["population"]) - 1),
287                    min_size=k,
288                    max_size=k,
289                )
290            )
291        elif method == "sample":
292            k = kwargs["k"]
293            seq = kwargs["population"]
294
295            if k > len(seq) or k < 0:
296                raise ValueError(
297                    f"Sample size {k} not in expected range 0 <= k <= {len(seq)}"
298                )
299
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_bits(kwargs["n"])
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_bits(1):
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        # This is tested for but only appears in 3.9 so doesn't appear in coverage.
326        elif method == "randbytes":  # pragma: no cover
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  # pragma: no cover  # Only on Python < 3.8
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_bits(64)
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            )
444