Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.10/site-packages/hypothesis/control.py: 45%

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

152 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 inspect 

12import math 

13import random 

14from collections import defaultdict 

15from collections.abc import Sequence 

16from contextlib import contextmanager 

17from typing import Any, Callable, NoReturn, Optional, Union 

18from weakref import WeakKeyDictionary 

19 

20from hypothesis import Verbosity, settings 

21from hypothesis._settings import note_deprecation 

22from hypothesis.errors import InvalidArgument, UnsatisfiedAssumption 

23from hypothesis.internal.compat import BaseExceptionGroup 

24from hypothesis.internal.conjecture.data import ConjectureData 

25from hypothesis.internal.observability import TESTCASE_CALLBACKS 

26from hypothesis.internal.reflection import get_pretty_function_description 

27from hypothesis.internal.validation import check_type 

28from hypothesis.reporting import report, verbose_report 

29from hypothesis.utils.dynamicvariables import DynamicVariable 

30from hypothesis.vendor.pretty import IDKey, PrettyPrintFunction, pretty 

31 

32 

33def _calling_function_location(what: str, frame: Any) -> str: 

34 where = frame.f_back 

35 return f"{what}() in {where.f_code.co_name} (line {where.f_lineno})" 

36 

37 

38def reject() -> NoReturn: 

39 if _current_build_context.value is None: 

40 note_deprecation( 

41 "Using `reject` outside a property-based test is deprecated", 

42 since="2023-09-25", 

43 has_codemod=False, 

44 ) 

45 where = _calling_function_location("reject", inspect.currentframe()) 

46 if currently_in_test_context(): 

47 count = current_build_context().data._observability_predicates[where] 

48 count["unsatisfied"] += 1 

49 raise UnsatisfiedAssumption(where) 

50 

51 

52def assume(condition: object) -> bool: 

53 """Calling ``assume`` is like an :ref:`assert <python:assert>` that marks 

54 the example as bad, rather than failing the test. 

55 

56 This allows you to specify properties that you *assume* will be 

57 true, and let Hypothesis try to avoid similar examples in future. 

58 """ 

59 if _current_build_context.value is None: 

60 note_deprecation( 

61 "Using `assume` outside a property-based test is deprecated", 

62 since="2023-09-25", 

63 has_codemod=False, 

64 ) 

65 if TESTCASE_CALLBACKS or not condition: 

66 where = _calling_function_location("assume", inspect.currentframe()) 

67 if TESTCASE_CALLBACKS and currently_in_test_context(): 

68 predicates = current_build_context().data._observability_predicates 

69 predicates[where]["satisfied" if condition else "unsatisfied"] += 1 

70 if not condition: 

71 raise UnsatisfiedAssumption(f"failed to satisfy {where}") 

72 return True 

73 

74 

75_current_build_context = DynamicVariable[Optional["BuildContext"]](None) 

76 

77 

78def currently_in_test_context() -> bool: 

79 """Return ``True`` if the calling code is currently running inside an 

80 :func:`@given <hypothesis.given>` or :ref:`stateful <stateful>` test, 

81 ``False`` otherwise. 

82 

83 This is useful for third-party integrations and assertion helpers which 

84 may be called from traditional or property-based tests, but can only use 

85 :func:`~hypothesis.assume` or :func:`~hypothesis.target` in the latter case. 

86 """ 

87 return _current_build_context.value is not None 

88 

89 

90def current_build_context() -> "BuildContext": 

91 context = _current_build_context.value 

92 if context is None: 

93 raise InvalidArgument("No build context registered") 

94 return context 

95 

96 

97class RandomSeeder: 

98 def __init__(self, seed): 

99 self.seed = seed 

100 

101 def __repr__(self): 

102 return f"RandomSeeder({self.seed!r})" 

103 

104 

105class _Checker: 

106 def __init__(self) -> None: 

107 self.saw_global_random = False 

108 

109 def __call__(self, x): 

110 self.saw_global_random |= isinstance(x, RandomSeeder) 

111 return x 

112 

113 

114@contextmanager 

115def deprecate_random_in_strategy(fmt, *args): 

116 _global_rand_state = random.getstate() 

