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

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

84 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 collections.abc import Generator, Hashable 

17from itertools import count 

18from random import Random 

19from typing import TYPE_CHECKING, Any, Callable, Optional 

20from weakref import WeakValueDictionary 

21 

22import hypothesis.core 

23from hypothesis.errors import HypothesisWarning, InvalidArgument 

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

25 

26if TYPE_CHECKING: 

27 from typing import Protocol 

28 

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

30 # protocols -- breaks ghostwriter tests 

31 class RandomLike(Protocol): 

32 def seed(self, *args: Any, **kwargs: Any) -> Any: ... 

33 def getstate(self, *args: Any, **kwargs: Any) -> Any: ... 

34 def setstate(self, *args: Any, **kwargs: Any) -> Any: ... 

35 

36else: # pragma: no cover 

37 RandomLike = random.Random 

38 

39_RKEY = count() 

40_global_random_rkey = next(_RKEY) 

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

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

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

44RANDOMS_TO_MANAGE: WeakValueDictionary[int, RandomLike] = WeakValueDictionary( 

45 {_global_random_rkey: random} 

46) 

47 

48 

49class NumpyRandomWrapper: 

50 def __init__(self) -> None: 

51 assert "numpy" in sys.modules 

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

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

54 import numpy.random 

55 

56 self.seed = numpy.random.seed 

57 self.getstate = numpy.random.get_state 

58 self.setstate = numpy.random.set_state 

59 

60 

61NP_RANDOM: Optional[RandomLike] = None 

62 

63 

64if not (PYPY or GRAALPY): 

65 

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

67 return sys.getrefcount(r) 

68 

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

70 # the given platform / version of Python. 

71 _PLATFORM_REF_COUNT = _get_platform_base_refcount(object()) 

72else: # pragma: no cover 

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

74 _PLATFORM_REF_COUNT = -1 

75 

76 

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

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

79 Hypothesis. 

80 

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

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

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

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

85 

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

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

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

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

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

91 

92 ``register_random`` only makes `weakrefs 

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

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

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

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

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

98 an illustration of this issue 

99 

100 .. code-block:: python 

101 

102 

103 def my_BROKEN_hook(): 

104 r = MyRandomLike() 

105 

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

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

108 register_random(r) # Hypothesis will emit a warning 

109 

110 

111 rng = MyRandomLike() 

112 

113 

114 def my_WORKING_hook(): 

115 register_random(rng) 

116 """ 

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

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

119 

120 if r in [ 

121 random 

122 for ref in RANDOMS_TO_MANAGE.data.copy().values() # type: ignore 

123 if (random := ref()) is not None 

124 ]: 

125 return 

126 

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

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

129 gc.collect() 

130 if not gc.get_referrers(r): 

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

132 raise ReferenceError( 

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

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

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

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

137 "details." 

138 ) 

139 elif not FREE_THREADED_CPYTHON: # pragma: no branch 

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

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

142 # and objects are immortalized in the Python 3.13 

143 # free-threading implementation at runtime. 

144 

145 warnings.warn( 

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

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

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

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

150 HypothesisWarning, 

151 stacklevel=2, 

152 ) 

153 

154 RANDOMS_TO_MANAGE[next(_RKEY)] = r 

155 

156 

157# Used to make the warning issued by `deprecate_random_in_strategy` thread-safe, 

158# as well as to avoid warning on uses of st.randoms(). 

159# Store just the hash to reduce memory consumption. This is an underapproximation 

160# of membership (distinct items might have the same hash), which is fine for the 

161# warning, as it results in missed alarms, not false alarms. 

162_known_random_state_hashes: set[Any] = set() 

163 

164 

165def get_seeder_and_restorer( 

166 seed: Hashable = 0, 

167) -> tuple[Callable[[], None], Callable[[], None]]: 

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

169 the state of all registered PRNGs. 

170 

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

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

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

174 to force determinism on simulation or scheduling frameworks which avoid 

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

176 """ 

177 assert isinstance(seed, int) 

178 assert 0 <= seed < 2**32 

179 states: dict[int, object] = {} 

180 

181 if "numpy" in sys.modules: 

182 global NP_RANDOM 

183 if NP_RANDOM is None: 

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

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

186 

187 def seed_all() -> None: 

188 assert not states 

189 # access .data.copy().items() instead of .items() to avoid a "dictionary 

190 # changed size during iteration" error under multithreading. 

191 # 

192 # I initially expected this to be fixed by 

193 # https://github.com/python/cpython/commit/96d37dbcd23e65a7a57819aeced9034296ef747e, 

194 # but I believe that is addressing the size change from weakrefs expiring 

195 # during gc, not from the user adding new elements to the dict. 

196 # 

197 # Since we're accessing .data, we have to manually handle checking for 

198 # expired ref instances during iteration. Normally WeakValueDictionary 

199 # handles this for us. 

200 # 

201 # This command reproduces at time of writing: 

202 # pytest hypothesis-python/tests/ -k test_intervals_are_equivalent_to_their_lists 

203 # --parallel-threads 2 

204 for k, ref in RANDOMS_TO_MANAGE.data.copy().items(): # type: ignore 

205 r = ref() 

206 if r is None: 

207 # ie the random instance has been gc'd 

208 continue # pragma: no cover 

209 states[k] = r.getstate() 

210 if k == _global_random_rkey: 

211 # r.seed sets the random's state. We want to add that state to 

212 # _known_random_states before calling r.seed, in case a thread 

213 # switch occurs between the two. To figure out the seed -> state 

214 # mapping, set the seed on a dummy random and add that state to 

215 # _known_random_state. 

216 # 

217 # we could use a global dummy random here, but then we'd have to 

218 # put a lock around it, and it's not clear to me if that's more 

219 # efficient than constructing a new instance each time. 

220 dummy_random = Random() 

221 dummy_random.seed(seed) 

222 _known_random_state_hashes.add(hash(dummy_random.getstate())) 

223 # we expect `assert r.getstate() == dummy_random.getstate()` to 

224 # hold here, but thread switches means it might not. 

225 

226 r.seed(seed) 

227 

228 def restore_all() -> None: 

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

230 r = RANDOMS_TO_MANAGE.get(k) 

231 if r is None: # i.e., has been garbage-collected 

232 continue 

233 

234 if k == _global_random_rkey: 

235 _known_random_state_hashes.add(hash(state)) 

236 r.setstate(state) 

237 

238 states.clear() 

239 

240 return seed_all, restore_all 

241 

242 

243@contextlib.contextmanager 

244def deterministic_PRNG(seed: int = 0) -> Generator[None, None, None]: 

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

246 

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

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

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

250 in practice. 

251 """ 

252 if ( 

253 hypothesis.core.threadlocal._hypothesis_global_random is None 

254 ): # pragma: no cover 

255 hypothesis.core.threadlocal._hypothesis_global_random = Random() 

256 register_random(hypothesis.core.threadlocal._hypothesis_global_random) 

257 

258 seed_all, restore_all = get_seeder_and_restorer(seed) 

259 seed_all() 

260 try: 

261 yield 

262 finally: 

263 restore_all() 

264 # TODO it would be nice to clean up _known_random_state_hashes when no 

265 # active deterministic_PRNG contexts remain, to free memory (see similar 

266 # logic in StackframeLimiter). But it's a bit annoying to get right, and 

267 # likely not a big deal.