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

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

160 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 Callable, Generator, Sequence 

16from contextlib import contextmanager 

17from types import TracebackType 

18from typing import Any, Literal, NoReturn, Optional, Self, overload 

19from weakref import WeakKeyDictionary 

20 

21from hypothesis import Verbosity, settings 

22from hypothesis.errors import InvalidArgument, UnsatisfiedAssumption 

23from hypothesis.internal.conjecture.data import ConjectureData 

24from hypothesis.internal.observability import observability_enabled 

25from hypothesis.internal.reflection import get_pretty_function_description 

26from hypothesis.internal.validation import check_type 

27from hypothesis.reporting import report, verbose_report 

28from hypothesis.utils.deprecation import note_deprecation 

29from hypothesis.utils.dynamicvariables import DynamicVariable 

30from hypothesis.vendor.pretty import ArgLabelsT, 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 _calling_frame_location(frame: Any) -> str: 

39 where = frame.f_back 

40 return f"{where.f_code.co_filename}:{where.f_lineno}" 

41 

42 

43def reject() -> NoReturn: 

44 if _current_build_context.value is None: 

45 note_deprecation( 

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

47 since="2023-09-25", 

48 has_codemod=False, 

49 ) 

50 frame = inspect.currentframe() 

51 where = _calling_function_location("reject", frame) 

52 if currently_in_test_context(): 

53 counts = current_build_context().data._observability_predicates[where] 

54 counts.update_count(condition=False) 

55 raise UnsatisfiedAssumption(where, location=_calling_frame_location(frame)) 

56 

57 

58@overload 

59def assume(condition: Literal[False] | None) -> NoReturn: ... 

60@overload 

61def assume(condition: object) -> Literal[True]: ... 

62 

63 

64def assume(condition: object) -> Literal[True]: 

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

66 the |test case| as bad, rather than failing the test. 

67 

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

69 true, and let Hypothesis try to avoid similar test cases in future. 

70 """ 

71 if _current_build_context.value is None: 

72 note_deprecation( 

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

74 since="2023-09-25", 

75 has_codemod=False, 

76 ) 

77 if observability_enabled() or not condition: 

78 frame = inspect.currentframe() 

79 where = _calling_function_location("assume", frame) 

80 if observability_enabled() and currently_in_test_context(): 

81 counts = current_build_context().data._observability_predicates[where] 

82 counts.update_count(condition=bool(condition)) 

83 if not condition: 

84 raise UnsatisfiedAssumption( 

85 f"failed to satisfy {where}", 

86 location=_calling_frame_location(frame), 

87 ) 

88 return True 

89 

90 

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

92 

93 

94def currently_in_test_context() -> bool: 

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

96 |@given| or :ref:`stateful <stateful>` test, and ``False`` otherwise. 

97 

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

99 may be called from either traditional or property-based tests, and can only 

100 use e.g. |assume| or |target| in the latter case. 

101 """ 

102 return _current_build_context.value is not None 

103 

104 

105def current_build_context() -> "BuildContext": 

106 context = _current_build_context.value 

107 if context is None: 

108 raise InvalidArgument("No build context registered") 

109 return context 

110 

111 

112@contextmanager 

113def deprecate_random_in_strategy(fmt: str, *args: Any) -> Generator[None, None, None]: 

114 from hypothesis.internal import entropy 

115 

116 state_before = random.getstate() 

117 yield 

118 state_after = random.getstate() 

119 if ( 

120 # there is a threading race condition here with deterministic_PRNG. Say 

121 # we have two threads 1 and 2. We start in global random state A, and 

122 # deterministic_PRNG sets to global random state B (which is constant across 

123 # threads since we seed to 0 unconditionally). Then we might have state 

124 # transitions: 

125 # 

126 # [1] [2] 

127 # A -> B deterministic_PRNG().__enter__ 

128 # B ->B deterministic_PRNG().__enter__ 

129 # state_before = B deprecate_random_in_strategy.__enter__ 

130 # B -> A deterministic_PRNG().__exit__ 

131 # state_after = A deprecate_random_in_strategy.__exit__ 

132 # 

133 # where state_before != state_after because a different thread has reset 

134 # the global random state. 

135 # 

136 # To fix this, we track the known random states set by deterministic_PRNG, 

137 # and will not note a deprecation if it matches one of those. 

138 state_after != state_before 

139 and hash(state_after) not in entropy._known_random_state_hashes 

140 ): 

141 note_deprecation( 

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

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

144 since="2024-02-05", 

145 has_codemod=False, 

146 stacklevel=1, 

147 ) 

148 

149 

150class BuildContext: 

151 def __init__( 

152 self, 

153 data: ConjectureData, 

154 *, 

155 is_final: bool = False, 

156 wrapped_test: Callable, 

157 ) -> None: 

158 self.data = data 

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

160 self.is_final = is_final 

161 self.wrapped_test = wrapped_test 

162 

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

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

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

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

167 defaultdict(list) 

168 ) 

169 

170 def record_call( 

171 self, 

172 obj: object, 

173 func: object, 

174 *, 

175 args: Sequence[object], 

176 kwargs: dict[str, object], 

177 arg_labels: ArgLabelsT | None = None, 

178 ) -> None: 

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

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

