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

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

169 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 TYPE_CHECKING, Any, Literal, NoReturn, Optional, overload 

19from weakref import WeakKeyDictionary 

20 

21from hypothesis import Verbosity, settings 

22from hypothesis.errors import InvalidArgument, UnsatisfiedAssumption 

23from hypothesis.internal.compat import BaseExceptionGroup 

24from hypothesis.internal.conjecture.data import ConjectureData 

25from hypothesis.internal.observability import observability_enabled 

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.deprecation import note_deprecation 

30from hypothesis.utils.dynamicvariables import DynamicVariable 

31from hypothesis.vendor.pretty import ArgLabelsT, IDKey, PrettyPrintFunction, pretty 

32 

33if TYPE_CHECKING: 

34 from typing_extensions import Self 

35 

36 

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

38 where = frame.f_back 

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

40 

41 

42def reject() -> NoReturn: 

43 if _current_build_context.value is None: 

44 note_deprecation( 

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

46 since="2023-09-25", 

47 has_codemod=False, 

48 ) 

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

50 if currently_in_test_context(): 

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

52 counts.update_count(condition=False) 

53 raise UnsatisfiedAssumption(where) 

54 

55 

56@overload 

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

58@overload 

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

60 

61 

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

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

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

65 

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

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

68 """ 

69 if _current_build_context.value is None: 

70 note_deprecation( 

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

72 since="2023-09-25", 

73 has_codemod=False, 

74 ) 

75 if observability_enabled() or not condition: 

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

77 if observability_enabled() and currently_in_test_context(): 

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

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

80 if not condition: 

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

82 return True 

83 

84 

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

86 

87 

88def currently_in_test_context() -> bool: 

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

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

91 

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

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

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

95 """ 

96 return _current_build_context.value is not None 

97 

98 

99def current_build_context() -> "BuildContext": 

100 context = _current_build_context.value 

101 if context is None: 

102 raise InvalidArgument("No build context registered") 

103 return context 

104 

105 

106@contextmanager 

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

108 from hypothesis.internal import entropy 

109 

110 state_before = random.getstate() 

111 yield 

112 state_after = random.getstate() 

113 if ( 

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

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

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

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

118 # transitions: 

119 # 

120 # [1] [2] 

121 # A -> B deterministic_PRNG().__enter__ 

122 # B ->B deterministic_PRNG().__enter__ 

123 # state_before = B deprecate_random_in_strategy.__enter__ 

124 # B -> A deterministic_PRNG().__exit__ 

125 # state_after = A deprecate_random_in_strategy.__exit__ 

126 # 

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

128 # the global random state. 

129 # 

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

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

132 state_after != state_before 

133 and hash(state_after) not in entropy._known_random_state_hashes 

134 ): 

135 note_deprecation( 

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

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

138 since="2024-02-05", 

139 has_codemod=False, 

140 stacklevel=1, 

141 ) 

142 

143 

144class BuildContext: 

145 def __init__( 

146 self, 

147 data: ConjectureData, 

148 *, 

149 is_final: bool = False, 

150 wrapped_test: Callable, 

151 ) -> None: 

152 self.data = data 

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

154 self.is_final = is_final 

155 self.wrapped_test = wrapped_test 

156 

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

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

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

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

161 defaultdict(list) 

162 ) 

163 

164 # Track nested strategy calls for explain-phase label paths 

165 self._label_path: list[str] = [] 

166 

167 @contextmanager 

168 def track_arg_label(self, label: str) -> Generator[ArgLabelsT, None, None]: 

169 span_index = self.data.next_span_index 

170 self._label_path.append(label) 

171 arg_labels: ArgLabelsT = {} 

172 try: 

173 yield arg_labels 

174 finally: 

175 self._label_path.pop() 

176 

177 # The draw inside this block opened a span, whose index we knew in 

178 # advance even though Span objects are only materialized after the 

179 # test case is completed. Stash that index on our data object for 

180 # the shrinker's explain phase to vary, and mutate the arg_labels 

181 # dict so that the pretty-printer knows where to place the 

182 # which-parts-matter comments later. (If the draw raised instead, 

183 # we skip recording, along with the rest of the test case.) 

