328 lines
9.7 KiB
Python
328 lines
9.7 KiB
Python
# This file is part of Hypothesis, which may be found at
|
|
# https://github.com/HypothesisWorks/hypothesis/
|
|
#
|
|
# Most of this work is copyright (C) 2013-2021 David R. MacIver
|
|
# (david@drmaciver.com), but it contains contributions by others. See
|
|
# CONTRIBUTING.rst for a full list of people who may hold copyright, and
|
|
# consult the git log if you need to determine who owns an individual
|
|
# contribution.
|
|
#
|
|
# This Source Code Form is subject to the terms of the Mozilla Public License,
|
|
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
|
|
# obtain one at https://mozilla.org/MPL/2.0/.
|
|
#
|
|
# END HEADER
|
|
|
|
"""A module for miscellaneous useful bits and bobs that don't
|
|
obviously belong anywhere else. If you spot a better home for
|
|
anything that lives here, please move it."""
|
|
|
|
import array
|
|
import sys
|
|
|
|
|
|
def array_or_list(code, contents):
|
|
if code == "O":
|
|
return list(contents)
|
|
return array.array(code, contents)
|
|
|
|
|
|
def replace_all(buffer, replacements):
|
|
"""Substitute multiple replacement values into a buffer.
|
|
|
|
Replacements is a list of (start, end, value) triples.
|
|
"""
|
|
|
|
result = bytearray()
|
|
prev = 0
|
|
offset = 0
|
|
for u, v, r in replacements:
|
|
result.extend(buffer[prev:u])
|
|
result.extend(r)
|
|
prev = v
|
|
offset += len(r) - (v - u)
|
|
result.extend(buffer[prev:])
|
|
assert len(result) == len(buffer) + offset
|
|
return bytes(result)
|
|
|
|
|
|
ARRAY_CODES = ["B", "H", "I", "L", "Q", "O"]
|
|
NEXT_ARRAY_CODE = dict(zip(ARRAY_CODES, ARRAY_CODES[1:]))
|
|
|
|
|
|
class IntList:
|
|
"""Class for storing a list of non-negative integers compactly.
|
|
|
|
We store them as the smallest size integer array we can get
|
|
away with. When we try to add an integer that is too large,
|
|
we upgrade the array to the smallest word size needed to store
|
|
the new value."""
|
|
|
|
__slots__ = ("__underlying",)
|
|
|
|
def __init__(self, values=()):
|
|
for code in ARRAY_CODES:
|
|
try:
|
|
self.__underlying = array_or_list(code, values)
|
|
break
|
|
except OverflowError:
|
|
pass
|
|
else: # pragma: no cover
|
|
raise AssertionError(f"Could not create storage for {values!r}")
|
|
if isinstance(self.__underlying, list):
|
|
for v in self.__underlying:
|
|
if v < 0 or not isinstance(v, int):
|
|
raise ValueError(f"Could not create IntList for {values!r}")
|
|
|
|
@classmethod
|
|
def of_length(cls, n):
|
|
return cls(array_or_list("B", [0]) * n)
|
|
|
|
def count(self, n):
|
|
return self.__underlying.count(n)
|
|
|
|
def __repr__(self):
|
|
return f"IntList({list(self)!r})"
|
|
|
|
def __len__(self):
|
|
return len(self.__underlying)
|
|
|
|
def __getitem__(self, i):
|
|
if isinstance(i, slice):
|
|
return IntList(self.__underlying[i])
|
|
return self.__underlying[i]
|
|
|
|
def __delitem__(self, i):
|
|
del self.__underlying[i]
|
|
|
|
def insert(self, i, v):
|
|
self.__underlying.insert(i, v)
|
|
|
|
def __iter__(self):
|
|
return iter(self.__underlying)
|
|
|
|
def __eq__(self, other):
|
|
if self is other:
|
|
return True
|
|
if not isinstance(other, IntList):
|
|
return NotImplemented
|
|
return self.__underlying == other.__underlying
|
|
|
|
def __ne__(self, other):
|
|
if self is other:
|
|
return False
|
|
if not isinstance(other, IntList):
|
|
return NotImplemented
|
|
return self.__underlying != other.__underlying
|
|
|
|
def append(self, n):
|
|
i = len(self)
|
|
self.__underlying.append(0)
|
|
self[i] = n
|
|
|
|
def __setitem__(self, i, n):
|
|
while True:
|
|
try:
|
|
self.__underlying[i] = n
|
|
return
|
|
except OverflowError:
|
|
assert n > 0
|
|
self.__upgrade()
|
|
|
|
def extend(self, ls):
|
|
for n in ls:
|
|
self.append(n)
|
|
|
|
def __upgrade(self):
|
|
code = NEXT_ARRAY_CODE[self.__underlying.typecode]
|
|
self.__underlying = array_or_list(code, self.__underlying)
|
|
|
|
|
|
def binary_search(lo, hi, f):
|
|
"""Binary searches in [lo , hi) to find
|
|
n such that f(n) == f(lo) but f(n + 1) != f(lo).
|
|
It is implicitly assumed and will not be checked
|
|
that f(hi) != f(lo).
|
|
"""
|
|
|
|
reference = f(lo)
|
|
|
|
while lo + 1 < hi:
|
|
mid = (lo + hi) // 2
|
|
if f(mid) == reference:
|
|
lo = mid
|
|
else:
|
|
hi = mid
|
|
return lo
|
|
|
|
|
|
def uniform(random, n):
|
|
"""Returns a bytestring of length n, distributed uniformly at random."""
|
|
return random.getrandbits(n * 8).to_bytes(n, "big")
|
|
|
|
|
|
class LazySequenceCopy:
|
|
"""A "copy" of a sequence that works by inserting a mask in front
|
|
of the underlying sequence, so that you can mutate it without changing
|
|
the underlying sequence. Effectively behaves as if you could do list(x)
|
|
in O(1) time. The full list API is not supported yet but there's no reason
|
|
in principle it couldn't be."""
|
|
|
|
def __init__(self, values):
|
|
self.__values = values
|
|
self.__len = len(values)
|
|
self.__mask = None
|
|
|
|
def __len__(self):
|
|
return self.__len
|
|
|
|
def pop(self):
|
|
if len(self) == 0:
|
|
raise IndexError("Cannot pop from empty list")
|
|
result = self[-1]
|
|
self.__len -= 1
|
|
if self.__mask is not None:
|
|
self.__mask.pop(self.__len, None)
|
|
return result
|
|
|
|
def __getitem__(self, i):
|
|
i = self.__check_index(i)
|
|
default = self.__values[i]
|
|
if self.__mask is None:
|
|
return default
|
|
else:
|
|
return self.__mask.get(i, default)
|
|
|
|
def __setitem__(self, i, v):
|
|
i = self.__check_index(i)
|
|
if self.__mask is None:
|
|
self.__mask = {}
|
|
self.__mask[i] = v
|
|
|
|
def __check_index(self, i):
|
|
n = len(self)
|
|
if i < -n or i >= n:
|
|
raise IndexError(f"Index {i} out of range [0, {n})")
|
|
if i < 0:
|
|
i += n
|
|
assert 0 <= i < n
|
|
return i
|
|
|
|
|
|
def clamp(lower, value, upper):
|
|
"""Given a value and lower/upper bounds, 'clamp' the value so that
|
|
it satisfies lower <= value <= upper."""
|
|
return max(lower, min(value, upper))
|
|
|
|
|
|
def swap(ls, i, j):
|
|
"""Swap the elements ls[i], ls[j]."""
|
|
if i == j:
|
|
return
|
|
ls[i], ls[j] = ls[j], ls[i]
|
|
|
|
|
|
def stack_depth_of_caller():
|
|
"""Get stack size for caller's frame.
|
|
|
|
From https://stackoverflow.com/a/47956089/9297601 , this is a simple
|
|
but much faster alternative to `len(inspect.stack(0))`. We use it
|
|
with get/set recursionlimit to make stack overflows non-flaky; see
|
|
https://github.com/HypothesisWorks/hypothesis/issues/2494 for details.
|
|
"""
|
|
frame = sys._getframe(2)
|
|
size = 1
|
|
while frame:
|
|
frame = frame.f_back
|
|
size += 1
|
|
return size
|
|
|
|
|
|
def find_integer(f):
|
|
"""Finds a (hopefully large) integer such that f(n) is True and f(n + 1) is
|
|
False.
|
|
|
|
f(0) is assumed to be True and will not be checked.
|
|
"""
|
|
# We first do a linear scan over the small numbers and only start to do
|
|
# anything intelligent if f(4) is true. This is because it's very hard to
|
|
# win big when the result is small. If the result is 0 and we try 2 first
|
|
# then we've done twice as much work as we needed to!
|
|
for i in range(1, 5):
|
|
if not f(i):
|
|
return i - 1
|
|
|
|
# We now know that f(4) is true. We want to find some number for which
|
|
# f(n) is *not* true.
|
|
# lo is the largest number for which we know that f(lo) is true.
|
|
lo = 4
|
|
|
|
# Exponential probe upwards until we find some value hi such that f(hi)
|
|
# is not true. Subsequently we maintain the invariant that hi is the
|
|
# smallest number for which we know that f(hi) is not true.
|
|
hi = 5
|
|
while f(hi):
|
|
lo = hi
|
|
hi *= 2
|
|
|
|
# Now binary search until lo + 1 = hi. At that point we have f(lo) and not
|
|
# f(lo + 1), as desired..
|
|
while lo + 1 < hi:
|
|
mid = (lo + hi) // 2
|
|
if f(mid):
|
|
lo = mid
|
|
else:
|
|
hi = mid
|
|
return lo
|
|
|
|
|
|
def pop_random(random, seq):
|
|
"""Remove and return a random element of seq. This runs in O(1) but leaves
|
|
the sequence in an arbitrary order."""
|
|
i = random.randrange(0, len(seq))
|
|
swap(seq, i, len(seq) - 1)
|
|
return seq.pop()
|
|
|
|
|
|
class NotFound(Exception):
|
|
pass
|
|
|
|
|
|
class SelfOrganisingList:
|
|
"""A self-organising list with the move-to-front heuristic.
|
|
|
|
A self-organising list is a collection which we want to retrieve items
|
|
that satisfy some predicate from. There is no faster way to do this than
|
|
a linear scan (as the predicates may be arbitrary), but the performance
|
|
of a linear scan can vary dramatically - if we happen to find a good item
|
|
on the first try it's O(1) after all. The idea of a self-organising list is
|
|
to reorder the list to try to get lucky this way as often as possible.
|
|
|
|
There are various heuristics we could use for this, and it's not clear
|
|
which are best. We use the simplest, which is that every time we find
|
|
an item we move it to the "front" (actually the back in our implementation
|
|
because we iterate in reverse) of the list.
|
|
|
|
"""
|
|
|
|
def __init__(self, values=()):
|
|
self.__values = list(values)
|
|
|
|
def __repr__(self):
|
|
return f"SelfOrganisingList({self.__values!r})"
|
|
|
|
def add(self, value):
|
|
"""Add a value to this list."""
|
|
self.__values.append(value)
|
|
|
|
def find(self, condition):
|
|
"""Returns some value in this list such that ``condition(value)`` is
|
|
True. If no such value exists raises ``NotFound``."""
|
|
for i in range(len(self.__values) - 1, -1, -1):
|
|
value = self.__values[i]
|
|
if condition(value):
|
|
del self.__values[i]
|
|
self.__values.append(value)
|
|
return value
|
|
raise NotFound("No values satisfying condition")
|