117 yield (checker := _Checker()) 

118 if _global_rand_state != random.getstate() and not checker.saw_global_random: 

119 # raise InvalidDefinition 

120 note_deprecation( 

121 "Do not use the `random` module inside strategies; instead " 

122 "consider `st.randoms()`, `st.sampled_from()`, etc. " + fmt.format(*args), 

123 since="2024-02-05", 

124 has_codemod=False, 

125 stacklevel=1, 

126 ) 

127 

128 

129class BuildContext: 

130 def __init__( 

131 self, 

132 data: ConjectureData, 

133 *, 

134 is_final: bool = False, 

135 ) -> None: 

136 self.data = data 

137 self.tasks: list[Callable[[], Any]] = [] 

138 self.is_final = is_final 

139 

140 # Use defaultdict(list) here to handle the possibility of having multiple 

141 # functions registered for the same object (due to caching, small ints, etc). 

142 # The printer will discard duplicates which return different representations. 

143 self.known_object_printers: dict[IDKey, list[PrettyPrintFunction]] = ( 

144 defaultdict(list) 

145 ) 

146 

147 def record_call( 

148 self, 

149 obj: object, 

150 func: object, 

151 args: Sequence[object], 

152 kwargs: dict[str, object], 

153 ) -> None: 

154 self.known_object_printers[IDKey(obj)].append( 

155 # _func=func prevents mypy from inferring lambda type. Would need 

156 # paramspec I think - not worth it. 

157 lambda obj, p, cycle, *, _func=func: p.maybe_repr_known_object_as_call( # type: ignore 

158 obj, cycle, get_pretty_function_description(_func), args, kwargs 

159 ) 

160 ) 

161 

162 def prep_args_kwargs_from_strategies(self, kwarg_strategies): 

163 arg_labels = {} 

164 kwargs = {} 

165 for k, s in kwarg_strategies.items(): 

166 start_idx = len(self.data.nodes) 

167 with deprecate_random_in_strategy("from {}={!r}", k, s) as check: 

168 obj = check(self.data.draw(s, observe_as=f"generate:{k}")) 

169 end_idx = len(self.data.nodes) 

170 kwargs[k] = obj 

171 

172 # This high up the stack, we can't see or really do much with the conjecture 

173 # Example objects - not least because they're only materialized after the 

174 # test case is completed. Instead, we'll stash the (start_idx, end_idx) 

175 # pair on our data object for the ConjectureRunner engine to deal with, and 

176 # pass a dict of such out so that the pretty-printer knows where to place 

177 # the which-parts-matter comments later. 

178 if start_idx != end_idx: 

179 arg_labels[k] = (start_idx, end_idx) 

180 self.data.arg_slices.add((start_idx, end_idx)) 

181 

182 return kwargs, arg_labels 

183 

184 def __enter__(self): 

185 self.assign_variable = _current_build_context.with_value(self) 

186 self.assign_variable.__enter__() 

187 return self 

188 

189 def __exit__(self, exc_type, exc_value, tb): 

190 self.assign_variable.__exit__(exc_type, exc_value, tb) 

191 errors = [] 

192 for task in self.tasks: 

193 try: 

194 task() 

195 except BaseException as err: 

196 errors.append(err) 

197 if errors: 

198 if len(errors) == 1: 

199 raise errors[0] from exc_value 

200 raise BaseExceptionGroup("Cleanup failed", errors) from exc_value 

201 

202 

203def cleanup(teardown): 

204 """Register a function to be called when the current test has finished 

205 executing. Any exceptions thrown in teardown will be printed but not 

206 rethrown. 

207 

208 Inside a test this isn't very interesting, because you can just use 

209 a finally block, but note that you can use this inside map, flatmap, 

210 etc. in order to e.g. insist that a value is closed at the end. 

211 """ 

212 context = _current_build_context.value 

213 if context is None: 

214 raise InvalidArgument("Cannot register cleanup outside of build context") 

215 context.tasks.append(teardown) 

216 

217 

218def should_note(): 

219 context = _current_build_context.value 

220 if context is None: 

221 raise InvalidArgument("Cannot make notes outside of a test") 