184 arg_labels[label] = span_index 

185 self.data.arg_spans.add(span_index) 

186 

187 def record_call( 

188 self, 

189 obj: object, 

190 func: object, 

191 *, 

192 args: Sequence[object], 

193 kwargs: dict[str, object], 

194 arg_labels: ArgLabelsT | None = None, 

195 ) -> None: 

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

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

198 obj, 

199 cycle, 

200 get_pretty_function_description(_func), 

201 args, 

202 kwargs, 

203 arg_labels=_arg_labels, 

204 ) 

205 ) 

206 

207 def prep_args_kwargs_from_strategies( 

208 self, 

209 kwarg_strategies: dict[str, Any], 

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

211 arg_labels: ArgLabelsT = {} 

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

213 

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

215 with ( 

216 self.track_arg_label(k) as arg_label, 

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

218 ): 

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

220 arg_labels |= arg_label 

221 

222 return kwargs, arg_labels 

223 

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

225 self.assign_variable = _current_build_context.with_value(self) 

226 self.assign_variable.__enter__() 

227 return self 

228 

229 def __exit__( 

230 self, 

231 exc_type: type[BaseException] | None, 

232 exc_value: BaseException | None, 

233 tb: TracebackType | None, 

234 ) -> None: 

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

236 errors = [] 

237 for task in self.tasks: 

238 try: 

239 task() 

240 except BaseException as err: 

241 errors.append(err) 

242 if errors: 

243 if len(errors) == 1: 

244 raise errors[0] from exc_value 

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

246 

247 

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

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

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

251 rethrown. 

252 

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

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

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

256 """ 

257 context = _current_build_context.value 

258 if context is None: 

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

260 context.tasks.append(teardown) 

261 

262 

263def should_note() -> bool: 

264 context = _current_build_context.value 

265 if context is None: 

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

267 assert settings.default is not None 

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

269 

270 

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

272 """ 

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

274 to a string. 

275 

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

277 or higher. 

278 

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

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

281 """ 

282 should_report = should_note() 

283 if should_report or observability_enabled(): 

284 if not isinstance(value, str): 

285 value = pretty(value) 

286 context = _current_build_context.value 

287 assert context is not None 

288 if observability_enabled(): 

289 context.data.note(value) 

290 if should_report: 

291 report(value) 

292 

293 

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

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

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

297 statistics reporting mode. 

298 

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

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

301 """ 

302 context = _current_build_context.value 

303 if context is None: 

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

305 

306 avoid_realization = context.data.provider.avoid_realization 

307 payload = _serialize_event( 

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

309 ) 

310 value = _serialize_event(value, avoid_realization=avoid_realization) 

311 context.data.events[value] = payload 

312 

313 

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

315 

316 

317def _serialize_event( 

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

319) -> Any: 

320 if isinstance(event, allowed_types): 

321 return event 

322 

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

324 # problems for symbolic backends. see 

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

326 if avoid_realization: 

327 return str(event) 

328 

329 try: 

330 return _events_to_strings[event] 

331 except (KeyError, TypeError): 

332 pass 

333 

334 result = str(event) 

335 try: 

336 _events_to_strings[event] = result 

337 except TypeError: 

338 pass 

339 return result 

340 

341 

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

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

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

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

346 

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

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

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

350 approaches zero. 

351 

352 Example metrics: 

353 

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

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

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

357 - Number of steps taken by a state machine 

358 

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

360 and therefore separately optimise distinct observations, such as the 

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

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

363 

364 .. note:: 

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

366 

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

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

369 and immediately obvious by around ten thousand test cases 

370 *per label* used by your test. 

371 

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

373 which can help avoid `the threshold problem 

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

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

376 """ 

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

378 if not math.isfinite(observation): 

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

380 check_type(str, label, "label") 

381 

382 context = _current_build_context.value 

383 if context is None: 

384 raise InvalidArgument( 

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

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

387 ) 

388 elif context.data.provider.avoid_realization: 

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

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

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

392 return observation # pragma: no cover 

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

394 

395 if label in context.data.target_observations: 

396 raise InvalidArgument( 

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

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

399 ) 

400 else: 

401 context.data.target_observations[label] = observation 

402 

403 return observation