181 obj, 

182 cycle, 

183 get_pretty_function_description(_func), 

184 args, 

185 kwargs, 

186 arg_labels=_arg_labels, 

187 ) 

188 ) 

189 

190 def prep_args_kwargs_from_strategies( 

191 self, 

192 kwarg_strategies: dict[str, Any], 

193 ) -> tuple[dict[str, Any], ArgLabelsT]: 

194 arg_labels: ArgLabelsT = {} 

195 kwargs: dict[str, Any] = {} 

196 

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

198 with ( 

199 self.data.track_arg_label(k) as arg_label, 

200 deprecate_random_in_strategy("from {}={!r}", k, s), 

201 ): 

202 kwargs[k] = self.data.draw(s, observe_as=f"generate:{k}") 

203 arg_labels |= arg_label 

204 

205 return kwargs, arg_labels 

206 

207 def __enter__(self) -> "Self": 

208 self.assign_variable = _current_build_context.with_value(self) 

209 self.assign_variable.__enter__() 

210 return self 

211 

212 def __exit__( 

213 self, 

214 exc_type: type[BaseException] | None, 

215 exc_value: BaseException | None, 

216 tb: TracebackType | None, 

217 ) -> None: 

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

219 errors = [] 

220 for task in self.tasks: 

221 try: 

222 task() 

223 except BaseException as err: 

224 errors.append(err) 

225 if errors: 

226 if len(errors) == 1: 

227 raise errors[0] from exc_value 

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

229 

230 

231def cleanup(teardown: Callable[[], Any]) -> None: 

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

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

234 rethrown. 

235 

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

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

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

239 """ 

240 context = _current_build_context.value 

241 if context is None: 

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

243 context.tasks.append(teardown) 

244 

245 

246def should_note() -> bool: 

247 context = _current_build_context.value 

248 if context is None: 

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

250 assert settings.default is not None 

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

252 

253 

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

255 """ 

256 Record a note on this |test case|. Non-string values will be automatically converted 

257 to a string. 

258 

259 This value is reported for the |minimal failing test case|, and on |Verbosity.verbose| 

260 or higher. 

261 

262 Notes are also recorded in the ``metadata.notes`` key of each test case 

263 observation, if :ref:`observability <observability>` is enabled. 

264 """ 

265 should_report = should_note() 

266 if should_report or observability_enabled(): 

267 if not isinstance(value, str): 

268 value = pretty(value) 

269 context = _current_build_context.value 

270 assert context is not None 

271 if observability_enabled(): 

272 context.data.note(value) 

273 if should_report: 

274 report(value) 

275 

276 

277def event(value: str, payload: Any = "") -> None: 

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

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

280 statistics reporting mode. 

281 

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

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

284 """ 

285 context = _current_build_context.value 

286 if context is None: 

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

288 

289 avoid_realization = context.data.provider.avoid_realization 

290 payload = _serialize_event( 

291 payload, allowed_types=(str, int, float), avoid_realization=avoid_realization 

292 ) 

293 value = _serialize_event(value, avoid_realization=avoid_realization) 

294 context.data.events[value] = payload 

295 

296 

297_events_to_strings: WeakKeyDictionary[Any, str] = WeakKeyDictionary() 

298 

299 

300def _serialize_event( 

301 event: Any, *, allowed_types: tuple[type, ...] = (str,), avoid_realization: bool 

302) -> Any: 

303 if isinstance(event, allowed_types): 

304 return event 

305 

306 # _events_to_strings is a cache which persists across iterations, causing 

307 # problems for symbolic backends. see 

308 # https://github.com/pschanely/hypothesis-crosshair/issues/41 

309 if avoid_realization: 

310 return str(event) 

311 

312 try: 

313 return _events_to_strings[event] 

314 except (KeyError, TypeError): 

315 pass 

316 

317 result = str(event) 

318 try: 

319 _events_to_strings[event] = result 

320 except TypeError: 

321 pass 

322 return result 

323 

324 

325def target(observation: int | float, *, label: str = "") -> int | float: 

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

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

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

329 

330 Hypothesis will try to maximize the observed value over several |test cases|; 

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

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

333 approaches zero. 

334 

335 Example metrics: 

336 

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

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

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

340 - Number of steps taken by a state machine 

341 

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

343 and therefore separately optimise distinct observations, such as the 

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

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

346 

347 .. note:: 

348 The more test cases you run, the better this technique works. 

349 

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

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

352 and immediately obvious by around ten thousand test cases 

353 *per label* used by your test. 

354 

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

356 which can help avoid `the threshold problem 

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

358 test case shrinks right down to the threshold of failure (:issue:`2180`). 

359 """ 

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

361 if not math.isfinite(observation): 

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

363 check_type(str, label, "label") 

364 

365 context = _current_build_context.value 

366 if context is None: 

367 raise InvalidArgument( 

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

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

370 ) 

371 elif context.data.provider.avoid_realization: 

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

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

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

375 return observation # pragma: no cover 

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

377 

378 if label in context.data.target_observations: 

379 raise InvalidArgument( 

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

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

382 ) 

383 else: 

384 context.data.target_observations[label] = observation 

385 

386 return observation