Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/hypothesis/internal/entropy.py: 66%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

70 statements  

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 contextlib 

12import gc 

13import random 

14import sys 

15import warnings 

16from itertools import count 

17from typing import TYPE_CHECKING, Any, Callable, Hashable, Tuple 

18from weakref import WeakValueDictionary 

19 

20import hypothesis.core 

21from hypothesis.errors import HypothesisWarning, InvalidArgument 

22from hypothesis.internal.compat import FREE_THREADED_CPYTHON, GRAALPY, PYPY 

23 

24if TYPE_CHECKING: 

25 from typing import Protocol 

26 

27 # we can't use this at runtime until from_type supports 

28 # protocols -- breaks ghostwriter tests 

29 class RandomLike(Protocol): 

30 seed: Callable[..., Any] 

31 getstate: Callable[[], Any] 

32 setstate: Callable[..., Any] 

33 

34else: # pragma: no cover 

35 RandomLike = random.Random 

36 

37# This is effectively a WeakSet, which allows us to associate the saved states 

38# with their respective Random instances even as new ones are registered and old 

39# ones go out of scope and get garbage collected. Keys are ascending integers. 

40_RKEY = count() 

41RANDOMS_TO_MANAGE: WeakValueDictionary = WeakValueDictionary({next(_RKEY): random}) 

42 

43 

44class NumpyRandomWrapper: 

45 def __init__(self): 

46 assert "numpy" in sys.modules 

47 # This class provides a shim that matches the numpy to stdlib random, 

48 # and lets us avoid importing Numpy until it's already in use. 

49 import numpy.random 

50 

51 self.seed = numpy.random.seed 

52 self.getstate = numpy.random.get_state 

53 self.setstate = numpy.random.set_state 

54 

55 

56NP_RANDOM = None 

57 

58 

59if not (PYPY or GRAALPY): 

60 

61 def _get_platform_base_refcount(r: Any) -> int: 

62 return sys.getrefcount(r) 

63 

64 # Determine the number of refcounts created by function scope for 

65 # the given platform / version of Python. 

66 _PLATFORM_REF_COUNT = _get_platform_base_refcount(object()) 

67else: # pragma: no cover 

68 # PYPY and GRAALPY don't have `sys.getrefcount` 

69 _PLATFORM_REF_COUNT = -1 

70 

71 

72def register_random(r: RandomLike) -> None: 

73 """Register (a weakref to) the given Random-like instance for management by 

74 Hypothesis. 

75 

76 You can pass instances of structural subtypes of ``random.Random`` 

77 (i.e., objects with seed, getstate, and setstate methods) to 

78 ``register_random(r)`` to have their states seeded and restored in the same 

79 way as the global PRNGs from the ``random`` and ``numpy.random`` modules. 

80 

81 All global PRNGs, from e.g. simulation or scheduling frameworks, should 

82 be registered to prevent flaky tests. Hypothesis will ensure that the 

83 PRNG state is consistent for all test runs, always seeding them to zero and 

84 restoring the previous state after the test, or, reproducibly varied if you 

85 choose to use the :func:`~hypothesis.strategies.random_module` strategy. 

86 

87 ``register_random`` only makes `weakrefs 

88 <https://docs.python.org/3/library/weakref.html#module-weakref>`_ to ``r``, 

89 thus ``r`` will only be managed by Hypothesis as long as it has active 

90 references elsewhere at runtime. The pattern ``register_random(MyRandom())`` 

91 will raise a ``ReferenceError`` to help protect users from this issue. 

92 This check does not occur for the PyPy interpreter. See the following example for 

93 an illustration of this issue 

94 

95 .. code-block:: python 

96 

97 

98 def my_BROKEN_hook(): 

99 r = MyRandomLike() 

100 

101 # `r` will be garbage collected after the hook resolved 

102 # and Hypothesis will 'forget' that it was registered 

103 register_random(r) # Hypothesis will emit a warning 

104 

105 

106 rng = MyRandomLike() 

107 

108 

109 def my_WORKING_hook(): 

110 register_random(rng) 

111 """ 