222 return context.is_final or settings.default.verbosity >= Verbosity.verbose 

223 

224 

225def note(value: object) -> None: 

226 """Report this value for the minimal failing example.""" 

227 if should_note(): 

228 if not isinstance(value, str): 

229 value = pretty(value) 

230 report(value) 

231 

232 

233def event(value: str, payload: Union[str, int, float] = "") -> None: 

234 """Record an event that occurred during this test. Statistics on the number of test 

235 runs with each event will be reported at the end if you run Hypothesis in 

236 statistics reporting mode. 

237 

238 Event values should be strings or convertible to them. If an optional 

239 payload is given, it will be included in the string for :ref:`statistics`. 

240 """ 

241 context = _current_build_context.value 

242 if context is None: 

243 raise InvalidArgument("Cannot make record events outside of a test") 

244 

245 payload = _event_to_string(payload, (str, int, float)) 

246 context.data.events[_event_to_string(value)] = payload 

247 

248 

249_events_to_strings: WeakKeyDictionary = WeakKeyDictionary() 

250 

251 

252def _event_to_string(event, allowed_types=str): 

253 if isinstance(event, allowed_types): 

254 return event 

255 try: 

256 return _events_to_strings[event] 

257 except (KeyError, TypeError): 

258 pass 

259 result = str(event) 

260 try: 

261 _events_to_strings[event] = result 

262 except TypeError: 

263 pass 

264 return result 

265 

266 

267def target(observation: Union[int, float], *, label: str = "") -> Union[int, float]: 

268 """Calling this function with an ``int`` or ``float`` observation gives it feedback 

269 with which to guide our search for inputs that will cause an error, in 

270 addition to all the usual heuristics. Observations must always be finite. 

271 

272 Hypothesis will try to maximize the observed value over several examples; 

273 almost any metric will work so long as it makes sense to increase it. 

274 For example, ``-abs(error)`` is a metric that increases as ``error`` 

275 approaches zero. 

276 

277 Example metrics: 

278 

279 - Number of elements in a collection, or tasks in a queue 

280 - Mean or maximum runtime of a task (or both, if you use ``label``) 

281 - Compression ratio for data (perhaps per-algorithm or per-level) 

282 - Number of steps taken by a state machine 

283 

284 The optional ``label`` argument can be used to distinguish between 

285 and therefore separately optimise distinct observations, such as the 

286 mean and standard deviation of a dataset. It is an error to call 

287 ``target()`` with any label more than once per test case. 

288 

289 .. note:: 

290 The more examples you run, the better this technique works. 

291 

292 As a rule of thumb, the targeting effect is noticeable above 

293 :obj:`max_examples=1000 <hypothesis.settings.max_examples>`, 

294 and immediately obvious by around ten thousand examples 

295 *per label* used by your test. 

296 

297 :ref:`statistics` include the best score seen for each label, 

298 which can help avoid `the threshold problem 

299 <https://hypothesis.works/articles/threshold-problem/>`__ when the minimal 

300 example shrinks right down to the threshold of failure (:issue:`2180`). 

301 """ 

302 check_type((int, float), observation, "observation") 

303 if not math.isfinite(observation): 

304 raise InvalidArgument(f"{observation=} must be a finite float.") 

305 check_type(str, label, "label") 

306 

307 context = _current_build_context.value 

308 if context is None: 

309 raise InvalidArgument( 

310 "Calling target() outside of a test is invalid. " 

311 "Consider guarding this call with `if currently_in_test_context(): ...`" 

312 ) 

313 elif context.data.provider.avoid_realization: 

314 # We could in principle realize this in the engine, but it seems more 

315 # efficient to have our alternative backend optimize it for us. 

316 # See e.g. https://github.com/pschanely/hypothesis-crosshair/issues/3 

317 return observation # pragma: no cover 

318 verbose_report(f"Saw target({observation!r}, {label=})") 

319 

320 if label in context.data.target_observations: 

321 raise InvalidArgument( 

322 f"Calling target({observation!r}, {label=}) would overwrite " 

323 f"target({context.data.target_observations[label]!r}, {label=})" 

324 ) 

325 else: 

326 context.data.target_observations[label] = observation 

327 

328 return observation