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
11"""A module for miscellaneous useful bits and bobs that don't
12obviously belong anywhere else. If you spot a better home for
13anything that lives here, please move it."""
14
15import array
16import gc
17import itertools
18import sys
19import time
20import warnings
21from array import ArrayType
22from collections.abc import Callable, Iterable, Iterator, Sequence
23from threading import Lock
24from typing import (
25 Any,
26 ClassVar,
27 Generic,
28 Literal,
29 TypeVar,
30 Union,
31 overload,
32)
33
34from hypothesis.errors import HypothesisWarning
35from hypothesis.internal.floats import float_to_int
36
37T = TypeVar("T")
38
39
40def equal_values(a: Any, b: Any) -> bool:
41 """Like ==, but requires identical types (so True != 1 != 1.0) and
42 compares floats by bitpattern (so nan == nan and 0.0 != -0.0), applying
43 both rules elementwise through lists and tuples.
44
45 Values of any other type - dicts, sets, arbitrary objects - are compared
46 by plain == after the type check, so a NaN, signed zero, or bool/int
47 confusion nested inside them is missed: a NaN there compares unequal and
48 a nested True matches 1. Callers use this for best-effort,
49 replay-verified inversion, where such a miss costs a widening opportunity
50 - the replay produces a wrong or unequal value and is rejected - never
51 correctness.
52 """
53 if type(a) is not type(b):
54 return False
55 if isinstance(a, float):
56 # by bitpattern, matching choice_equal
57 return float_to_int(a) == float_to_int(b)
58 if isinstance(a, (list, tuple)):
59 return len(a) == len(b) and all(map(equal_values, a, b))
60 return bool(a == b)
61
62
63def replace_all(
64 ls: Sequence[T],
65 replacements: Iterable[tuple[int, int, Sequence[T]]],
66) -> list[T]:
67 """Substitute multiple replacement values into a list.
68
69 Replacements is a list of (start, end, value) triples.
70 """
71
72 result: list[T] = []
73 prev = 0
74 offset = 0
75 for u, v, r in replacements:
76 result.extend(ls[prev:u])
77 result.extend(r)
78 prev = v
79 offset += len(r) - (v - u)
80 result.extend(ls[prev:])
81 assert len(result) == len(ls) + offset
82 return result
83
84
85class IntList(Sequence[int]):
86 """Class for storing a list of non-negative integers compactly.
87
88 We store them as the smallest size integer array we can get
89 away with. When we try to add an integer that is too large,
90 we upgrade the array to the smallest word size needed to store
91 the new value."""
92
93 ARRAY_CODES: ClassVar[list[str]] = ["B", "H", "I", "L", "Q", "O"]
94 NEXT_ARRAY_CODE: ClassVar[dict[str, str]] = dict(itertools.pairwise(ARRAY_CODES))
95
96 __slots__ = ("__underlying",)
97
98 def __init__(self, values: Sequence[int] = ()):
99 for code in self.ARRAY_CODES:
100 try:
101 underlying = self._array_or_list(code, values)
102 break
103 except OverflowError:
104 pass
105 else: # pragma: no cover
106 raise AssertionError(f"Could not create storage for {values!r}")
107 if isinstance(underlying, list):
108 for v in underlying:
109 if not isinstance(v, int) or v < 0:
110 raise ValueError(f"Could not create IntList for {values!r}")
111 self.__underlying: list[int] | ArrayType[int] = underlying
112
113 @classmethod
114 def of_length(cls, n: int) -> "IntList":
115 return cls(array.array("B", [0]) * n)
116
117 @staticmethod
118 def _array_or_list(
119 code: str, contents: Iterable[int]
120 ) -> Union[list[int], "ArrayType[int]"]:
121 if code == "O":
122 return list(contents)
123 return array.array(code, contents)
124
125 def count(self, value: int) -> int:
126 return self.__underlying.count(value)
127
128 def __repr__(self) -> str:
129 return f"IntList({list(self.__underlying)!r})"
130
131 def __len__(self) -> int:
132 return len(self.__underlying)
133
134 @overload
135 def __getitem__(self, i: int) -> int: ...
136
137 @overload
138 def __getitem__(self, i: slice) -> "list[int] | ArrayType[int]": ...
139
140 def __getitem__(self, i: int | slice) -> "int | list[int] | ArrayType[int]":
141 return self.__underlying[i]
142
143 def __delitem__(self, i: int | slice) -> None:
144 del self.__underlying[i]
145
146 def insert(self, i: int, v: int) -> None:
147 self.__underlying.insert(i, v)
148
149 def __iter__(self) -> Iterator[int]:
150 return iter(self.__underlying)
151
152 def __eq__(self, other: object) -> bool:
153 if self is other:
154 return True
155 if not isinstance(other, IntList):
156 return NotImplemented
157 return self.__underlying == other.__underlying
158
159 def __ne__(self, other: object) -> bool:
160 if self is other:
161 return False
162 if not isinstance(other, IntList):
163 return NotImplemented
164 return self.__underlying != other.__underlying
165
166 def append(self, n: int) -> None:
167 # try the fast path of appending n first. If this overflows, use the
168 # __setitem__ path, which will upgrade the underlying array.
169 try:
170 self.__underlying.append(n)
171 except OverflowError:
172 i = len(self.__underlying)
173 self.__underlying.append(0)
174 self[i] = n
175
176 def __setitem__(self, i: int, n: int) -> None:
177 while True:
178 try:
179 self.__underlying[i] = n
180 return
181 except OverflowError:
182 assert n > 0
183 self.__upgrade()
184
185 def extend(self, ls: Iterable[int]) -> None:
186 for n in ls:
187 self.append(n)
188
189 def __upgrade(self) -> None:
190 assert isinstance(self.__underlying, array.array)
191 code = self.NEXT_ARRAY_CODE[self.__underlying.typecode]
192 self.__underlying = self._array_or_list(code, self.__underlying)
193
194
195def binary_search(lo: int, hi: int, f: Callable[[int], bool]) -> int:
196 """Binary searches in [lo , hi) to find
197 n such that f(n) == f(lo) but f(n + 1) != f(lo).
198 It is implicitly assumed and will not be checked
199 that f(hi) != f(lo).
200 """
201
202 reference = f(lo)
203
204 while lo + 1 < hi:
205 mid = (lo + hi) // 2
206 if f(mid) == reference:
207 lo = mid
208 else:
209 hi = mid
210 return lo
211
212
213class LazySequenceCopy(Generic[T]):
214 """A "copy" of a sequence that works by inserting a mask in front
215 of the underlying sequence, so that you can mutate it without changing
216 the underlying sequence. Effectively behaves as if you could do list(x)
217 in O(1) time. The full list API is not supported yet but there's no reason
218 in principle it couldn't be."""
219
220 def __init__(self, values: Sequence[T]):
221 self.__values = values
222 self.__len = len(values)
223 self.__mask: dict[int, T] | None = None
224 # Indices passed to pop(), in pop order, each in the coordinates that
225 # were current at the time of that pop. See __underlying_index.
226 self.__popped_at: list[int] | None = None
227
228 def __len__(self) -> int:
229 if self.__popped_at is None:
230 return self.__len
231 return self.__len - len(self.__popped_at)
232
233 def pop(self, i: int = -1) -> T:
234 if len(self) == 0:
235 raise IndexError("Cannot pop from empty list")
236 i, u = self.__underlying_index(i)
237
238 v = None
239 if self.__mask is not None:
240 v = self.__mask.pop(u, None)
241 if v is None:
242 v = self.__values[u]
243
244 if self.__popped_at is None:
245 self.__popped_at = []
246 self.__popped_at.append(i)
247 return v
248
249 def swap(self, i: int, j: int) -> None:
250 """Swap the elements ls[i], ls[j]."""
251 if i == j:
252 return
253 self[i], self[j] = self[j], self[i]
254
255 def __getitem__(self, i: int) -> T:
256 _, i = self.__underlying_index(i)
257
258 default = self.__values[i]
259 if self.__mask is None:
260 return default
261 else:
262 return self.__mask.get(i, default)
263
264 def __setitem__(self, i: int, v: T) -> None:
265 _, i = self.__underlying_index(i)
266 if self.__mask is None:
267 self.__mask = {}
268 self.__mask[i] = v
269
270 def __underlying_index(self, i: int) -> tuple[int, int]:
271 # given an index i in the popped representation of the list, compute
272 # its corresponding index u in the underlying list. given
273 # l = [1, 4, 2, 10, 188]
274 # l.pop(3)
275 # l.pop(1)
276 # assert l == [1, 2, 188]
277 #
278 # we want l[i] == self.__values[u], and return both the normalized
279 # (non-negative, bounds-checked) i and u.
280 #
281 # A pop at index p maps each later index x in the popped coordinates
282 # to x + (p <= x) in the coordinates current at the time of that pop,
283 # so applying this from the most recent pop back to the oldest turns i
284 # into an index into the underlying sequence. We add the comparison
285 # as an integer rather than branching on it so that, under
286 # symbolic-execution backends, symbolic indices pass through here
287 # without forcing any solver queries.
288 n = len(self)
289 if i < -n or i >= n:
290 raise IndexError(f"Index {i} out of range [0, {n})")
291 if i < 0:
292 i += n
293 assert 0 <= i < n
294
295 u = i
296 if self.__popped_at is not None:
297 assert len(self.__popped_at) <= len(self.__values)
298 for p in reversed(self.__popped_at):
299 u += p <= u
300 return i, u
301
302 # even though we have len + getitem, mypyc requires iter.
303 def __iter__(self) -> Iterable[T]:
304 for i in range(len(self)):
305 yield self[i]
306
307
308def stack_depth_of_caller() -> int:
309 """Get stack size for caller's frame.
310
311 From https://stackoverflow.com/a/47956089/9297601 , this is a simple
312 but much faster alternative to `len(inspect.stack(0))`. We use it
313 with get/set recursionlimit to make stack overflows non-flaky; see
314 https://github.com/HypothesisWorks/hypothesis/issues/2494 for details.
315 """
316 frame = sys._getframe(2)
317 size = 1
318 while frame:
319 frame = frame.f_back
320 size += 1
321 return size
322
323
324class StackframeLimiter:
325 # StackframeLimiter is used to make the recursion limit warning issued via
326 # ensure_free_stackframes thread-safe. We track the known values we have
327 # passed to sys.setrecursionlimit in _known_limits, and only issue a warning
328 # if sys.getrecursionlimit is not in _known_limits.
329 #
330 # This will always be an under-approximation of when we would ideally issue
331 # this warning, since a non-hypothesis caller could coincidentaly set the
332 # recursion limit to one of our known limits. Currently, StackframeLimiter
333 # resets _known_limits whenever all of the ensure_free_stackframes contexts
334 # have exited. We could increase the power of the warning by tracking a
335 # refcount for each limit, and removing it as soon as the refcount hits zero.
336 # I didn't think this extra complexity is worth the minor power increase for
337 # what is already only a "nice to have" warning.
338
339 def __init__(self):
340 self._active_contexts = 0
341 self._known_limits: set[int] = set()
342 self._original_limit: int | None = None
343
344 def _setrecursionlimit(self, new_limit: int, *, check: bool = True) -> None:
345 if (
346 check
347 and (current_limit := sys.getrecursionlimit()) not in self._known_limits
348 ):
349 warnings.warn(
350 "The recursion limit will not be reset, since it was changed "
351 f"during test execution (from {self._original_limit} to {current_limit}).",
352 HypothesisWarning,
353 stacklevel=4,
354 )
355 return
356
357 self._known_limits.add(new_limit)
358 sys.setrecursionlimit(new_limit)
359
360 def enter_context(self, new_limit: int, *, current_limit: int) -> None:
361 if self._active_contexts == 0:
362 # this is the first context on the stack. Record the true original
363 # limit, to restore later.
364 assert self._original_limit is None
365 self._original_limit = current_limit
366 self._known_limits.add(self._original_limit)
367
368 self._active_contexts += 1
369 self._setrecursionlimit(new_limit)
370
371 def exit_context(self, new_limit: int, *, check: bool = True) -> None:
372 assert self._active_contexts > 0
373 self._active_contexts -= 1
374
375 if self._active_contexts == 0:
376 # this is the last context to exit. Restore the true original
377 # limit and clear our known limits.
378 original_limit = self._original_limit
379 assert original_limit is not None
380 try:
381 self._setrecursionlimit(original_limit, check=check)
382 finally:
383 self._original_limit = None
384 # we want to clear the known limits, but preserve the limit
385 # we just set it to as known.
386 self._known_limits = {original_limit}
387 else:
388 self._setrecursionlimit(new_limit, check=check)
389
390
391_stackframe_limiter = StackframeLimiter()
392_stackframe_limiter_lock = Lock()
393
394
395class ensure_free_stackframes:
396 """Context manager that ensures there are at least N free stackframes (for
397 a reasonable value of N).
398 """
399
400 def __enter__(self) -> None:
401 cur_depth = stack_depth_of_caller()
402 with _stackframe_limiter_lock:
403 self.old_limit = sys.getrecursionlimit()
404 # The default CPython recursionlimit is 1000, but pytest seems to bump
405 # it to 3000 during test execution. Let's make it something reasonable:
406 self.new_limit = cur_depth + 2000
407 # Because we add to the recursion limit, to be good citizens we also
408 # add a check for unbounded recursion. The default limit is typically
409 # 1000/3000, so this can only ever trigger if something really strange
410 # is happening and it's hard to imagine an
411 # intentionally-deeply-recursive use of this code.
412 assert cur_depth <= 1000, (
413 f"Hypothesis would usually add {self.new_limit - self.old_limit} to "
414 f"the stack depth of {self.old_limit} here, but we are already much "
415 "deeper than expected. Aborting now, to avoid extending the stack "
416 "limit in an infinite loop..."
417 )
418 try:
419 _stackframe_limiter.enter_context(
420 self.new_limit, current_limit=self.old_limit
421 )
422 except Exception:
423 # if the stackframe limiter raises a HypothesisWarning (under eg
424 # -Werror), __exit__ is not called, since we errored in __enter__.
425 # Preserve the state of the stackframe limiter by exiting, and
426 # avoid showing a duplicate warning with check=False.
427 _stackframe_limiter.exit_context(self.old_limit, check=False)
428 raise
429
430 def __exit__(self, *args, **kwargs):
431 with _stackframe_limiter_lock:
432 _stackframe_limiter.exit_context(self.old_limit)
433
434
435def find_integer(f: Callable[[int], bool]) -> int:
436 """Finds a (hopefully large) integer such that f(n) is True and f(n + 1) is
437 False.
438
439 f(0) is assumed to be True and will not be checked.
440 """
441 # We first do a linear scan over the small numbers and only start to do
442 # anything intelligent if f(4) is true. This is because it's very hard to
443 # win big when the result is small. If the result is 0 and we try 2 first
444 # then we've done twice as much work as we needed to!
445 for i in range(1, 5):
446 if not f(i):
447 return i - 1
448
449 # We now know that f(4) is true. We want to find some number for which
450 # f(n) is *not* true.
451 # lo is the largest number for which we know that f(lo) is true.
452 lo = 4
453
454 # Exponential probe upwards until we find some value hi such that f(hi)
455 # is not true. Subsequently we maintain the invariant that hi is the
456 # smallest number for which we know that f(hi) is not true.
457 hi = 5
458 while f(hi):
459 lo = hi
460 hi *= 2
461
462 # Now binary search until lo + 1 = hi. At that point we have f(lo) and not
463 # f(lo + 1), as desired..
464 while lo + 1 < hi:
465 mid = (lo + hi) // 2
466 if f(mid):
467 lo = mid
468 else:
469 hi = mid
470 return lo
471
472
473_gc_initialized = False
474_gc_start: float = 0
475_gc_cumulative_time: float = 0
476
477# Since gc_callback potentially runs in test context, and perf_counter
478# might be monkeypatched, we store a reference to the real one.
479_perf_counter = time.perf_counter
480
481
482def gc_cumulative_time() -> float:
483 global _gc_initialized
484
485 # I don't believe we need a lock for the _gc_cumulative_time increment here,
486 # since afaik each gc callback is only executed once when the garbage collector
487 # runs, by the thread which initiated the gc.
488
489 if not _gc_initialized:
490 if hasattr(gc, "callbacks"):
491 # CPython
492 def gc_callback(
493 phase: Literal["start", "stop"], info: dict[str, int]
494 ) -> None:
495 global _gc_start, _gc_cumulative_time
496 try:
497 now = _perf_counter()
498 if phase == "start":
499 _gc_start = now
500 elif phase == "stop" and _gc_start > 0:
501 _gc_cumulative_time += now - _gc_start # pragma: no cover # ??
502 except RecursionError: # pragma: no cover
503 # Avoid flakiness via UnraisableException, which is caught and
504 # warned by pytest. The actual callback (this function) is
505 # validated to never trigger a RecursionError itself when
506 # when called by gc.collect.
507 # Anyway, we should hit the same error on "start"
508 # and "stop", but to ensure we don't get out of sync we just
509 # signal that there is no matching start.
510 _gc_start = 0
511 return
512
513 gc.callbacks.insert(0, gc_callback)
514 elif hasattr(gc, "hooks"): # pragma: no cover # pypy only
515 # PyPy
516 def hook(stats: Any) -> None:
517 global _gc_cumulative_time
518 try:
519 _gc_cumulative_time += stats.duration
520 except RecursionError:
521 pass
522
523 if gc.hooks.on_gc_minor is None:
524 gc.hooks.on_gc_minor = hook
525 if gc.hooks.on_gc_collect_step is None:
526 gc.hooks.on_gc_collect_step = hook
527
528 _gc_initialized = True
529
530 return _gc_cumulative_time
531
532
533def startswith(l1: Sequence[T], l2: Sequence[T]) -> bool:
534 if len(l1) < len(l2):
535 return False
536 return all(v1 == v2 for v1, v2 in zip(l1[: len(l2)], l2, strict=False))
537
538
539def endswith(l1: Sequence[T], l2: Sequence[T]) -> bool:
540 if len(l1) < len(l2):
541 return False
542 return all(v1 == v2 for v1, v2 in zip(l1[-len(l2) :], l2, strict=False))
543
544
545def bits_to_bytes(n: int) -> int:
546 """The number of bytes required to represent an n-bit number.
547 Equivalent to (n + 7) // 8, but slightly faster. This really is
548 called enough times that that matters."""
549 return (n + 7) >> 3