112 if not (hasattr(r, "seed") and hasattr(r, "getstate") and hasattr(r, "setstate")): 

113 raise InvalidArgument(f"{r=} does not have all the required methods") 

114 

115 if r in RANDOMS_TO_MANAGE.values(): 

116 return 

117 

118 if not (PYPY or GRAALPY): # pragma: no branch 

119 # PYPY and GRAALPY do not have `sys.getrefcount`. 

120 gc.collect() 

121 if not gc.get_referrers(r): 

122 if sys.getrefcount(r) <= _PLATFORM_REF_COUNT: 

123 raise ReferenceError( 

124 f"`register_random` was passed `r={r}` which will be " 

125 "garbage collected immediately after `register_random` creates a " 

126 "weakref to it. This will prevent Hypothesis from managing this " 

127 "PRNG. See the docs for `register_random` for more " 

128 "details." 

129 ) 

130 elif not FREE_THREADED_CPYTHON: 

131 # On CPython, check for the free-threaded build because 

132 # gc.get_referrers() ignores objects with immortal refcounts 

133 # and objects are immortalized in the Python 3.13 

134 # free-threading implementation at runtime. 

135 

136 warnings.warn( 

137 "It looks like `register_random` was passed an object that could " 

138 "be garbage collected immediately after `register_random` creates " 

139 "a weakref to it. This will prevent Hypothesis from managing this " 

140 "PRNG. See the docs for `register_random` for more details.", 

141 HypothesisWarning, 

142 stacklevel=2, 

143 ) 

144 

145 RANDOMS_TO_MANAGE[next(_RKEY)] = r 

146 

147 

148def get_seeder_and_restorer( 

149 seed: Hashable = 0, 

150) -> Tuple[Callable[[], None], Callable[[], None]]: 

151 """Return a pair of functions which respectively seed all and restore 

152 the state of all registered PRNGs. 

153 

154 This is used by the core engine via `deterministic_PRNG`, and by users 

155 via `register_random`. We support registration of additional random.Random 

156 instances (or other objects with seed, getstate, and setstate methods) 

157 to force determinism on simulation or scheduling frameworks which avoid 

158 using the global random state. See e.g. #1709. 

159 """ 

160 assert isinstance(seed, int) 

161 assert 0 <= seed < 2**32 

162 states: dict = {} 

163 

164 if "numpy" in sys.modules: 

165 global NP_RANDOM 

166 if NP_RANDOM is None: 

167 # Protect this from garbage-collection by adding it to global scope 

168 NP_RANDOM = RANDOMS_TO_MANAGE[next(_RKEY)] = NumpyRandomWrapper() 

169 

170 def seed_all(): 

171 assert not states 

172 for k, r in RANDOMS_TO_MANAGE.items(): 

173 states[k] = r.getstate() 

174 r.seed(seed) 

175 

176 def restore_all(): 

177 for k, state in states.items(): 

178 r = RANDOMS_TO_MANAGE.get(k) 

179 if r is not None: # i.e., hasn't been garbage-collected 

180 r.setstate(state) 

181 states.clear() 

182 

183 return seed_all, restore_all 

184 

185 

186@contextlib.contextmanager 

187def deterministic_PRNG(seed=0): 

188 """Context manager that handles random.seed without polluting global state. 

189 

190 See issue #1255 and PR #1295 for details and motivation - in short, 

191 leaving the global pseudo-random number generator (PRNG) seeded is a very 

192 bad idea in principle, and breaks all kinds of independence assumptions 

193 in practice. 

194 """ 

195 if hypothesis.core._hypothesis_global_random is None: # pragma: no cover 

196 hypothesis.core._hypothesis_global_random = random.Random() 

197 register_random(hypothesis.core._hypothesis_global_random) 

198 

199 seed_all, restore_all = get_seeder_and_restorer(seed) 

200 seed_all() 

201 try: 

202 yield 

203 finally: 

204 restore_all()