diff options
| author | robot-piglet <[email protected]> | 2023-12-10 12:55:20 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2023-12-10 13:10:14 +0300 |
| commit | dfbaba029321bce139af71ae1de12bea432dcbaa (patch) | |
| tree | b15602f49f63fca95e34e95b46552549e8471114 /contrib/python/hypothesis | |
| parent | 3dfda0adc5f6695090a1bbd4ee775a8697f830cb (diff) | |
Intermediate changes
Diffstat (limited to 'contrib/python/hypothesis')
26 files changed, 750 insertions, 492 deletions
diff --git a/contrib/python/hypothesis/py3/.dist-info/METADATA b/contrib/python/hypothesis/py3/.dist-info/METADATA index d1b6f76c1d3..b6eca63e8fa 100644 --- a/contrib/python/hypothesis/py3/.dist-info/METADATA +++ b/contrib/python/hypothesis/py3/.dist-info/METADATA @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: hypothesis -Version: 6.89.0 +Version: 6.90.0 Summary: A library for property-based testing Home-page: https://hypothesis.works Author: David R. MacIver and Zac Hatfield-Dodds diff --git a/contrib/python/hypothesis/py3/hypothesis/core.py b/contrib/python/hypothesis/py3/hypothesis/core.py index 74e363447c9..1c4cf9e1441 100644 --- a/contrib/python/hypothesis/py3/hypothesis/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/core.py @@ -610,10 +610,10 @@ def get_random_for_wrapped_test(test, wrapped_test): @attr.s class Stuff: - selfy = attr.ib(default=None) - args = attr.ib(factory=tuple) - kwargs = attr.ib(factory=dict) - given_kwargs = attr.ib(factory=dict) + selfy: Any = attr.ib(default=None) + args: tuple = attr.ib(factory=tuple) + kwargs: dict = attr.ib(factory=dict) + given_kwargs: dict = attr.ib(factory=dict) def process_arguments_to_given(wrapped_test, arguments, kwargs, given_kwargs, params): diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py b/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py index a525d3c473f..c66853fd1c3 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/_array_helpers.py @@ -13,7 +13,7 @@ from typing import NamedTuple, Optional, Tuple, Union from hypothesis import assume, strategies as st from hypothesis.errors import InvalidArgument -from hypothesis.internal.conjecture import utils as cu +from hypothesis.internal.conjecture.utils import _calc_p_continue from hypothesis.internal.coverage import check_function from hypothesis.internal.validation import check_type, check_valid_interval from hypothesis.strategies._internal.utils import defines_strategy @@ -544,7 +544,7 @@ class MutuallyBroadcastableShapesStrategy(st.SearchStrategy): if name not in dims: dim = name.strip("?") dims[dim] = data.draw(self.side_strat) - if self.min_dims == 0 and not data.draw_bits(3): + if self.min_dims == 0 and not data.draw_boolean(7 / 8): dims[dim + "?"] = None else: dims[dim + "?"] = dims[dim] @@ -562,6 +562,9 @@ class MutuallyBroadcastableShapesStrategy(st.SearchStrategy): assert len(use) == self.num_shapes assert all(isinstance(x, bool) for x in use) + _gap = self.max_dims - self.min_dims + p_keep_extending_shape = _calc_p_continue(desired_avg=_gap / 2, max_size=_gap) + for dim_count in range(1, self.max_dims + 1): dim = dim_count - 1 @@ -596,9 +599,7 @@ class MutuallyBroadcastableShapesStrategy(st.SearchStrategy): # shape-tuple even if it is no longer being added to. # This helps to ensure more stable shrinking behavior. if self.min_dims < dim_count: - use[shape_id] &= cu.biased_coin( - data, 1 - 1 / (1 + self.max_dims - dim) - ) + use[shape_id] &= data.draw_boolean(p_keep_extending_shape) if use[shape_id]: shape.append(side) diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py b/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py index cb5a3fee409..d62418f317d 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/array_api.py @@ -422,7 +422,7 @@ class ArrayStrategy(st.SearchStrategy): seen = set() while elements.more(): - i = cu.integer_range(data, 0, self.array_size - 1) + i = data.draw_integer(0, self.array_size - 1) if i in assigned: elements.reject() continue diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/lark.py b/contrib/python/hypothesis/py3/hypothesis/extra/lark.py index 9bab2c65cee..57b68ec9f77 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/lark.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/lark.py @@ -158,7 +158,7 @@ class LarkStrategy(st.SearchStrategy): data.stop_example() def gen_ignore(self, data, draw_state): - if self.ignored_symbols and data.draw_bits(2) == 3: + if self.ignored_symbols and data.draw_boolean(1 / 4): emit = data.draw(st.sampled_from(self.ignored_symbols)) self.draw_symbol(data, emit, draw_state) diff --git a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py index f7f27a74138..396b9498fe3 100644 --- a/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py +++ b/contrib/python/hypothesis/py3/hypothesis/extra/numpy.py @@ -330,7 +330,7 @@ class ArrayStrategy(st.SearchStrategy): seen = set() while elements.more(): - i = cu.integer_range(data, 0, self.array_size - 1) + i = data.draw_integer(0, self.array_size - 1) if not needs_fill[i]: elements.reject() continue @@ -756,7 +756,7 @@ def array_dtypes( field_names, st.tuples(field_names, field_names).filter(lambda ns: ns[0] != ns[1]), ) - elements = st.tuples(name_titles, subtype_strategy) + elements: st.SearchStrategy[tuple] = st.tuples(name_titles, subtype_strategy) if allow_subarrays: elements |= st.tuples( name_titles, subtype_strategy, array_shapes(max_dims=2, max_side=2) diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py index c1337cde0fa..7eb5fbe0800 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/data.py @@ -8,19 +8,23 @@ # 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/. +import math import time from collections import defaultdict from enum import IntEnum from random import Random +from sys import float_info from typing import ( TYPE_CHECKING, Any, + Callable, Dict, FrozenSet, Hashable, Iterable, Iterator, List, + Literal, NoReturn, Optional, Sequence, @@ -33,9 +37,27 @@ from typing import ( import attr from hypothesis.errors import Frozen, InvalidArgument, StopTest -from hypothesis.internal.compat import int_from_bytes, int_to_bytes +from hypothesis.internal.cache import LRUReusedCache +from hypothesis.internal.compat import floor, int_from_bytes, int_to_bytes +from hypothesis.internal.conjecture.floats import float_to_lex, lex_to_float from hypothesis.internal.conjecture.junkdrawer import IntList, uniform -from hypothesis.internal.conjecture.utils import calc_label_from_name +from hypothesis.internal.conjecture.utils import ( + INT_SIZES, + INT_SIZES_SAMPLER, + Sampler, + calc_label_from_name, + many, +) +from hypothesis.internal.floats import ( + SIGNALING_NAN, + SMALLEST_SUBNORMAL, + float_to_int, + make_float_clamper, + next_down, + next_up, + sign_aware_lte, +) +from hypothesis.internal.intervalsets import IntervalSet if TYPE_CHECKING: from typing_extensions import dataclass_transform @@ -51,9 +73,17 @@ else: return wrapper +ONE_BOUND_INTEGERS_LABEL = calc_label_from_name("trying a one-bound int allowing 0") +INTEGER_RANGE_DRAW_LABEL = calc_label_from_name("another draw in integer_range()") +BIASED_COIN_LABEL = calc_label_from_name("biased_coin()") +BIASED_COIN_INNER_LABEL = calc_label_from_name("inside biased_coin()") + TOP_LABEL = calc_label_from_name("top") DRAW_BYTES_LABEL = calc_label_from_name("draw_bytes() in ConjectureData") - +DRAW_FLOAT_LABEL = calc_label_from_name("drawing a float") +FLOAT_STRATEGY_DO_DRAW_LABEL = calc_label_from_name( + "getting another float in FloatStrategy" +) InterestingOrigin = Tuple[ Type[BaseException], str, int, Tuple[Any, ...], Tuple[Tuple[Any, ...], ...] @@ -100,6 +130,39 @@ def structural_coverage(label: int) -> StructuralCoverageTag: return STRUCTURAL_COVERAGE_CACHE.setdefault(label, StructuralCoverageTag(label)) +NASTY_FLOATS = sorted( + [ + 0.0, + 0.5, + 1.1, + 1.5, + 1.9, + 1.0 / 3, + 10e6, + 10e-6, + 1.175494351e-38, + next_up(0.0), + float_info.min, + float_info.max, + 3.402823466e38, + 9007199254740992, + 1 - 10e-6, + 2 + 10e-6, + 1.192092896e-07, + 2.2204460492503131e-016, + ] + + [2.0**-n for n in (24, 14, 149, 126)] # minimum (sub)normals for float16,32 + + [float_info.min / n for n in (2, 10, 1000, 100_000)] # subnormal in float64 + + [math.inf, math.nan] * 5 + + [SIGNALING_NAN], + key=float_to_lex, +) +NASTY_FLOATS = list(map(float, NASTY_FLOATS)) +NASTY_FLOATS.extend([-x for x in NASTY_FLOATS]) + +FLOAT_INIT_LOGIC_CACHE = LRUReusedCache(4096) + + class Example: """Examples track the hierarchical structure of draws from the byte stream, within a single test run. @@ -796,6 +859,475 @@ BYTE_MASKS = [(1 << n) - 1 for n in range(8)] BYTE_MASKS[0] = 255 +class PrimitiveProvider: + # This is the low-level interface which would also be implemented + # by e.g. CrossHair, by an Atheris-hypothesis integration, etc. + # We'd then build the structured tree handling, database and replay + # support, etc. on top of this - so all backends get those for free. + # + # See https://github.com/HypothesisWorks/hypothesis/issues/3086 + + def __init__(self, conjecturedata: "ConjectureData", /) -> None: + self._cd = conjecturedata + + def draw_boolean(self, p: float = 0.5, *, forced: Optional[bool] = None) -> bool: + """Return True with probability p (assuming a uniform generator), + shrinking towards False. If ``forced`` is set to a non-None value, this + will always return that value but will write choices appropriate to having + drawn that value randomly.""" + # Note that this could also be implemented in terms of draw_integer(). + + # NB this function is vastly more complicated than it may seem reasonable + # for it to be. This is because it is used in a lot of places and it's + # important for it to shrink well, so it's worth the engineering effort. + + if p <= 0 or p >= 1: + bits = 1 + else: + # When there is a meaningful draw, in order to shrink well we will + # set things up so that 0 and 1 always correspond to False and True + # respectively. This means we want enough bits available that in a + # draw we will always have at least one truthy value and one falsey + # value. + bits = math.ceil(-math.log(min(p, 1 - p), 2)) + # In order to avoid stupidly large draws where the probability is + # effectively zero or one, we treat probabilities of under 2^-64 to be + # effectively zero. + if bits > 64: + # There isn't enough precision near one for this to occur for values + # far from 0. + p = 0.0 + bits = 1 + + size = 2**bits + + self._cd.start_example(BIASED_COIN_LABEL) + while True: + # The logic here is a bit complicated and special cased to make it + # play better with the shrinker. + + # We imagine partitioning the real interval [0, 1] into 2**n equal parts + # and looking at each part and whether its interior is wholly <= p + # or wholly >= p. At most one part can be neither. + + # We then pick a random part. If it's wholly on one side or the other + # of p then we use that as the answer. If p is contained in the + # interval then we start again with a new probability that is given + # by the fraction of that interval that was <= our previous p. + + # We then take advantage of the fact that we have control of the + # labelling to make this shrink better, using the following tricks: + + # If p is <= 0 or >= 1 the result of this coin is certain. We make sure + # to write a byte to the data stream anyway so that these don't cause + # difficulties when shrinking. + if p <= 0: + self._cd.draw_bits(1, forced=0) + result = False + elif p >= 1: + self._cd.draw_bits(1, forced=1) + result = True + else: + falsey = floor(size * (1 - p)) + truthy = floor(size * p) + remainder = size * p - truthy + + if falsey + truthy == size: + partial = False + else: + partial = True + + if forced is None: + # We want to get to the point where True is represented by + # 1 and False is represented by 0 as quickly as possible, so + # we use the remove_discarded machinery in the shrinker to + # achieve that by discarding any draws that are > 1 and writing + # a suitable draw into the choice sequence at the end of the + # loop. + self._cd.start_example(BIASED_COIN_INNER_LABEL) + i = self._cd.draw_bits(bits) + self._cd.stop_example(discard=i > 1) + else: + i = self._cd.draw_bits(bits, forced=int(forced)) + + # We always choose the region that causes us to repeat the loop as + # the maximum value, so that shrinking the drawn bits never causes + # us to need to draw more self._cd. + if partial and i == size - 1: + p = remainder + continue + if falsey == 0: + # Every other partition is truthy, so the result is true + result = True + elif truthy == 0: + # Every other partition is falsey, so the result is false + result = False + elif i <= 1: + # We special case so that zero is always false and 1 is always + # true which makes shrinking easier because we can always + # replace a truthy block with 1. This has the slightly weird + # property that shrinking from 2 to 1 can cause the result to + # grow, but the shrinker always tries 0 and 1 first anyway, so + # this will usually be fine. + result = bool(i) + else: + # Originally everything in the region 0 <= i < falsey was false + # and everything above was true. We swapped one truthy element + # into this region, so the region becomes 0 <= i <= falsey + # except for i = 1. We know i > 1 here, so the test for truth + # becomes i > falsey. + result = i > falsey + + if i > 1: + self._cd.draw_bits(bits, forced=int(result)) + break + self._cd.stop_example() + return result + + def draw_integer( + self, + min_value: Optional[int] = None, + max_value: Optional[int] = None, + *, + # weights are for choosing an element index from a bounded range + weights: Optional[Sequence[float]] = None, + shrink_towards: int = 0, + forced: Optional[int] = None, + ) -> int: + # This is easy to build on top of our existing conjecture utils, + # and it's easy to build sampled_from and weighted_coin on this. + if weights is not None: + assert min_value is not None + assert max_value is not None + + sampler = Sampler(weights) + idx = sampler.sample(self._cd) + + if shrink_towards <= min_value: + return min_value + idx + elif max_value <= shrink_towards: + return max_value - idx + else: + # For range -2..2, interpret idx = 0..4 as [0, 1, 2, -1, -2] + if idx <= (gap := max_value - shrink_towards): + return shrink_towards + idx + else: + return shrink_towards - (idx - gap) + + if min_value is None and max_value is None: + return self._draw_unbounded_integer() + + if min_value is None: + assert max_value is not None # make mypy happy + if max_value <= shrink_towards: + return max_value - abs(self._draw_unbounded_integer()) + else: + probe = max_value + 1 + while max_value < probe: + self._cd.start_example(ONE_BOUND_INTEGERS_LABEL) + probe = self._draw_unbounded_integer() + shrink_towards + self._cd.stop_example(discard=max_value < probe) + return probe + + if max_value is None: + assert min_value is not None + if min_value >= shrink_towards: + return min_value + abs(self._draw_unbounded_integer()) + else: + probe = min_value - 1 + while probe < min_value: + self._cd.start_example(ONE_BOUND_INTEGERS_LABEL) + probe = self._draw_unbounded_integer() + shrink_towards + self._cd.stop_example(discard=probe < min_value) + return probe + + return self._draw_bounded_integer( + min_value, + max_value, + center=shrink_towards, + forced=forced, + ) + + def draw_float( + self, + *, + min_value: float = -math.inf, + max_value: float = math.inf, + allow_nan: bool = True, + smallest_nonzero_magnitude: float, + # TODO: consider supporting these float widths at the IR level in the + # future. + # width: Literal[16, 32, 64] = 64, + # exclude_min and exclude_max handled higher up + ) -> float: + ( + sampler, + forced_sign_bit, + neg_clamper, + pos_clamper, + nasty_floats, + ) = self._draw_float_init_logic( + min_value=min_value, + max_value=max_value, + allow_nan=allow_nan, + smallest_nonzero_magnitude=smallest_nonzero_magnitude, + ) + + while True: + self._cd.start_example(FLOAT_STRATEGY_DO_DRAW_LABEL) + i = sampler.sample(self._cd) if sampler else 0 + self._cd.start_example(DRAW_FLOAT_LABEL) + if i == 0: + result = self._draw_float(forced_sign_bit=forced_sign_bit) + if math.copysign(1.0, result) == -1: + assert neg_clamper is not None + clamped = -neg_clamper(-result) + else: + assert pos_clamper is not None + clamped = pos_clamper(result) + if clamped != result: + self._cd.stop_example(discard=True) + self._cd.start_example(DRAW_FLOAT_LABEL) + self._write_float(clamped) + result = clamped + else: + result = nasty_floats[i - 1] + + self._write_float(result) + + self._cd.stop_example() # (DRAW_FLOAT_LABEL) + self._cd.stop_example() # (FLOAT_STRATEGY_DO_DRAW_LABEL) + return result + + def draw_string( + self, + intervals: IntervalSet, + *, + min_size: int = 0, + max_size: Optional[int] = None, + ) -> str: + if max_size is None: + max_size = 10**10 # "arbitrarily large" + + average_size = min( + max(min_size * 2, min_size + 5), + 0.5 * (min_size + max_size), + ) + + chars = [] + elements = many( + self._cd, + min_size=min_size, + max_size=max_size, + average_size=average_size, + ) + while elements.more(): + if len(intervals) > 256: + if self.draw_boolean(0.2): + i = self._draw_bounded_integer(256, len(intervals) - 1) + else: + i = self._draw_bounded_integer(0, 255) + else: + i = self._draw_bounded_integer(0, len(intervals) - 1) + + chars.append(intervals.char_in_shrink_order(i)) + + return "".join(chars) + + def draw_bytes(self, size: int) -> bytes: + return self._cd.draw_bits(8 * size).to_bytes(size, "big") + + def _draw_float(self, forced_sign_bit: Optional[int] = None) -> float: + """ + Helper for draw_float which draws a random 64-bit float. + """ + self._cd.start_example(DRAW_FLOAT_LABEL) + try: + is_negative = self._cd.draw_bits(1, forced=forced_sign_bit) + f = lex_to_float(self._cd.draw_bits(64)) + return -f if is_negative else f + finally: + self._cd.stop_example() + + def _write_float(self, f: float) -> None: + sign = float_to_int(f) >> 63 + self._cd.draw_bits(1, forced=sign) + self._cd.draw_bits(64, forced=float_to_lex(abs(f))) + + def _draw_unbounded_integer(self) -> int: + size = INT_SIZES[INT_SIZES_SAMPLER.sample(self._cd)] + r = self._cd.draw_bits(size) + sign = r & 1 + r >>= 1 + if sign: + r = -r + return int(r) + + def _draw_bounded_integer( + self, + lower: int, + upper: int, + *, + center: Optional[int] = None, + forced: Optional[int] = None, + ) -> int: + assert lower <= upper + assert forced is None or lower <= forced <= upper + if lower == upper: + # Write a value even when this is trivial so that when a bound depends + # on other values we don't suddenly disappear when the gap shrinks to + # zero - if that happens then often the data stream becomes misaligned + # and we fail to shrink in cases where we really should be able to. + self._cd.draw_bits(1, forced=0) + return int(lower) + + if center is None: + center = lower + center = min(max(center, lower), upper) + + if center == upper: + above = False + elif center == lower: + above = True + else: + force_above = None if forced is None else forced < center + above = not self._cd.draw_bits(1, forced=force_above) + + if above: + gap = upper - center + else: + gap = center - lower + + assert gap > 0 + + bits = gap.bit_length() + probe = gap + 1 + + if bits > 24 and self._cd.draw_bits(3, forced=None if forced is None else 0): + # For large ranges, we combine the uniform random distribution from draw_bits + # with a weighting scheme with moderate chance. Cutoff at 2 ** 24 so that our + # choice of unicode characters is uniform but the 32bit distribution is not. + idx = INT_SIZES_SAMPLER.sample(self._cd) + bits = min(bits, INT_SIZES[idx]) + + while probe > gap: + self._cd.start_example(INTEGER_RANGE_DRAW_LABEL) + probe = self._cd.draw_bits( + bits, forced=None if forced is None else abs(forced - center) + ) + self._cd.stop_example(discard=probe > gap) + + if above: + result = center + probe + else: + result = center - probe + + assert lower <= result <= upper + assert forced is None or result == forced, (result, forced, center, above) + return result + + @classmethod + def _draw_float_init_logic( + cls, + *, + min_value: float, + max_value: float, + allow_nan: bool, + smallest_nonzero_magnitude: float, + ) -> Tuple[ + Optional[Sampler], + Optional[Literal[0, 1]], + Optional[Callable[[float], float]], + Optional[Callable[[float], float]], + List[float], + ]: + """ + Caches initialization logic for draw_float, as an alternative to + computing this for *every* float draw. + """ + # float_to_int allows us to distinguish between e.g. -0.0 and 0.0, + # even in light of hash(-0.0) == hash(0.0) and -0.0 == 0.0. + key = ( + float_to_int(min_value), + float_to_int(max_value), + allow_nan, + float_to_int(smallest_nonzero_magnitude), + ) + if key in FLOAT_INIT_LOGIC_CACHE: + return FLOAT_INIT_LOGIC_CACHE[key] + + result = cls._compute_draw_float_init_logic( + min_value=min_value, + max_value=max_value, + allow_nan=allow_nan, + smallest_nonzero_magnitude=smallest_nonzero_magnitude, + ) + FLOAT_INIT_LOGIC_CACHE[key] = result + return result + + @staticmethod + def _compute_draw_float_init_logic( + *, + min_value: float, + max_value: float, + allow_nan: bool, + smallest_nonzero_magnitude: float, + ) -> Tuple[ + Optional[Sampler], + Optional[Literal[0, 1]], + Optional[Callable[[float], float]], + Optional[Callable[[float], float]], + List[float], + ]: + if smallest_nonzero_magnitude == 0.0: # pragma: no cover + raise FloatingPointError( + "Got allow_subnormal=True, but we can't represent subnormal floats " + "right now, in violation of the IEEE-754 floating-point " + "specification. This is usually because something was compiled with " + "-ffast-math or a similar option, which sets global processor state. " + "See https://simonbyrne.github.io/notes/fastmath/ for a more detailed " + "writeup - and good luck!" + ) + + def permitted(f): + assert isinstance(f, float) + if math.isnan(f): + return allow_nan + if 0 < abs(f) < smallest_nonzero_magnitude: + return False + return sign_aware_lte(min_value, f) and sign_aware_lte(f, max_value) + + boundary_values = [ + min_value, + next_up(min_value), + min_value + 1, + max_value - 1, + next_down(max_value), + max_value, + ] + nasty_floats = [f for f in NASTY_FLOATS + boundary_values if permitted(f)] + weights = [0.2 * len(nasty_floats)] + [0.8] * len(nasty_floats) + sampler = Sampler(weights) if nasty_floats else None + + pos_clamper = neg_clamper = None + if sign_aware_lte(0.0, max_value): + pos_min = max(min_value, smallest_nonzero_magnitude) + allow_zero = sign_aware_lte(min_value, 0.0) + pos_clamper = make_float_clamper(pos_min, max_value, allow_zero=allow_zero) + if sign_aware_lte(min_value, -0.0): + neg_max = min(max_value, -smallest_nonzero_magnitude) + allow_zero = sign_aware_lte(-0.0, max_value) + neg_clamper = make_float_clamper( + -neg_max, -min_value, allow_zero=allow_zero + ) + + forced_sign_bit: Optional[Literal[0, 1]] = None + if (pos_clamper is None) != (neg_clamper is None): + forced_sign_bit = 1 if neg_clamper else 0 + + return (sampler, forced_sign_bit, neg_clamper, pos_clamper, nasty_floats) + + class ConjectureData: @classmethod def for_buffer( @@ -841,6 +1373,7 @@ class ConjectureData: self.draw_times: "List[float]" = [] self.max_depth = 0 self.has_discards = False + self.provider = PrimitiveProvider(self) self.__result: "Optional[ConjectureResult]" = None @@ -879,6 +1412,71 @@ class ConjectureData: ", frozen" if self.frozen else "", ) + def draw_integer( + self, + min_value: Optional[int] = None, + max_value: Optional[int] = None, + *, + # weights are for choosing an element index from a bounded range + weights: Optional[Sequence[float]] = None, + shrink_towards: int = 0, + forced: Optional[int] = None, + ) -> int: + # Validate arguments + if weights is not None: + assert min_value is not None + assert max_value is not None + assert (max_value - min_value) <= 1024 # arbitrary practical limit + + if forced is not None: + assert min_value is not None + assert max_value is not None + + return self.provider.draw_integer( + min_value=min_value, + max_value=max_value, + weights=weights, + shrink_towards=shrink_towards, + forced=forced, + ) + + def draw_float( + self, + min_value: float = -math.inf, + max_value: float = math.inf, + *, + allow_nan: bool = True, + smallest_nonzero_magnitude: float = SMALLEST_SUBNORMAL, + # TODO: consider supporting these float widths at the IR level in the + # future. + # width: Literal[16, 32, 64] = 64, + # exclude_min and exclude_max handled higher up + ) -> float: + assert smallest_nonzero_magnitude > 0 + return self.provider.draw_float( + min_value=min_value, + max_value=max_value, + allow_nan=allow_nan, + smallest_nonzero_magnitude=smallest_nonzero_magnitude, + ) + + def draw_string( + self, + intervals: IntervalSet, + *, + min_size: int = 0, + max_size: Optional[int] = None, + ) -> str: + return self.provider.draw_string( + intervals, min_size=min_size, max_size=max_size + ) + + def draw_bytes(self, size: int) -> bytes: + return self.provider.draw_bytes(size) + + def draw_boolean(self, p: float = 0.5, *, forced: Optional[bool] = None) -> bool: + return self.provider.draw_boolean(p, forced=forced) + def as_result(self) -> Union[ConjectureResult, _Overrun]: """Convert the result of running this test into either an Overrun object or a ConjectureResult.""" @@ -1099,10 +1697,6 @@ class ConjectureData: assert result.bit_length() <= n return result - def draw_bytes(self, n: int) -> bytes: - """Draw n bytes from the underlying source.""" - return int_to_bytes(self.draw_bits(8 * n), n) - def write(self, string: bytes) -> Optional[bytes]: """Write ``string`` to the output buffer.""" self.__assert_not_frozen("write") diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/floats.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/floats.py index 5608570130b..2b82bea07c0 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/floats.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/floats.py @@ -9,14 +9,9 @@ # obtain one at https://mozilla.org/MPL/2.0/. from array import array -from typing import TYPE_CHECKING, Optional -from hypothesis.internal.conjecture.utils import calc_label_from_name from hypothesis.internal.floats import float_to_int, int_to_float -if TYPE_CHECKING: - from hypothesis.internal.conjecture.data import ConjectureData - """ This module implements support for arbitrary floating point numbers in Conjecture. It doesn't make any attempt to get a good distribution, only to @@ -83,8 +78,6 @@ MAX_EXPONENT = 0x7FF BIAS = 1023 MAX_POSITIVE_EXPONENT = MAX_EXPONENT - 1 - BIAS -DRAW_FLOAT_LABEL = calc_label_from_name("drawing a float") - def exponent_key(e: int) -> float: if e == MAX_EXPONENT: @@ -224,19 +217,3 @@ def is_simple(f: float) -> int: if i != f: return False return i.bit_length() <= 56 - - -def draw_float(data: "ConjectureData", forced_sign_bit: Optional[int] = None) -> float: - try: - data.start_example(DRAW_FLOAT_LABEL) - is_negative = data.draw_bits(1, forced=forced_sign_bit) - f = lex_to_float(data.draw_bits(64)) - return -f if is_negative else f - finally: - data.stop_example() - - -def write_float(data: "ConjectureData", f: float) -> None: - sign = float_to_int(f) >> 63 - data.draw_bits(1, forced=sign) - data.draw_bits(64, forced=float_to_lex(abs(f))) diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py index 69bec609404..f965829759a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/shrinker.py @@ -19,13 +19,14 @@ from hypothesis.internal.conjecture.choicetree import ( prefix_selection_order, random_selection_order, ) -from hypothesis.internal.conjecture.data import ConjectureData, ConjectureResult, Status -from hypothesis.internal.conjecture.dfa import ConcreteDFA -from hypothesis.internal.conjecture.floats import ( +from hypothesis.internal.conjecture.data import ( DRAW_FLOAT_LABEL, - float_to_lex, - lex_to_float, + ConjectureData, + ConjectureResult, + Status, ) +from hypothesis.internal.conjecture.dfa import ConcreteDFA +from hypothesis.internal.conjecture.floats import float_to_lex, lex_to_float from hypothesis.internal.conjecture.junkdrawer import ( binary_search, find_integer, diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py index 7402bb47d38..48a5ec3f27c 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/conjecture/utils.py @@ -11,15 +11,14 @@ import enum import hashlib import heapq -import math import sys from collections import OrderedDict, abc from functools import lru_cache from typing import TYPE_CHECKING, List, Optional, Sequence, Tuple, Type, TypeVar, Union from hypothesis.errors import InvalidArgument -from hypothesis.internal.compat import floor, int_from_bytes -from hypothesis.internal.floats import int_to_float, next_up +from hypothesis.internal.compat import int_from_bytes +from hypothesis.internal.floats import next_up if TYPE_CHECKING: from hypothesis.internal.conjecture.data import ConjectureData @@ -45,86 +44,10 @@ def combine_labels(*labels: int) -> int: return label -INTEGER_RANGE_DRAW_LABEL = calc_label_from_name("another draw in integer_range()") -BIASED_COIN_LABEL = calc_label_from_name("biased_coin()") -BIASED_COIN_INNER_LABEL = calc_label_from_name("inside biased_coin()") SAMPLE_IN_SAMPLER_LABEL = calc_label_from_name("a sample() in Sampler") ONE_FROM_MANY_LABEL = calc_label_from_name("one more from many()") -def unbounded_integers(data: "ConjectureData") -> int: - size = INT_SIZES[INT_SIZES_SAMPLER.sample(data)] - r = data.draw_bits(size) - sign = r & 1 - r >>= 1 - if sign: - r = -r - return int(r) - - -def integer_range( - data: "ConjectureData", - lower: int, - upper: int, - center: Optional[int] = None, - forced: Optional[int] = None, -) -> int: - assert lower <= upper - assert forced is None or lower <= forced <= upper - if lower == upper: - # Write a value even when this is trivial so that when a bound depends - # on other values we don't suddenly disappear when the gap shrinks to - # zero - if that happens then often the data stream becomes misaligned - # and we fail to shrink in cases where we really should be able to. - data.draw_bits(1, forced=0) - return int(lower) - - if center is None: - center = lower - center = min(max(center, lower), upper) - - if center == upper: - above = False - elif center == lower: - above = True - else: - force_above = None if forced is None else forced < center - above = not data.draw_bits(1, forced=force_above) - - if above: - gap = upper - center - else: - gap = center - lower - - assert gap > 0 - - bits = gap.bit_length() - probe = gap + 1 - - if bits > 24 and data.draw_bits(3, forced=None if forced is None else 0): - # For large ranges, we combine the uniform random distribution from draw_bits - # with a weighting scheme with moderate chance. Cutoff at 2 ** 24 so that our - # choice of unicode characters is uniform but the 32bit distribution is not. - idx = INT_SIZES_SAMPLER.sample(data) - bits = min(bits, INT_SIZES[idx]) - - while probe > gap: - data.start_example(INTEGER_RANGE_DRAW_LABEL) - probe = data.draw_bits( - bits, forced=None if forced is None else abs(forced - center) - ) - data.stop_example(discard=probe > gap) - - if above: - result = center + probe - else: - result = center - probe - - assert lower <= result <= upper - assert forced is None or result == forced, (result, forced, center, above) - return result - - T = TypeVar("T") @@ -159,131 +82,7 @@ def check_sample( def choice(data: "ConjectureData", values: Sequence[T]) -> T: - return values[integer_range(data, 0, len(values) - 1)] - - -FLOAT_PREFIX = 0b1111111111 << 52 -FULL_FLOAT = int_to_float(FLOAT_PREFIX | ((2 << 53) - 1)) - 1 - - -def fractional_float(data: "ConjectureData") -> float: - return (int_to_float(FLOAT_PREFIX | data.draw_bits(52)) - 1) / FULL_FLOAT - - -def biased_coin( - data: "ConjectureData", p: float, *, forced: Optional[bool] = None -) -> bool: - """Return True with probability p (assuming a uniform generator), - shrinking towards False. If ``forced`` is set to a non-None value, this - will always return that value but will write choices appropriate to having - drawn that value randomly.""" - - # NB this function is vastly more complicated than it may seem reasonable - # for it to be. This is because it is used in a lot of places and it's - # important for it to shrink well, so it's worth the engineering effort. - - if p <= 0 or p >= 1: - bits = 1 - else: - # When there is a meaningful draw, in order to shrink well we will - # set things up so that 0 and 1 always correspond to False and True - # respectively. This means we want enough bits available that in a - # draw we will always have at least one truthy value and one falsey - # value. - bits = math.ceil(-math.log(min(p, 1 - p), 2)) - # In order to avoid stupidly large draws where the probability is - # effectively zero or one, we treat probabilities of under 2^-64 to be - # effectively zero. - if bits > 64: - # There isn't enough precision near one for this to occur for values - # far from 0. - p = 0.0 - bits = 1 - - size = 2**bits - - data.start_example(BIASED_COIN_LABEL) - while True: - # The logic here is a bit complicated and special cased to make it - # play better with the shrinker. - - # We imagine partitioning the real interval [0, 1] into 2**n equal parts - # and looking at each part and whether its interior is wholly <= p - # or wholly >= p. At most one part can be neither. - - # We then pick a random part. If it's wholly on one side or the other - # of p then we use that as the answer. If p is contained in the - # interval then we start again with a new probability that is given - # by the fraction of that interval that was <= our previous p. - - # We then take advantage of the fact that we have control of the - # labelling to make this shrink better, using the following tricks: - - # If p is <= 0 or >= 1 the result of this coin is certain. We make sure - # to write a byte to the data stream anyway so that these don't cause - # difficulties when shrinking. - if p <= 0: - data.draw_bits(1, forced=0) - result = False - elif p >= 1: - data.draw_bits(1, forced=1) - result = True - else: - falsey = floor(size * (1 - p)) - truthy = floor(size * p) - remainder = size * p - truthy - - if falsey + truthy == size: - partial = False - else: - partial = True - - if forced is None: - # We want to get to the point where True is represented by - # 1 and False is represented by 0 as quickly as possible, so - # we use the remove_discarded machinery in the shrinker to - # achieve that by discarding any draws that are > 1 and writing - # a suitable draw into the choice sequence at the end of the - # loop. - data.start_example(BIASED_COIN_INNER_LABEL) - i = data.draw_bits(bits) - data.stop_example(discard=i > 1) - else: - i = data.draw_bits(bits, forced=int(forced)) - - # We always choose the region that causes us to repeat the loop as - # the maximum value, so that shrinking the drawn bits never causes - # us to need to draw more data. - if partial and i == size - 1: - p = remainder - continue - if falsey == 0: - # Every other partition is truthy, so the result is true - result = True - elif truthy == 0: - # Every other partition is falsey, so the result is false - result = False - elif i <= 1: - # We special case so that zero is always false and 1 is always - # true which makes shrinking easier because we can always - # replace a truthy block with 1. This has the slightly weird - # property that shrinking from 2 to 1 can cause the result to - # grow, but the shrinker always tries 0 and 1 first anyway, so - # this will usually be fine. - result = bool(i) - else: - # Originally everything in the region 0 <= i < falsey was false - # and everything above was true. We swapped one truthy element - # into this region, so the region becomes 0 <= i <= falsey - # except for i = 1. We know i > 1 here, so the test for truth - # becomes i > falsey. - result = i > falsey - - if i > 1: - data.draw_bits(bits, forced=int(result)) - break - data.stop_example() - return result + return values[data.draw_integer(0, len(values) - 1)] class Sampler: @@ -375,7 +174,7 @@ class Sampler: def sample(self, data: "ConjectureData") -> int: data.start_example(SAMPLE_IN_SAMPLER_LABEL) base, alternate, alternate_chance = choice(data, self.table) - use_alternate = biased_coin(data, alternate_chance) + use_alternate = data.draw_boolean(alternate_chance) data.stop_example() if use_alternate: return alternate @@ -437,8 +236,8 @@ class many: forced_result = True elif self.count >= self.max_size: forced_result = False - should_continue = biased_coin( - self.data, self.p_continue, forced=forced_result + should_continue = self.data.draw_boolean( + self.p_continue, forced=forced_result ) if should_continue: diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/floats.py b/contrib/python/hypothesis/py3/hypothesis/internal/floats.py index e0cd95d3741..6c4210e997b 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/floats.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/floats.py @@ -149,3 +149,17 @@ def make_float_clamper( return max(min_float, min(max_float, float_val)) return float_clamper + + +def sign_aware_lte(x: float, y: float) -> bool: + """Less-than-or-equals, but strictly orders -0.0 and 0.0""" + if x == 0.0 == y: + return math.copysign(1.0, x) <= math.copysign(1.0, y) + else: + return x <= y + + +SMALLEST_SUBNORMAL = next_up(0.0) +SIGNALING_NAN = int_to_float(0x7FF8_0000_0000_0001) # nonzero mantissa +assert math.isnan(SIGNALING_NAN) +assert math.copysign(1, SIGNALING_NAN) == 1 diff --git a/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py b/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py index a4d88515819..c5e82f6b22c 100644 --- a/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py +++ b/contrib/python/hypothesis/py3/hypothesis/internal/intervalsets.py @@ -26,6 +26,8 @@ class IntervalSet: for u, v in self.intervals: self.offsets.append(self.offsets[-1] + v - u + 1) self.size = self.offsets.pop() + self._idx_of_zero = self.index_above(ord("0")) + self._idx_of_Z = min(self.index_above(ord("Z")), len(self) - 1) def __len__(self): return self.size @@ -228,3 +230,27 @@ class IntervalSet: else: j += 1 return IntervalSet(intervals) + + def char_in_shrink_order(self, i: int) -> str: + # We would like it so that, where possible, shrinking replaces + # characters with simple ascii characters, so we rejig this + # bit so that the smallest values are 0, 1, 2, ..., Z. + # + # Imagine that numbers are laid out as abc0yyyZ... + # this rearranges them so that they are laid out as + # 0yyyZcba..., which gives a better shrinking order. + if i <= self._idx_of_Z: + # We want to rewrite the integers [0, n] inclusive + # to [zero_point, Z_point]. + n = self._idx_of_Z - self._idx_of_zero + if i <= n: + i += self._idx_of_zero + else: + # We want to rewrite the integers [n + 1, Z_point] to + # [zero_point, 0] (reversing the order so that codepoints below + # zero_point shrink upwards). + i = self._idx_of_zero - (i - n) + assert i < self._idx_of_zero + assert 0 <= i <= self._idx_of_Z + + return chr(self[i]) diff --git a/contrib/python/hypothesis/py3/hypothesis/stateful.py b/contrib/python/hypothesis/py3/hypothesis/stateful.py index b827d50d2db..e70c805815a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/stateful.py +++ b/contrib/python/hypothesis/py3/hypothesis/stateful.py @@ -140,7 +140,7 @@ def run_state_machine_as_test(state_machine_factory, *, settings=None, _min_step must_stop = True elif steps_run <= _min_steps: must_stop = False - if cu.biased_coin(cd, 2**-16, forced=must_stop): + if cd.draw_boolean(p=2**-16, forced=must_stop): break steps_run += 1 @@ -220,9 +220,10 @@ def run_state_machine_as_test(state_machine_factory, *, settings=None, _min_step class StateMachineMeta(type): def __setattr__(cls, name, value): if name == "settings" and isinstance(value, Settings): + descr = f"settings({value.show_changed()})" raise AttributeError( - f"Assigning {cls.__name__}.settings = {value} does nothing. Assign " - f"to {cls.__name__}.TestCase.settings, or use @{value} as a decorator " + f"Assigning {cls.__name__}.settings = {descr} does nothing. Assign " + f"to {cls.__name__}.TestCase.settings, or use @{descr} as a decorator " f"on the {cls.__name__} class." ) return super().__setattr__(name, value) @@ -255,6 +256,15 @@ class RuleBasedStateMachine(metaclass=StateMachineMeta): self._initialize_rules_to_run = copy(self.initialize_rules()) self._rules_strategy = RuleStrategy(self) + if isinstance(s := vars(type(self)).get("settings"), Settings): + tname = type(self).__name__ + descr = f"settings({s.show_changed()})" + raise InvalidDefinition( + f"Assigning settings = {descr} as a class attribute does nothing. " + f"Assign to {tname}.TestCase.settings, or use @{descr} as a decorator " + f"on the {tname} class." + ) + def _pretty_print(self, value): if isinstance(value, VarReference): return value.name @@ -440,7 +450,7 @@ class BundleReferenceStrategy(SearchStrategy): # Shrink towards the right rather than the left. This makes it easier # to delete data generated earlier, as when the error is towards the # end there can be a lot of hard to remove padding. - position = cu.integer_range(data, 0, len(bundle) - 1, center=len(bundle)) + position = data.draw_integer(0, len(bundle) - 1, shrink_towards=len(bundle)) if self.consume: return bundle.pop(position) # pragma: no cover # coverage is flaky here else: diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py index 5d9c9617aa3..2bee499ac2e 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/collections.py @@ -260,7 +260,7 @@ class UniqueSampledListStrategy(UniqueListStrategy): while remaining and should_draw.more(): i = len(remaining) - 1 - j = cu.integer_range(data, 0, i) + j = data.draw_integer(0, i) if j != i: remaining[i], remaining[j] = remaining[j], remaining[i] value = self.element_strategy._transform(remaining.pop()) @@ -330,7 +330,7 @@ class FixedAndOptionalKeysDictStrategy(SearchStrategy): data, min_size=0, max_size=len(remaining), average_size=len(remaining) / 2 ) while should_draw.more(): - j = cu.integer_range(data, 0, len(remaining) - 1) + j = data.draw_integer(0, len(remaining) - 1) remaining[-1], remaining[j] = remaining[j], remaining[-1] key = remaining.pop() result[key] = data.draw(self.optional[key]) diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py index 3624e7e239a..b8c56015877 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/core.py @@ -74,11 +74,7 @@ from hypothesis.internal.compat import ( get_type_hints, is_typed_named_tuple, ) -from hypothesis.internal.conjecture.utils import ( - calc_label_from_cls, - check_sample, - integer_range, -) +from hypothesis.internal.conjecture.utils import calc_label_from_cls, check_sample from hypothesis.internal.entropy import get_seeder_and_restorer from hypothesis.internal.floats import float_of from hypothesis.internal.reflection import ( @@ -1707,7 +1703,7 @@ class PermutationStrategy(SearchStrategy): # change. We don't consider the last element as it's always a no-op. result = list(self.values) for i in range(len(result) - 1): - j = integer_range(data, i, len(result) - 1) + j = data.draw_integer(i, len(result) - 1) result[i], result[j] = result[j], result[i] return result @@ -2011,7 +2007,7 @@ def shared( @composite def _maybe_nil_uuids(draw, uuid): # Equivalent to `random_uuids | just(...)`, with a stronger bias to the former. - if draw(data()).conjecture_data.draw_bits(6) == 63: + if draw(data()).conjecture_data.draw_boolean(1 / 64): return UUID("00000000-0000-0000-0000-000000000000") return uuid diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py index a23779711f7..abf77c5026f 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/datetime.py @@ -16,7 +16,6 @@ from pathlib import Path from typing import Optional from hypothesis.errors import InvalidArgument -from hypothesis.internal.conjecture import utils from hypothesis.internal.validation import check_type, check_valid_interval from hypothesis.strategies._internal.core import sampled_from from hypothesis.strategies._internal.misc import just, none @@ -113,9 +112,9 @@ def draw_capped_multipart( if name == "day" and not cap_high: _, high = monthrange(**result) if name == "year": - val = utils.integer_range(data, low, high, 2000) + val = data.draw_integer(low, high, shrink_towards=2000) else: - val = utils.integer_range(data, low, high) + val = data.draw_integer(low, high) result[name] = val cap_low = cap_low and val == low cap_high = cap_high and val == high @@ -125,7 +124,7 @@ def draw_capped_multipart( # the logic above, and be very sensitive to the specific timezone # (at the cost of efficient shrinking and mutation), so at least for # now we stick with the status quo and generate it independently. - result["fold"] = utils.integer_range(data, 0, 1) + result["fold"] = data.draw_integer(0, 1) return result @@ -312,7 +311,7 @@ class TimedeltaStrategy(SearchStrategy): for name in ("days", "seconds", "microseconds"): low = getattr(self.min_value if low_bound else dt.timedelta.min, name) high = getattr(self.max_value if high_bound else dt.timedelta.max, name) - val = utils.integer_range(data, low, high, 0) + val = data.draw_integer(low, high) result[name] = val low_bound = low_bound and val == low high_bound = high_bound and val == high diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py index 66d648cce19..cf72b5c10bf 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/featureflags.py @@ -52,7 +52,7 @@ class FeatureFlags: # features will be enabled. This is so that we shrink in the direction # of more features being enabled. if self.__data is not None: - self.__p_disabled = data.draw_bits(8) / 255.0 + self.__p_disabled = data.draw_integer(0, 255) / 255.0 else: # If data is None we're in example mode so all that matters is the # enabled/disabled lists above. We set this up so that everything @@ -81,8 +81,8 @@ class FeatureFlags: # of the test case where we originally decided, the next point at # which we make this decision just makes the decision it previously # made. - is_disabled = cu.biased_coin( - self.__data, self.__p_disabled, forced=self.__is_disabled.get(name) + is_disabled = self.__data.draw_boolean( + self.__p_disabled, forced=self.__is_disabled.get(name) ) self.__is_disabled[name] = is_disabled data.stop_example() diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py index ae66a2c1b23..302f6f92b55 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/numbers.py @@ -11,23 +11,20 @@ import math from decimal import Decimal from fractions import Fraction -from sys import float_info from typing import Literal, Optional, Union from hypothesis.control import reject from hypothesis.errors import InvalidArgument -from hypothesis.internal.conjecture import floats as flt, utils as d -from hypothesis.internal.conjecture.utils import calc_label_from_name from hypothesis.internal.filtering import ( get_float_predicate_bounds, get_integer_predicate_bounds, ) from hypothesis.internal.floats import ( + SMALLEST_SUBNORMAL, float_of, float_to_int, int_to_float, is_negative, - make_float_clamper, next_down, next_down_normal, next_up, @@ -48,7 +45,6 @@ from hypothesis.strategies._internal.utils import cacheable, defines_strategy # See https://github.com/python/mypy/issues/3186 - numbers.Real is wrong! Real = Union[int, float, Fraction, Decimal] -ONE_BOUND_INTEGERS_LABEL = d.calc_label_from_name("trying a one-bound int allowing 0") class IntegersStrategy(SearchStrategy): @@ -69,34 +65,14 @@ class IntegersStrategy(SearchStrategy): return f"integers({self.start}, {self.end})" def do_draw(self, data): - if self.start is None and self.end is None: - return d.unbounded_integers(data) - - if self.start is None: - if self.end <= 0: - return self.end - abs(d.unbounded_integers(data)) - else: - probe = self.end + 1 - while self.end < probe: - data.start_example(ONE_BOUND_INTEGERS_LABEL) - probe = d.unbounded_integers(data) - data.stop_example(discard=self.end < probe) - return probe - - if self.end is None: - if self.start >= 0: - return self.start + abs(d.unbounded_integers(data)) - else: - probe = self.start - 1 - while probe < self.start: - data.start_example(ONE_BOUND_INTEGERS_LABEL) - probe = d.unbounded_integers(data) - data.stop_example(discard=probe < self.start) - return probe - # For bounded integers, make the bounds and near-bounds more likely. forced = None - if self.end - self.start > 127: + if ( + self.end is not None + and self.start is not None + and self.end - self.start > 127 + ): + bits = data.draw_integer(0, 127) forced = { 122: self.start, 123: self.start, @@ -104,9 +80,11 @@ class IntegersStrategy(SearchStrategy): 125: self.end, 126: self.start + 1, 127: self.end - 1, - }.get(data.draw_bits(7)) + }.get(bits) - return d.integer_range(data, self.start, self.end, center=0, forced=forced) + return data.draw_integer( + min_value=self.start, max_value=self.end, forced=forced + ) def filter(self, condition): if condition is math.isfinite: @@ -166,54 +144,6 @@ def integers( return IntegersStrategy(min_value, max_value) -SMALLEST_SUBNORMAL = next_up(0.0) -SIGNALING_NAN = int_to_float(0x7FF8_0000_0000_0001) # nonzero mantissa -assert math.isnan(SIGNALING_NAN) -assert math.copysign(1, SIGNALING_NAN) == 1 - -NASTY_FLOATS = sorted( - [ - 0.0, - 0.5, - 1.1, - 1.5, - 1.9, - 1.0 / 3, - 10e6, - 10e-6, - 1.175494351e-38, - next_up(0.0), - float_info.min, - float_info.max, - 3.402823466e38, - 9007199254740992, - 1 - 10e-6, - 2 + 10e-6, - 1.192092896e-07, - 2.2204460492503131e-016, - ] - + [2.0**-n for n in (24, 14, 149, 126)] # minimum (sub)normals for float16,32 - + [float_info.min / n for n in (2, 10, 1000, 100_000)] # subnormal in float64 - + [math.inf, math.nan] * 5 - + [SIGNALING_NAN], - key=flt.float_to_lex, -) -NASTY_FLOATS = list(map(float, NASTY_FLOATS)) -NASTY_FLOATS.extend([-x for x in NASTY_FLOATS]) - -FLOAT_STRATEGY_DO_DRAW_LABEL = calc_label_from_name( - "getting another float in FloatStrategy" -) - - -def _sign_aware_lte(x: float, y: float) -> bool: - """Less-than-or-equals, but strictly orders -0.0 and 0.0""" - if x == 0.0 == y: - return math.copysign(1.0, x) <= math.copysign(1.0, y) - else: - return x <= y - - class FloatStrategy(SearchStrategy): """A strategy for floating point numbers.""" @@ -246,38 +176,6 @@ class FloatStrategy(SearchStrategy): self.allow_nan = allow_nan self.smallest_nonzero_magnitude = smallest_nonzero_magnitude - boundary_values = [ - min_value, - next_up(min_value), - min_value + 1, - max_value - 1, - next_down(max_value), - max_value, - ] - self.nasty_floats = [ - f for f in NASTY_FLOATS + boundary_values if self.permitted(f) - ] - weights = [0.2 * len(self.nasty_floats)] + [0.8] * len(self.nasty_floats) - self.sampler = d.Sampler(weights) if self.nasty_floats else None - - self.pos_clamper = self.neg_clamper = None - if _sign_aware_lte(0.0, max_value): - pos_min = max(min_value, smallest_nonzero_magnitude) - allow_zero = _sign_aware_lte(min_value, 0.0) - self.pos_clamper = make_float_clamper( - pos_min, max_value, allow_zero=allow_zero - ) - if _sign_aware_lte(min_value, -0.0): - neg_max = min(max_value, -smallest_nonzero_magnitude) - allow_zero = _sign_aware_lte(-0.0, max_value) - self.neg_clamper = make_float_clamper( - -neg_max, -min_value, allow_zero=allow_zero - ) - - self.forced_sign_bit: Optional[int] = None - if (self.pos_clamper is None) != (self.neg_clamper is None): - self.forced_sign_bit = 1 if self.neg_clamper else 0 - def __repr__(self): return "{}(min_value={}, max_value={}, allow_nan={}, smallest_nonzero_magnitude={})".format( self.__class__.__name__, @@ -287,39 +185,13 @@ class FloatStrategy(SearchStrategy): self.smallest_nonzero_magnitude, ) - def permitted(self, f): - assert isinstance(f, float) - if math.isnan(f): - return self.allow_nan - if 0 < abs(f) < self.smallest_nonzero_magnitude: - return False - return _sign_aware_lte(self.min_value, f) and _sign_aware_lte(f, self.max_value) - def do_draw(self, data): - while True: - data.start_example(FLOAT_STRATEGY_DO_DRAW_LABEL) - i = self.sampler.sample(data) if self.sampler else 0 - data.start_example(flt.DRAW_FLOAT_LABEL) - if i == 0: - result = flt.draw_float(data, forced_sign_bit=self.forced_sign_bit) - is_negative = flt.float_to_int(result) >> 63 - if is_negative: - clamped = -self.neg_clamper(-result) - else: - clamped = self.pos_clamper(result) - if clamped != result: - data.stop_example(discard=True) - data.start_example(flt.DRAW_FLOAT_LABEL) - flt.write_float(data, clamped) - result = clamped - else: - result = self.nasty_floats[i - 1] - - flt.write_float(data, result) - - data.stop_example() # (DRAW_FLOAT_LABEL) - data.stop_example() # (FLOAT_STRATEGY_DO_DRAW_LABEL) - return result + return data.draw_float( + min_value=self.min_value, + max_value=self.max_value, + allow_nan=self.allow_nan, + smallest_nonzero_magnitude=self.smallest_nonzero_magnitude, + ) def filter(self, condition): # Handle a few specific weird cases. @@ -331,10 +203,13 @@ class FloatStrategy(SearchStrategy): smallest_nonzero_magnitude=self.smallest_nonzero_magnitude, ) if condition is math.isinf: - permitted_infs = [x for x in (-math.inf, math.inf) if self.permitted(x)] - if not permitted_infs: - return nothing() - return SampledFromStrategy(permitted_infs) + if permitted_infs := [ + x + for x in (-math.inf, math.inf) + if self.min_value <= x <= self.max_value + ]: + return SampledFromStrategy(permitted_infs) + return nothing() if condition is math.isnan: if not self.allow_nan: return nothing() @@ -639,7 +514,7 @@ class NanStrategy(SearchStrategy): def do_draw(self, data): # Nans must have all exponent bits and the first mantissa bit set, so # we generate by taking 64 random bits and setting the required ones. - sign_bit = data.draw_bits(1) << 63 + sign_bit = int(data.draw_boolean()) << 63 nan_bits = float_to_int(math.nan) - mantissa_bits = data.draw_bits(52) + mantissa_bits = data.draw_integer(0, 2**52 - 1) return int_to_float(sign_bit | nan_bits | mantissa_bits) diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py index f7afd4d9578..41c9feb5234 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/random.py @@ -16,7 +16,6 @@ from typing import Any, Dict import attr from hypothesis.control import should_note -from hypothesis.internal.conjecture import utils as cu from hypothesis.internal.reflection import define_function_signature from hypothesis.reporting import report from hypothesis.strategies._internal.core import ( @@ -233,7 +232,7 @@ class ArtificialRandom(HypothesisRandom): return self.__convert_result(method, kwargs, result) if method == "_randbelow": - result = cu.integer_range(self.__data, 0, kwargs["n"] - 1) + result = self.__data.draw_integer(0, kwargs["n"] - 1) elif method in ("betavariate", "random"): result = self.__data.draw(UNIFORM) elif method == "uniform": @@ -266,18 +265,18 @@ class ArtificialRandom(HypothesisRandom): if (start - stop) % step == 0: endpoint -= 1 - i = cu.integer_range(self.__data, 0, endpoint) + i = self.__data.draw_integer(0, endpoint) result = start + i * step else: - result = cu.integer_range(self.__data, start, stop - 1) + result = self.__data.draw_integer(start, stop - 1) elif method == "randint": - result = cu.integer_range(self.__data, kwargs["a"], kwargs["b"]) + result = self.__data.draw_integer(kwargs["a"], kwargs["b"]) # New in Python 3.12, so not taken by our coverage job elif method == "binomialvariate": # pragma: no cover - result = cu.integer_range(self.__data, 0, kwargs["n"]) + result = self.__data.draw_integer(0, kwargs["n"]) elif method == "choice": seq = kwargs["seq"] - result = cu.integer_range(self.__data, 0, len(seq) - 1) + result = self.__data.draw_integer(0, len(seq) - 1) elif method == "choices": k = kwargs["k"] result = self.__data.draw( @@ -309,14 +308,14 @@ class ArtificialRandom(HypothesisRandom): ) elif method == "getrandbits": - result = self.__data.draw_bits(kwargs["n"]) + result = self.__data.draw_integer(0, 2 ** kwargs["n"] - 1) elif method == "triangular": low = normalize_zero(kwargs["low"]) high = normalize_zero(kwargs["high"]) mode = normalize_zero(kwargs["mode"]) if mode is None: result = self.__data.draw(floats(low, high)) - elif self.__data.draw_bits(1): + elif self.__data.draw_boolean(0.5): result = self.__data.draw(floats(mode, high)) else: result = self.__data.draw(floats(low, mode)) @@ -436,7 +435,7 @@ class RandomStrategy(SearchStrategy): def do_draw(self, data): if self.__use_true_random: - seed = data.draw_bits(64) + seed = data.draw_integer(0, 2**64 - 1) return TrueRandom(seed=seed, note_method_calls=self.__note_method_calls) else: return ArtificialRandom( diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/regex.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/regex.py index a7f99c4ae56..23c3aedc82a 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/regex.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/regex.py @@ -58,7 +58,9 @@ BYTES_LOOKUP = { } -GROUP_CACHE_STRATEGY = st.shared(st.builds(dict), key="hypothesis.regex.group_cache") +GROUP_CACHE_STRATEGY: st.SearchStrategy[dict] = st.shared( + st.builds(dict), key="hypothesis.regex.group_cache" +) @st.composite diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py index 415c9092ee6..6b68a1d353d 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strategies.py @@ -544,7 +544,7 @@ class SampledFromStrategy(SearchStrategy): # Start with ordinary rejection sampling. It's fast if it works, and # if it doesn't work then it was only a small amount of overhead. for _ in range(3): - i = cu.integer_range(data, 0, len(self.elements) - 1) + i = data.draw_integer(0, len(self.elements) - 1) if i not in known_bad_indices: element = self.get_element(i) if element is not filter_not_satisfied: @@ -558,10 +558,6 @@ class SampledFromStrategy(SearchStrategy): if not max_good_indices: return filter_not_satisfied - # Figure out the bit-length of the index that we will write back after - # choosing an allowed element. - write_length = len(self.elements).bit_length() - # Impose an arbitrary cutoff to prevent us from wasting too much time # on very large element lists. cutoff = 10000 @@ -570,7 +566,7 @@ class SampledFromStrategy(SearchStrategy): # Before building the list of allowed indices, speculatively choose # one of them. We don't yet know how many allowed indices there will be, # so this choice might be out-of-bounds, but that's OK. - speculative_index = cu.integer_range(data, 0, max_good_indices - 1) + speculative_index = data.draw_integer(0, max_good_indices - 1) # Calculate the indices of allowed values, so that we can choose one # of them at random. But if we encounter the speculatively-chosen one, @@ -585,14 +581,14 @@ class SampledFromStrategy(SearchStrategy): if len(allowed) > speculative_index: # Early-exit case: We reached the speculative index, so # we just return the corresponding element. - data.draw_bits(write_length, forced=i) + data.draw_integer(0, len(self.elements) - 1, forced=i) return element # The speculative index didn't work out, but at this point we've built # and can choose from the complete list of allowed indices and elements. if allowed: i, element = cu.choice(data, allowed) - data.draw_bits(write_length, forced=i) + data.draw_integer(0, len(self.elements) - 1, forced=i) return element # If there are no allowed indices, the filter couldn't be satisfied. return filter_not_satisfied diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py index 22952b6fe32..021ce3c6e6c 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/strings.py @@ -15,7 +15,6 @@ from functools import lru_cache from hypothesis.errors import HypothesisWarning, InvalidArgument from hypothesis.internal import charmap -from hypothesis.internal.conjecture.utils import biased_coin, integer_range from hypothesis.internal.intervalsets import IntervalSet from hypothesis.strategies._internal.collections import ListStrategy from hypothesis.strategies._internal.lazy import unwrap_strategies @@ -29,10 +28,6 @@ class OneCharStringStrategy(SearchStrategy): assert isinstance(intervals, IntervalSet) self.intervals = intervals self._force_repr = force_repr - self.zero_point = self.intervals.index_above(ord("0")) - self.Z_point = min( - self.intervals.index_above(ord("Z")), len(self.intervals) - 1 - ) @classmethod def from_characters_args( @@ -78,44 +73,20 @@ class OneCharStringStrategy(SearchStrategy): return self._force_repr or f"OneCharStringStrategy({self.intervals!r})" def do_draw(self, data): - if len(self.intervals) > 256: - if biased_coin(data, 0.2): - i = integer_range(data, 256, len(self.intervals) - 1) - else: - i = integer_range(data, 0, 255) - else: - i = integer_range(data, 0, len(self.intervals) - 1) - - i = self.rewrite_integer(i) - - return chr(self.intervals[i]) - - def rewrite_integer(self, i): - # We would like it so that, where possible, shrinking replaces - # characters with simple ascii characters, so we rejig this - # bit so that the smallest values are 0, 1, 2, ..., Z. - # - # Imagine that numbers are laid out as abc0yyyZ... - # this rearranges them so that they are laid out as - # 0yyyZcba..., which gives a better shrinking order. - if i <= self.Z_point: - # We want to rewrite the integers [0, n] inclusive - # to [zero_point, Z_point]. - n = self.Z_point - self.zero_point - if i <= n: - i += self.zero_point - else: - # We want to rewrite the integers [n + 1, Z_point] to - # [zero_point, 0] (reversing the order so that codepoints below - # zero_point shrink upwards). - i = self.zero_point - (i - n) - assert i < self.zero_point - assert 0 <= i <= self.Z_point - return i + return data.draw_string(self.intervals, min_size=1, max_size=1) class TextStrategy(ListStrategy): def do_draw(self, data): + # if our element strategy is OneCharStringStrategy, we can skip the + # ListStrategy draw and jump right to our nice IR string draw. + # Doing so for user-provided element strategies is not correct in + # general, as they may define a different distribution than our IR. + elems = unwrap_strategies(self.element_strategy) + if isinstance(elems, OneCharStringStrategy): + return data.draw_string( + elems.intervals, min_size=self.min_size, max_size=self.max_size + ) return "".join(super().do_draw(data)) def __repr__(self): diff --git a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py index 0a5fae4d9b4..d9790814150 100644 --- a/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py +++ b/contrib/python/hypothesis/py3/hypothesis/strategies/_internal/types.py @@ -713,7 +713,7 @@ _global_type_lookup.update( st.binary(), st.integers(0, 255), # As with Reversible, we tuplize this for compatibility with Hashable. - st.lists(st.integers(0, 255)).map(tuple), # type: ignore + st.lists(st.integers(0, 255)).map(tuple), ), typing.BinaryIO: st.builds(io.BytesIO, st.binary()), typing.TextIO: st.builds(io.StringIO, st.text()), diff --git a/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt b/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt index 931aeb432bd..5402c8d9cb1 100644 --- a/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt +++ b/contrib/python/hypothesis/py3/hypothesis/vendor/tlds-alpha-by-domain.txt @@ -1,4 +1,4 @@ -# Version 2023111100, Last Updated Sat Nov 11 07:07:01 2023 UTC +# Version 2023111800, Last Updated Sat Nov 18 07:07:01 2023 UTC AAA AARP ABB @@ -379,7 +379,6 @@ ES ESQ ESTATE ET -ETISALAT EU EUROVISION EUS @@ -1370,7 +1369,6 @@ XN--MGB9AWBF XN--MGBA3A3EJT XN--MGBA3A4F16A XN--MGBA7C0BBN0A -XN--MGBAAKC7DVF XN--MGBAAM7A8H XN--MGBAB2BD XN--MGBAH1A3HJKRD diff --git a/contrib/python/hypothesis/py3/hypothesis/version.py b/contrib/python/hypothesis/py3/hypothesis/version.py index 24995e5689f..158c609209c 100644 --- a/contrib/python/hypothesis/py3/hypothesis/version.py +++ b/contrib/python/hypothesis/py3/hypothesis/version.py @@ -8,5 +8,5 @@ # 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/. -__version_info__ = (6, 89, 0) +__version_info__ = (6, 90, 0) __version__ = ".".join(map(str, __version_info__)) diff --git a/contrib/python/hypothesis/py3/ya.make b/contrib/python/hypothesis/py3/ya.make index 2ff1945862d..b9abaef8bd6 100644 --- a/contrib/python/hypothesis/py3/ya.make +++ b/contrib/python/hypothesis/py3/ya.make @@ -2,7 +2,7 @@ PY3_LIBRARY() -VERSION(6.89.0) +VERSION(6.90.0) LICENSE(MPL-2.0) |
