diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..1989c527d0 --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +Improves sharing of some internal cache behavior. diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py index aacf020c72..47711f7c44 100644 --- a/hypothesis-python/src/hypothesis/core.py +++ b/hypothesis-python/src/hypothesis/core.py @@ -25,6 +25,7 @@ from collections import defaultdict from collections.abc import Coroutine, Generator, Hashable, Iterable, Sequence from functools import partial +from inspect import Parameter from random import Random from typing import ( TYPE_CHECKING, @@ -633,7 +634,13 @@ class Stuff: given_kwargs: dict = attr.ib(factory=dict) -def process_arguments_to_given(wrapped_test, arguments, kwargs, given_kwargs, params): +def process_arguments_to_given( + wrapped_test: Any, + arguments: Sequence[object], + kwargs: dict[str, object], + given_kwargs: dict[str, SearchStrategy], + params: dict[str, Parameter], +) -> tuple[Sequence[object], dict[str, object], Stuff]: selfy = None arguments, kwargs = convert_positional_arguments(wrapped_test, arguments, kwargs) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/choice.py b/hypothesis-python/src/hypothesis/internal/conjecture/choice.py index 491a99bd24..16db7ca8f2 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/choice.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/choice.py @@ -68,7 +68,7 @@ class BooleanKWargs(TypedDict): ChoiceKwargsT: "TypeAlias" = Union[ IntegerKWargs, FloatKWargs, StringKWargs, BytesKWargs, BooleanKWargs ] -ChoiceNameT: "TypeAlias" = Literal["integer", "string", "boolean", "float", "bytes"] +ChoiceTypeT: "TypeAlias" = Literal["integer", "string", "boolean", "float", "bytes"] ChoiceKeyT: "TypeAlias" = Union[ int, str, bytes, tuple[Literal["bool"], bool], tuple[Literal["float"], int] ] @@ -84,6 +84,100 @@ def __attrs_post_init__(self) -> None: assert self.count > 0 +@attr.s(slots=True, repr=False, eq=False) +class ChoiceNode: + type: ChoiceTypeT = attr.ib() + value: ChoiceT = attr.ib() + kwargs: ChoiceKwargsT = attr.ib() + was_forced: bool = attr.ib() + index: Optional[int] = attr.ib(default=None) + + def copy( + self, + *, + with_value: Optional[ChoiceT] = None, + with_kwargs: Optional[ChoiceKwargsT] = None, + ) -> "ChoiceNode": + # we may want to allow this combination in the future, but for now it's + # a footgun. + if self.was_forced: + assert with_value is None, "modifying a forced node doesn't make sense" + # explicitly not copying index. node indices are only assigned via + # ExampleRecord. This prevents footguns with relying on stale indices + # after copying. + return ChoiceNode( + type=self.type, + value=self.value if with_value is None else with_value, + kwargs=self.kwargs if with_kwargs is None else with_kwargs, + was_forced=self.was_forced, + ) + + @property + def trivial(self) -> bool: + """ + A node is trivial if it cannot be simplified any further. This does not + mean that modifying a trivial node can't produce simpler test cases when + viewing the tree as a whole. Just that when viewing this node in + isolation, this is the simplest the node can get. + """ + if self.was_forced: + return True + + if self.type != "float": + zero_value = choice_from_index(0, self.type, self.kwargs) + return choice_equal(self.value, zero_value) + else: + kwargs = cast(FloatKWargs, self.kwargs) + min_value = kwargs["min_value"] + max_value = kwargs["max_value"] + shrink_towards = 0.0 + + if min_value == -math.inf and max_value == math.inf: + return choice_equal(self.value, shrink_towards) + + if ( + not math.isinf(min_value) + and not math.isinf(max_value) + and math.ceil(min_value) <= math.floor(max_value) + ): + # the interval contains an integer. the simplest integer is the + # one closest to shrink_towards + shrink_towards = max(math.ceil(min_value), shrink_towards) + shrink_towards = min(math.floor(max_value), shrink_towards) + return choice_equal(self.value, float(shrink_towards)) + + # the real answer here is "the value in [min_value, max_value] with + # the lowest denominator when represented as a fraction". + # It would be good to compute this correctly in the future, but it's + # also not incorrect to be conservative here. + return False + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ChoiceNode): + return NotImplemented + + return ( + self.type == other.type + and choice_equal(self.value, other.value) + and choice_kwargs_equal(self.type, self.kwargs, other.kwargs) + and self.was_forced == other.was_forced + ) + + def __hash__(self) -> int: + return hash( + ( + self.type, + choice_key(self.value), + choice_kwargs_key(self.type, self.kwargs), + self.was_forced, + ) + ) + + def __repr__(self) -> str: + forced_marker = " [forced]" if self.was_forced else "" + return f"{self.type} {self.value!r}{forced_marker} {self.kwargs!r}" + + def _size_to_index(size: int, *, alphabet_size: int) -> int: # this is the closed form of this geometric series: # for i in range(size): @@ -330,10 +424,10 @@ def choice_to_index(choice: ChoiceT, kwargs: ChoiceKwargsT) -> int: def choice_from_index( - index: int, ir_type: ChoiceNameT, kwargs: ChoiceKwargsT + index: int, choice_type: ChoiceTypeT, kwargs: ChoiceKwargsT ) -> ChoiceT: assert index >= 0 - if ir_type == "integer": + if choice_type == "integer": kwargs = cast(IntegerKWargs, kwargs) shrink_towards = kwargs["shrink_towards"] min_value = kwargs["min_value"] @@ -375,7 +469,7 @@ def choice_from_index( if index <= zigzag_index(max_value, shrink_towards=shrink_towards): return zigzag_value(index, shrink_towards=shrink_towards) return max_value - index - elif ir_type == "boolean": + elif choice_type == "boolean": kwargs = cast(BooleanKWargs, kwargs) # Ordered by [False, True]. p = kwargs["p"] @@ -391,13 +485,13 @@ def choice_from_index( assert index == 0 return only return bool(index) - elif ir_type == "bytes": + elif choice_type == "bytes": kwargs = cast(BytesKWargs, kwargs) value_b = collection_value( index, min_size=kwargs["min_size"], alphabet_size=2**8, from_order=identity ) return bytes(value_b) - elif ir_type == "string": + elif choice_type == "string": kwargs = cast(StringKWargs, kwargs) intervals = kwargs["intervals"] # _s because mypy is unhappy with reusing different-typed names in branches, @@ -409,7 +503,7 @@ def choice_from_index( from_order=intervals.char_in_shrink_order, ) return "".join(value_s) - elif ir_type == "float": + elif choice_type == "float": kwargs = cast(FloatKWargs, kwargs) sign = -1 if index >> 64 else 1 result = sign * lex_to_float(index & ((1 << 64) - 1)) @@ -485,20 +579,22 @@ def choice_equal(choice1: ChoiceT, choice2: ChoiceT) -> bool: def choice_kwargs_equal( - ir_type: ChoiceNameT, kwargs1: ChoiceKwargsT, kwargs2: ChoiceKwargsT + choice_type: ChoiceTypeT, kwargs1: ChoiceKwargsT, kwargs2: ChoiceKwargsT ) -> bool: - return choice_kwargs_key(ir_type, kwargs1) == choice_kwargs_key(ir_type, kwargs2) + return choice_kwargs_key(choice_type, kwargs1) == choice_kwargs_key( + choice_type, kwargs2 + ) -def choice_kwargs_key(ir_type, kwargs): - if ir_type == "float": +def choice_kwargs_key(choice_type, kwargs): + if choice_type == "float": return ( float_to_int(kwargs["min_value"]), float_to_int(kwargs["max_value"]), kwargs["allow_nan"], kwargs["smallest_nonzero_magnitude"], ) - if ir_type == "integer": + if choice_type == "integer": return ( kwargs["min_value"], kwargs["max_value"], diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index e18458182e..40d3ab60f6 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -15,7 +15,7 @@ from enum import IntEnum from functools import cached_property from random import Random -from typing import TYPE_CHECKING, Any, NoReturn, Optional, TypeVar, Union, cast +from typing import TYPE_CHECKING, Any, NoReturn, Optional, TypeVar, Union import attr @@ -26,16 +26,14 @@ BooleanKWargs, BytesKWargs, ChoiceKwargsT, - ChoiceNameT, + ChoiceNode, ChoiceT, ChoiceTemplate, + ChoiceTypeT, FloatKWargs, IntegerKWargs, StringKWargs, - choice_equal, choice_from_index, - choice_key, - choice_kwargs_equal, choice_kwargs_key, choice_permitted, choices_size, @@ -85,8 +83,8 @@ def __getattr__(name: str) -> Any: T = TypeVar("T") TargetObservations = dict[str, Union[int, float]] -# index, ir_type, kwargs, forced -MisalignedAt: "TypeAlias" = tuple[int, ChoiceNameT, ChoiceKwargsT, Optional[ChoiceT]] +# index, choice_type, kwargs, forced value +MisalignedAt: "TypeAlias" = tuple[int, ChoiceTypeT, ChoiceKwargsT, Optional[ChoiceT]] TOP_LABEL = calc_label_from_name("top") @@ -249,14 +247,14 @@ def __init__(self, examples: "Examples"): self.example_stack: list[int] = [] self.examples = examples self.example_count = 0 - self.ir_node_count = 0 + self.choice_count = 0 def run(self) -> Any: """Rerun the test case with this visitor and return the results of ``self.finish()``.""" for record in self.examples.trail: - if record == IR_NODE_RECORD: - self.ir_node_count += 1 + if record == CHOICE_RECORD: + self.choice_count += 1 elif record >= START_EXAMPLE_RECORD: self.__push(record - START_EXAMPLE_RECORD) else: @@ -296,7 +294,7 @@ def finish(self) -> Any: STOP_EXAMPLE_NO_DISCARD_RECORD = 2 START_EXAMPLE_RECORD = 3 -IR_NODE_RECORD = calc_label_from_name("ir draw record") +CHOICE_RECORD = calc_label_from_name("ir draw record") class ExampleRecord: @@ -314,13 +312,13 @@ def __init__(self) -> None: self.labels: list[int] = [] self.__index_of_labels: Optional[dict[int, int]] = {} self.trail = IntList() - self.nodes: list[IRNode] = [] + self.nodes: list[ChoiceNode] = [] def freeze(self) -> None: self.__index_of_labels = None - def record_ir_draw(self) -> None: - self.trail.append(IR_NODE_RECORD) + def record_choice(self) -> None: + self.trail.append(CHOICE_RECORD) def start_example(self, label: int) -> None: assert self.__index_of_labels is not None @@ -345,10 +343,10 @@ def __init__(self, examples: "Examples") -> None: self.ends = IntList.of_length(len(self.examples)) def start_example(self, i: int, label_index: int) -> None: - self.starts[i] = self.ir_node_count + self.starts[i] = self.choice_count def stop_example(self, i: int, *, discarded: bool) -> None: - self.ends[i] = self.ir_node_count + self.ends[i] = self.choice_count def finish(self) -> tuple[IntList, IntList]: return (self.starts, self.ends) @@ -566,101 +564,6 @@ def draw_boolean( pass -@attr.s(slots=True, repr=False, eq=False) -class IRNode: - ir_type: ChoiceNameT = attr.ib() - value: ChoiceT = attr.ib() - kwargs: ChoiceKwargsT = attr.ib() - was_forced: bool = attr.ib() - index: Optional[int] = attr.ib(default=None) - - def copy( - self, - *, - with_value: Optional[ChoiceT] = None, - with_kwargs: Optional[ChoiceKwargsT] = None, - ) -> "IRNode": - # we may want to allow this combination in the future, but for now it's - # a footgun. - if self.was_forced: - assert with_value is None, "modifying a forced node doesn't make sense" - # explicitly not copying index. node indices are only assigned via - # ExampleRecord. This prevents footguns with relying on stale indices - # after copying. - return IRNode( - ir_type=self.ir_type, - value=self.value if with_value is None else with_value, - kwargs=self.kwargs if with_kwargs is None else with_kwargs, - was_forced=self.was_forced, - ) - - @property - def trivial(self) -> bool: - """ - A node is trivial if it cannot be simplified any further. This does not - mean that modifying a trivial node can't produce simpler test cases when - viewing the tree as a whole. Just that when viewing this node in - isolation, this is the simplest the node can get. - """ - if self.was_forced: - return True - - if self.ir_type != "float": - zero_value = choice_from_index(0, self.ir_type, self.kwargs) - return choice_equal(self.value, zero_value) - else: - kwargs = cast(FloatKWargs, self.kwargs) - min_value = kwargs["min_value"] - max_value = kwargs["max_value"] - shrink_towards = 0.0 - - if min_value == -math.inf and max_value == math.inf: - return choice_equal(self.value, shrink_towards) - - if ( - not math.isinf(min_value) - and not math.isinf(max_value) - and math.ceil(min_value) <= math.floor(max_value) - ): - # the interval contains an integer. the simplest integer is the - # one closest to shrink_towards - shrink_towards = max(math.ceil(min_value), shrink_towards) - shrink_towards = min(math.floor(max_value), shrink_towards) - return choice_equal(self.value, float(shrink_towards)) - - # the real answer here is "the value in [min_value, max_value] with - # the lowest denominator when represented as a fraction". - # It would be good to compute this correctly in the future, but it's - # also not incorrect to be conservative here. - return False - - def __eq__(self, other: object) -> bool: - if not isinstance(other, IRNode): - return NotImplemented - - return ( - self.ir_type == other.ir_type - and choice_equal(self.value, other.value) - and choice_kwargs_equal(self.ir_type, self.kwargs, other.kwargs) - and self.was_forced == other.was_forced - ) - - def __hash__(self) -> int: - return hash( - ( - self.ir_type, - choice_key(self.value), - choice_kwargs_key(self.ir_type, self.kwargs), - self.was_forced, - ) - ) - - def __repr__(self) -> str: - # repr to avoid "BytesWarning: str() on a bytes instance" for bytes nodes - forced_marker = " [forced]" if self.was_forced else "" - return f"{self.ir_type} {self.value!r}{forced_marker} {self.kwargs!r}" - - @attr.s(slots=True) class ConjectureResult: """Result class storing the parts of ConjectureData that we @@ -669,7 +572,7 @@ class ConjectureResult: status: Status = attr.ib() interesting_origin: Optional[InterestingOrigin] = attr.ib() - nodes: tuple[IRNode, ...] = attr.ib(eq=False, repr=False) + nodes: tuple[ChoiceNode, ...] = attr.ib(eq=False, repr=False) length: int = attr.ib() output: str = attr.ib() extra_information: Optional[ExtraInformation] = attr.ib() @@ -801,7 +704,7 @@ def __init__( self.extra_information = ExtraInformation() self.prefix = prefix - self.nodes: tuple[IRNode, ...] = () + self.nodes: tuple[ChoiceNode, ...] = () self.misaligned_at: Optional[MisalignedAt] = None self.start_example(TOP_LABEL) @@ -841,7 +744,7 @@ def choices(self) -> tuple[ChoiceT, ...]: # value to be returned, but we don't want to treat that block as fixed for # e.g. the shrinker. - def _draw(self, ir_type, kwargs, *, observe, forced): + def _draw(self, choice_type, kwargs, *, observe, forced): # this is somewhat redundant with the length > max_length check at the # end of the function, but avoids trying to use a null self.random when # drawing past the node of a ConjectureData.for_choices data. @@ -853,9 +756,9 @@ def _draw(self, ir_type, kwargs, *, observe, forced): self.mark_overrun() if observe and self.prefix is not None and self.index < len(self.prefix): - value = self._pop_choice(ir_type, kwargs, forced=forced) + value = self._pop_choice(choice_type, kwargs, forced=forced) elif forced is None: - value = getattr(self.provider, f"draw_{ir_type}")(**kwargs) + value = getattr(self.provider, f"draw_{choice_type}")(**kwargs) if forced is not None: value = forced @@ -880,12 +783,12 @@ def _draw(self, ir_type, kwargs, *, observe, forced): # bring that back (ABOVE the choice sequence layer) in the future. # # See https://github.com/HypothesisWorks/hypothesis/issues/3926. - if ir_type == "float" and math.isnan(value): + if choice_type == "float" and math.isnan(value): value = int_to_float(float_to_int(value)) if observe: was_forced = forced is not None - getattr(self.observer, f"draw_{ir_type}")( + getattr(self.observer, f"draw_{choice_type}")( value, kwargs=kwargs, was_forced=was_forced ) size = 0 if self.provider.avoid_realization else choices_size([value]) @@ -895,14 +798,14 @@ def _draw(self, ir_type, kwargs, *, observe, forced): ) self.mark_overrun() - node = IRNode( - ir_type=ir_type, + node = ChoiceNode( + type=choice_type, value=value, kwargs=kwargs, was_forced=was_forced, index=len(self.nodes), ) - self.__example_record.record_ir_draw() + self.__example_record.record_choice() self.nodes += (node,) self.length += size @@ -1035,13 +938,13 @@ def draw_boolean( kwargs: BooleanKWargs = self._pooled_kwargs("boolean", {"p": p}) return self._draw("boolean", kwargs, observe=observe, forced=forced) - def _pooled_kwargs(self, ir_type, kwargs): + def _pooled_kwargs(self, choice_type, kwargs): """Memoize common dictionary objects to reduce memory pressure.""" # caching runs afoul of nondeterminism checks if self.provider.avoid_realization: return kwargs - key = (ir_type, *choice_kwargs_key(ir_type, kwargs)) + key = (choice_type, *choice_kwargs_key(choice_type, kwargs)) try: return POOLED_KWARGS_CACHE[key] except KeyError: @@ -1049,7 +952,11 @@ def _pooled_kwargs(self, ir_type, kwargs): return kwargs def _pop_choice( - self, ir_type: ChoiceNameT, kwargs: ChoiceKwargsT, *, forced: Optional[ChoiceT] + self, + choice_type: ChoiceTypeT, + kwargs: ChoiceKwargsT, + *, + forced: Optional[ChoiceT], ) -> ChoiceT: assert self.prefix is not None # checked in _draw @@ -1067,12 +974,12 @@ def _pop_choice( if node.type == "simplest": if isinstance(self.provider, HypothesisProvider): try: - choice: ChoiceT = choice_from_index(0, ir_type, kwargs) + choice: ChoiceT = choice_from_index(0, choice_type, kwargs) except ChoiceTooLarge: self.mark_overrun() else: # give alternative backends control over these draws - choice = getattr(self.provider, f"draw_{ir_type}")(**kwargs) + choice = getattr(self.provider, f"draw_{choice_type}")(**kwargs) else: raise NotImplementedError @@ -1083,7 +990,7 @@ def _pop_choice( return choice choice = value - node_ir_type = { + node_choice_type = { str: "string", float: "float", int: "integer", @@ -1106,10 +1013,10 @@ def _pop_choice( # # When the choice sequence becomes misaligned, we generate a new value of the # type and kwargs the strategy expects. - if node_ir_type != ir_type or not choice_permitted(choice, kwargs): + if node_choice_type != choice_type or not choice_permitted(choice, kwargs): # only track first misalignment for now. if self.misaligned_at is None: - self.misaligned_at = (self.index, ir_type, kwargs, forced) + self.misaligned_at = (self.index, choice_type, kwargs, forced) try: # Fill in any misalignments with index 0 choices. An alternative to # this is using the index of the misaligned choice instead @@ -1127,7 +1034,7 @@ def _pop_choice( # slipping to high-complexity values are common. Though arguably # we may want to expand a bit beyond *just* the simplest choice. # (we could for example consider sampling choices from index 0-10). - choice = choice_from_index(0, ir_type, kwargs) + choice = choice_from_index(0, choice_type, kwargs) except ChoiceTooLarge: # should really never happen with a 0-index choice, but let's be safe. self.mark_overrun() @@ -1364,6 +1271,6 @@ def mark_overrun(self) -> NoReturn: self.conclude_test(Status.OVERRUN) -def draw_choice(ir_type, kwargs, *, random): +def draw_choice(choice_type, kwargs, *, random): cd = ConjectureData(random=random) - return getattr(cd.provider, f"draw_{ir_type}")(**kwargs) + return getattr(cd.provider, f"draw_{choice_type}")(**kwargs) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/datatree.py b/hypothesis-python/src/hypothesis/internal/conjecture/datatree.py index 426dda9dbb..70a6c5219b 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/datatree.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/datatree.py @@ -26,8 +26,8 @@ BooleanKWargs, BytesKWargs, ChoiceKwargsT, - ChoiceNameT, ChoiceT, + ChoiceTypeT, FloatKWargs, IntegerKWargs, StringKWargs, @@ -81,10 +81,10 @@ def _repr_pretty_(self, p: "RepresentationPrinter", cycle: bool) -> None: def _node_pretty( - ir_type: ChoiceNameT, value: ChoiceT, kwargs: ChoiceKwargsT, *, forced: bool + choice_type: ChoiceTypeT, value: ChoiceT, kwargs: ChoiceKwargsT, *, forced: bool ) -> str: forced_marker = " [forced]" if forced else "" - return f"{ir_type} {value!r}{forced_marker} {kwargs}" + return f"{choice_type} {value!r}{forced_marker} {kwargs}" @attr.s(slots=True) @@ -93,12 +93,12 @@ class Branch: to drawn.""" kwargs: ChoiceKwargsT = attr.ib() - ir_type: ChoiceNameT = attr.ib() + choice_type: ChoiceTypeT = attr.ib() children: dict[ChoiceT, "TreeNode"] = attr.ib(repr=False) @property def max_children(self) -> int: - max_children = compute_max_children(self.ir_type, self.kwargs) + max_children = compute_max_children(self.choice_type, self.kwargs) assert max_children > 0 return max_children @@ -107,7 +107,7 @@ def _repr_pretty_(self, p: "RepresentationPrinter", cycle: bool) -> None: for i, (value, child) in enumerate(self.children.items()): if i > 0: p.break_() - p.text(_node_pretty(self.ir_type, value, self.kwargs, forced=False)) + p.text(_node_pretty(self.choice_type, value, self.kwargs, forced=False)) with p.indent(2): p.break_() p.pretty(child) @@ -179,8 +179,8 @@ def _count_distinct_strings(*, alphabet_size: int, min_size: int, max_size: int) return sum(alphabet_size**k for k in range(min_size, max_size + 1)) -def compute_max_children(ir_type: ChoiceNameT, kwargs: ChoiceKwargsT) -> int: - if ir_type == "integer": +def compute_max_children(choice_type: ChoiceTypeT, kwargs: ChoiceKwargsT) -> int: + if choice_type == "integer": kwargs = cast(IntegerKWargs, kwargs) min_value = kwargs["min_value"] max_value = kwargs["max_value"] @@ -198,19 +198,19 @@ def compute_max_children(ir_type: ChoiceNameT, kwargs: ChoiceKwargsT) -> int: # direction we want. ((2**128 - 1) // 2) + 1 == 2 ** 127 assert (min_value is None) ^ (max_value is None) return 2**127 - elif ir_type == "boolean": + elif choice_type == "boolean": kwargs = cast(BooleanKWargs, kwargs) p = kwargs["p"] # probabilities of 0 or 1 (or effectively 0 or 1) only have one choice. if p <= 2 ** (-64) or p >= (1 - 2 ** (-64)): return 1 return 2 - elif ir_type == "bytes": + elif choice_type == "bytes": kwargs = cast(BytesKWargs, kwargs) return _count_distinct_strings( alphabet_size=2**8, min_size=kwargs["min_size"], max_size=kwargs["max_size"] ) - elif ir_type == "string": + elif choice_type == "string": kwargs = cast(StringKWargs, kwargs) min_size = kwargs["min_size"] max_size = kwargs["max_size"] @@ -229,7 +229,7 @@ def compute_max_children(ir_type: ChoiceNameT, kwargs: ChoiceKwargsT) -> int: return _count_distinct_strings( alphabet_size=len(intervals), min_size=min_size, max_size=max_size ) - elif ir_type == "float": + elif choice_type == "float": kwargs = cast(FloatKWargs, kwargs) min_value_f = kwargs["min_value"] max_value_f = kwargs["max_value"] @@ -261,12 +261,12 @@ def compute_max_children(ir_type: ChoiceNameT, kwargs: ChoiceKwargsT) -> int: count += 1 return count - raise NotImplementedError(f"unhandled ir_type {ir_type}") + raise NotImplementedError(f"unhandled choice_type {choice_type}") # In theory, this is a strict superset of the functionality of compute_max_children; # -# assert len(all_children(ir_type, kwargs)) == compute_max_children(ir_type, kwargs) +# assert len(all_children(choice_type, kwargs)) == compute_max_children(choice_type, kwargs) # # In practice, we maintain two distinct implementations for efficiency and space # reasons. If you just need the number of children, it is cheaper to use @@ -278,11 +278,11 @@ def _floats_between(a: float, b: float) -> Generator[float, None, None]: def all_children( - ir_type: ChoiceNameT, kwargs: ChoiceKwargsT + choice_type: ChoiceTypeT, kwargs: ChoiceKwargsT ) -> Generator[ChoiceT, None, None]: - if ir_type != "float": - for index in range(compute_max_children(ir_type, kwargs)): - yield choice_from_index(index, ir_type, kwargs) + if choice_type != "float": + for index in range(compute_max_children(choice_type, kwargs)): + yield choice_from_index(index, choice_type, kwargs) else: kwargs = cast(FloatKWargs, kwargs) # the float ordering is not injective (because of resampling @@ -331,7 +331,7 @@ class TreeNode: Conceptually, you can unfold a single TreeNode storing n values in its lists into a sequence of n nodes, each a child of the last. In other words, - (kwargs[i], values[i], ir_types[i]) corresponds to the single node at index + (kwargs[i], values[i], choice_types[i]) corresponds to the single node at index i. Note that if a TreeNode represents a choice (i.e. the nodes cannot be compacted @@ -384,11 +384,11 @@ class TreeNode: └───┘ └───┘ """ - # The kwargs, value, and ir_types of the nodes stored here. These always + # The kwargs, value, and choice_types of the nodes stored here. These always # have the same length. The values at index i belong to node i. kwargs: list[ChoiceKwargsT] = attr.ib(factory=list) values: list[ChoiceT] = attr.ib(factory=list) - ir_types: list[ChoiceNameT] = attr.ib(factory=list) + choice_types: list[ChoiceTypeT] = attr.ib(factory=list) # The indices of nodes which had forced values. # @@ -445,22 +445,24 @@ def split_at(self, i: int) -> None: key = self.values[i] child = TreeNode( - ir_types=self.ir_types[i + 1 :], + choice_types=self.choice_types[i + 1 :], kwargs=self.kwargs[i + 1 :], values=self.values[i + 1 :], transition=self.transition, ) self.transition = Branch( - kwargs=self.kwargs[i], ir_type=self.ir_types[i], children={key: child} + kwargs=self.kwargs[i], + choice_type=self.choice_types[i], + children={key: child}, ) if self.__forced is not None: child.__forced = {j - i - 1 for j in self.__forced if j > i} self.__forced = {j for j in self.__forced if j < i} child.check_exhausted() - del self.ir_types[i:] + del self.choice_types[i:] del self.values[i:] del self.kwargs[i:] - assert len(self.values) == len(self.kwargs) == len(self.ir_types) == i + assert len(self.values) == len(self.kwargs) == len(self.choice_types) == i def check_exhausted(self) -> bool: """ @@ -509,13 +511,15 @@ def check_exhausted(self) -> bool: def _repr_pretty_(self, p: "RepresentationPrinter", cycle: bool) -> None: assert cycle is False indent = 0 - for i, (ir_type, kwargs, value) in enumerate( - zip(self.ir_types, self.kwargs, self.values) + for i, (choice_type, kwargs, value) in enumerate( + zip(self.choice_types, self.kwargs, self.values) ): with p.indent(indent): if i > 0: p.break_() - p.text(_node_pretty(ir_type, value, kwargs, forced=i in self.forced)) + p.text( + _node_pretty(choice_type, value, kwargs, forced=i in self.forced) + ) indent += 2 with p.indent(indent): @@ -539,7 +543,7 @@ class DataTree: DataTree tracks the following: - - Draws, at the ir level (with some ir_type, e.g. "integer") + - Drawn choices in the typed choice sequence - ConjectureData.draw_integer() - ConjectureData.draw_float() - ConjectureData.draw_string() @@ -548,8 +552,8 @@ class DataTree: - Test conclusions (with some Status, e.g. Status.VALID) - ConjectureData.conclude_test() - A DataTree is — surprise — a *tree*. A node in this tree is either a draw with - some value, a test conclusion with some Status, or a special `Killed` value, + A DataTree is — surprise — a *tree*. A node in this tree is either a choice draw + with some value, a test conclusion with some Status, or a special `Killed` value, which denotes that further draws may exist beyond this node but should not be considered worth exploring when generating novel prefixes. A node is a leaf iff it is a conclusion or Killed. @@ -694,8 +698,8 @@ def generate_novel_prefix(self, random: Random) -> tuple[ChoiceT, ...]: assert not self.is_exhausted prefix = [] - def append_choice(ir_type: ChoiceNameT, choice: ChoiceT) -> None: - if ir_type == "float": + def append_choice(choice_type: ChoiceTypeT, choice: ChoiceT) -> None: + if choice_type == "float": assert isinstance(choice, int) choice = int_to_float(choice) prefix.append(choice) @@ -703,17 +707,19 @@ def append_choice(ir_type: ChoiceNameT, choice: ChoiceT) -> None: current_node = self.root while True: assert not current_node.is_exhausted - for i, (ir_type, kwargs, value) in enumerate( - zip(current_node.ir_types, current_node.kwargs, current_node.values) + for i, (choice_type, kwargs, value) in enumerate( + zip(current_node.choice_types, current_node.kwargs, current_node.values) ): if i in current_node.forced: - append_choice(ir_type, value) + append_choice(choice_type, value) else: attempts = 0 while True: if attempts <= 10: try: - node_value = self._draw(ir_type, kwargs, random=random) + node_value = self._draw( + choice_type, kwargs, random=random + ) except StopTest: # pragma: no cover # it is possible that drawing from a fresh data can # overrun BUFFER_SIZE, due to eg unlucky rejection sampling @@ -722,15 +728,15 @@ def append_choice(ir_type: ChoiceNameT, choice: ChoiceT) -> None: continue else: node_value = self._draw_from_cache( - ir_type, kwargs, key=id(current_node), random=random + choice_type, kwargs, key=id(current_node), random=random ) if node_value != value: - append_choice(ir_type, node_value) + append_choice(choice_type, node_value) break attempts += 1 self._reject_child( - ir_type, kwargs, child=node_value, key=id(current_node) + choice_type, kwargs, child=node_value, key=id(current_node) ) # We've now found a value that is allowed to # vary, so what follows is not fixed. @@ -747,27 +753,33 @@ def append_choice(ir_type: ChoiceNameT, choice: ChoiceT) -> None: if attempts <= 10: try: node_value = self._draw( - branch.ir_type, branch.kwargs, random=random + branch.choice_type, branch.kwargs, random=random ) except StopTest: # pragma: no cover attempts += 1 continue else: node_value = self._draw_from_cache( - branch.ir_type, branch.kwargs, key=id(branch), random=random + branch.choice_type, + branch.kwargs, + key=id(branch), + random=random, ) try: child = branch.children[node_value] except KeyError: - append_choice(branch.ir_type, node_value) + append_choice(branch.choice_type, node_value) return tuple(prefix) if not child.is_exhausted: - append_choice(branch.ir_type, node_value) + append_choice(branch.choice_type, node_value) current_node = child break attempts += 1 self._reject_child( - branch.ir_type, branch.kwargs, child=node_value, key=id(branch) + branch.choice_type, + branch.kwargs, + child=node_value, + key=id(branch), ) # We don't expect this assertion to ever fire, but coverage @@ -798,24 +810,26 @@ def simulate_test_function(self, data: ConjectureData) -> None: tree. This will likely change in future.""" node = self.root - def draw(ir_type, kwargs, *, forced=None, convert_forced=True): - if ir_type == "float" and forced is not None and convert_forced: + def draw(choice_type, kwargs, *, forced=None, convert_forced=True): + if choice_type == "float" and forced is not None and convert_forced: forced = int_to_float(forced) - draw_func = getattr(data, f"draw_{ir_type}") + draw_func = getattr(data, f"draw_{choice_type}") value = draw_func(**kwargs, forced=forced) - if ir_type == "float": + if choice_type == "float": value = float_to_int(value) return value try: while True: - for i, (ir_type, kwargs, previous) in enumerate( - zip(node.ir_types, node.kwargs, node.values) + for i, (choice_type, kwargs, previous) in enumerate( + zip(node.choice_types, node.kwargs, node.values) ): v = draw( - ir_type, kwargs, forced=previous if i in node.forced else None + choice_type, + kwargs, + forced=previous if i in node.forced else None, ) if v != previous: raise PreviouslyUnseenBehaviour @@ -825,7 +839,7 @@ def draw(ir_type, kwargs, *, forced=None, convert_forced=True): elif node.transition is None: raise PreviouslyUnseenBehaviour elif isinstance(node.transition, Branch): - v = draw(node.transition.ir_type, node.transition.kwargs) + v = draw(node.transition.choice_type, node.transition.kwargs) try: node = node.transition.children[v] except KeyError as err: @@ -841,11 +855,11 @@ def new_observer(self): return TreeRecordingObserver(self) def _draw( - self, ir_type: ChoiceNameT, kwargs: ChoiceKwargsT, *, random: Random + self, choice_type: ChoiceTypeT, kwargs: ChoiceKwargsT, *, random: Random ) -> ChoiceT: from hypothesis.internal.conjecture.data import draw_choice - value = draw_choice(ir_type, kwargs, random=random) + value = draw_choice(choice_type, kwargs, random=random) # using floats as keys into branch.children breaks things, because # e.g. hash(0.0) == hash(-0.0) would collide as keys when they are # in fact distinct child branches. @@ -854,12 +868,12 @@ def _draw( # float key is in its bits form (as a key into branch.children) and # when it is in its float form (as a value we want to write to the # buffer), and converting between the two forms as appropriate. - if ir_type == "float": + if choice_type == "float": value = float_to_int(value) return value def _get_children_cache( - self, ir_type: ChoiceNameT, kwargs: ChoiceKwargsT, *, key: ChoiceT + self, choice_type: ChoiceTypeT, kwargs: ChoiceKwargsT, *, key: ChoiceT ) -> ChildrenCacheValueT: # cache the state of the children generator per node/branch (passed as # `key` here), such that we track which children we've already tried @@ -871,7 +885,7 @@ def _get_children_cache( # with. Whenever we need to top up this list, we will draw a new value # from the generator. if key not in self._children_cache: - generator = all_children(ir_type, kwargs) + generator = all_children(choice_type, kwargs) children: list[ChoiceT] = [] rejected: set[ChoiceT] = set() self._children_cache[key] = (generator, children, rejected) @@ -880,14 +894,14 @@ def _get_children_cache( def _draw_from_cache( self, - ir_type: ChoiceNameT, + choice_type: ChoiceTypeT, kwargs: ChoiceKwargsT, *, key: ChoiceT, random: Random, ) -> ChoiceT: (generator, children, rejected) = self._get_children_cache( - ir_type, kwargs, key=key + choice_type, kwargs, key=key ) # Keep a stock of 100 potentially-valid children at all times. # This number is chosen to balance memory/speed vs randomness. Ideally @@ -897,7 +911,7 @@ def _draw_from_cache( # annoying. if len(children) < 100: # pragma: no branch for v in generator: - if ir_type == "float": + if choice_type == "float": assert isinstance(v, float) v = float_to_int(v) if v in rejected: @@ -910,14 +924,14 @@ def _draw_from_cache( def _reject_child( self, - ir_type: ChoiceNameT, + choice_type: ChoiceTypeT, kwargs: ChoiceKwargsT, *, child: ChoiceT, key: ChoiceT, ) -> None: (_generator, children, rejected) = self._get_children_cache( - ir_type, kwargs, key=key + choice_type, kwargs, key=key ) rejected.add(child) # we remove a child from the list of possible children *only* when it is @@ -978,7 +992,7 @@ def draw_boolean( def draw_value( self, - ir_type: ChoiceNameT, + choice_type: ChoiceTypeT, value: ChoiceT, *, was_forced: bool, @@ -991,9 +1005,9 @@ def draw_value( if isinstance(value, float): value = float_to_int(value) - assert len(node.kwargs) == len(node.values) == len(node.ir_types) + assert len(node.kwargs) == len(node.values) == len(node.choice_types) if i < len(node.values): - if ir_type != node.ir_types[i] or kwargs != node.kwargs[i]: + if choice_type != node.choice_types[i] or kwargs != node.kwargs[i]: raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG) # Note that we don't check whether a previously # forced value is now free. That will be caught @@ -1014,7 +1028,7 @@ def draw_value( else: trans = node.transition if trans is None: - node.ir_types.append(ir_type) + node.choice_types.append(choice_type) node.kwargs.append(kwargs) node.values.append(value) if was_forced: @@ -1035,7 +1049,7 @@ def draw_value( # An alternative is not writing such choices to the tree at # all, and thus guaranteeing that each node has at least 2 max # children. - if compute_max_children(ir_type, kwargs) == 1 and not was_forced: + if compute_max_children(choice_type, kwargs) == 1 and not was_forced: node.split_at(i) assert isinstance(node.transition, Branch) self.__current_node = node.transition.children[value] @@ -1047,7 +1061,7 @@ def draw_value( raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG) else: assert isinstance(trans, Branch), trans - if ir_type != trans.ir_type or kwargs != trans.kwargs: + if choice_type != trans.choice_type or kwargs != trans.kwargs: raise FlakyStrategyDefinition(_FLAKY_STRAT_MSG) try: self.__current_node = trans.children[value] diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py index fa8860ba33..48d9b6ac72 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py @@ -37,6 +37,7 @@ from hypothesis.internal.conjecture.choice import ( ChoiceKeyT, ChoiceKwargsT, + ChoiceNode, ChoiceT, ChoiceTemplate, choices_key, @@ -45,7 +46,6 @@ ConjectureData, ConjectureResult, DataObserver, - IRNode, Overrun, Status, _Overrun, @@ -495,7 +495,7 @@ def test_function(self, data: ConjectureData) -> None: "integer": int, "boolean": bool, "bytes": bytes, - }[node.ir_type] + }[node.type] if type(value) is not expected_type: raise HypothesisException( f"expected {expected_type} from " @@ -1431,8 +1431,8 @@ def new_shrinker( ) def passing_choice_sequences( - self, prefix: Sequence[IRNode] = () - ) -> frozenset[tuple[IRNode, ...]]: + self, prefix: Sequence[ChoiceNode] = () + ) -> frozenset[tuple[ChoiceNode, ...]]: """Return a collection of choice sequence nodes which cause the test to pass. Optionally restrict this by a certain prefix, which is useful for explain mode. """ diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/optimiser.py b/hypothesis-python/src/hypothesis/internal/conjecture/optimiser.py index 69d7690d56..17aaec17d7 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/optimiser.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/optimiser.py @@ -108,14 +108,14 @@ def hill_climb(self) -> None: # we can only (sensibly & easily) define hill climbing for # numeric-style nodes. It's not clear hill-climbing a string is # useful, for instance. - if node.ir_type not in {"integer", "float", "bytes", "boolean"}: + if node.type not in {"integer", "float", "bytes", "boolean"}: continue def attempt_replace(k: int) -> bool: """ Try replacing the current node in the current best test case with a value which is "k times larger", where the exact notion - of "larger" depends on the ir_type. + of "larger" depends on the choice_type. Note that we use the *current* best and not the one we started with. This helps ensure that if we luck into a good draw when making @@ -131,10 +131,10 @@ def attempt_replace(k: int) -> bool: return False # pragma: no cover new_choice: ChoiceT - if node.ir_type in {"integer", "float"}: + if node.type in {"integer", "float"}: assert isinstance(node.value, (int, float)) new_choice = node.value + k - elif node.ir_type == "boolean": + elif node.type == "boolean": assert isinstance(node.value, bool) if abs(k) > 1: return False @@ -145,7 +145,7 @@ def attempt_replace(k: int) -> bool: if k == 0: # pragma: no cover new_choice = node.value else: - assert node.ir_type == "bytes" + assert node.type == "bytes" assert isinstance(node.value, bytes) v = int_from_bytes(node.value) # can't go below zero for bytes diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py index e114484d6d..62a42326d1 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py @@ -16,6 +16,7 @@ import attr from hypothesis.internal.conjecture.choice import ( + ChoiceNode, ChoiceT, choice_equal, choice_from_index, @@ -27,7 +28,6 @@ ConjectureData, ConjectureResult, Examples, - IRNode, Status, _Overrun, draw_choice, @@ -61,7 +61,7 @@ ShrinkPredicateT: "TypeAlias" = Callable[[Union[ConjectureResult, _Overrun]], bool] -def sort_key(nodes: Sequence[IRNode]) -> tuple[int, tuple[int, ...]]: +def sort_key(nodes: Sequence[ChoiceNode]) -> tuple[int, tuple[int, ...]]: """Returns a sort key such that "simpler" choice sequences are smaller than "more complicated" ones. @@ -404,17 +404,17 @@ def cached_test_function_ir(self, nodes): self.check_calls() return result - def consider_new_tree(self, tree: Sequence[IRNode]) -> bool: - tree = tree[: len(self.nodes)] + def consider_new_nodes(self, nodes: Sequence[ChoiceNode]) -> bool: + nodes = nodes[: len(self.nodes)] - if startswith(tree, self.nodes): + if startswith(nodes, self.nodes): return True - if sort_key(self.nodes) < sort_key(tree): + if sort_key(self.nodes) < sort_key(nodes): return False previous = self.shrink_target - self.cached_test_function_ir(tree) + self.cached_test_function_ir(nodes) return previous is not self.shrink_target def incorporate_test_data(self, data): @@ -540,9 +540,7 @@ def explain(self) -> None: for i in range(start, end): node = nodes[i] if not node.was_forced: - value = draw_choice( - node.ir_type, node.kwargs, random=self.random - ) + value = draw_choice(node.type, node.kwargs, random=self.random) node = node.copy(with_value=value) replacement.append(node.value) @@ -689,7 +687,7 @@ def reduce_each_alternative(self): nodes = self.shrink_target.nodes node = nodes[i] if ( - node.ir_type == "integer" + node.type == "integer" and not node.was_forced and node.value <= 10 and node.kwargs["min_value"] == 0 @@ -721,7 +719,7 @@ def reduce_each_alternative(self): zero_node = zero_attempt.nodes[j] orig_node = nodes[j] if ( - zero_node.ir_type != orig_node.ir_type + zero_node.type != orig_node.type or not choice_permitted( orig_node.value, zero_node.kwargs ) @@ -759,7 +757,7 @@ def try_lower_node_as_alternative(self, i, v): initial_ex = initial.examples[j] attempt_ex = random_attempt.examples[j] contents = random_attempt.nodes[attempt_ex.start : attempt_ex.end] - self.consider_new_tree(nodes[:i] + contents + nodes[initial_ex.end :]) + self.consider_new_nodes(nodes[:i] + contents + nodes[initial_ex.end :]) if initial is not self.shrink_target: return True return False @@ -865,7 +863,7 @@ def fixate_shrink_passes(self, passes): passes.sort(key=reordering.__getitem__) @property - def nodes(self) -> tuple[IRNode, ...]: + def nodes(self) -> tuple[ChoiceNode, ...]: return self.shrink_target.nodes @property @@ -939,7 +937,7 @@ def descendants(): assert ancestor.end >= descendant.end assert descendant.choice_count < ancestor.choice_count - self.consider_new_tree( + self.consider_new_nodes( self.nodes[: ancestor.start] + self.nodes[descendant.start : descendant.end] + self.nodes[ancestor.end :] @@ -984,7 +982,7 @@ def lower_common_node_offset(self): changed = [] for i in sorted(self.__changed_nodes): node = self.nodes[i] - if node.trivial or node.ir_type != "integer": + if node.trivial or node.type != "integer": continue changed.append(node) @@ -1008,7 +1006,7 @@ def offset_node(node, n): ) def consider(n, sign): - return self.consider_new_tree( + return self.consider_new_nodes( replace_all( st.nodes, [ @@ -1043,14 +1041,14 @@ def __changed_nodes(self) -> set[int]: assert sort_key(new_target.nodes) < sort_key(prev_target.nodes) if len(prev_nodes) != len(new_nodes) or any( - n1.ir_type != n2.ir_type for n1, n2 in zip(prev_nodes, new_nodes) + n1.type != n2.type for n1, n2 in zip(prev_nodes, new_nodes) ): # should we check kwargs are equal as well? self.__all_changed_nodes = set() else: assert len(prev_nodes) == len(new_nodes) for i, (n1, n2) in enumerate(zip(prev_nodes, new_nodes)): - assert n1.ir_type == n2.ir_type + assert n1.type == n2.type if not choice_equal(n1.value, n2.value): self.__all_changed_nodes.add(i) @@ -1139,16 +1137,16 @@ def try_shrinking_nodes(self, nodes, n): # case of this function of preserving from the right instead of # preserving from the left. see test_can_shrink_variable_string_draws. - (index, attempt_ir_type, attempt_kwargs, _attempt_forced) = ( + (index, attempt_choice_type, attempt_kwargs, _attempt_forced) = ( attempt.misaligned_at ) node = self.nodes[index] - if node.ir_type != attempt_ir_type: + if node.type != attempt_choice_type: return False # pragma: no cover if node.was_forced: return False # pragma: no cover - if node.ir_type in {"string", "bytes"}: + if node.type in {"string", "bytes"}: # if the size *increased*, we would have to guess what to pad with # in order to try fixing up this attempt. Just give up. if node.kwargs["min_size"] <= attempt_kwargs["min_size"]: @@ -1158,7 +1156,7 @@ def try_shrinking_nodes(self, nodes, n): # the size decreased in our attempt. Try again, but replace with # the min_size that we would have gotten, and truncate the value # to that size by removing any elements past min_size. - return self.consider_new_tree( + return self.consider_new_nodes( initial_attempt[: node.index] + [ initial_attempt[node.index].copy( @@ -1212,7 +1210,7 @@ def try_shrinking_nodes(self, nodes, n): for u, v in sorted(regions_to_delete, key=lambda x: x[1] - x[0], reverse=True): try_with_deleted = initial_attempt[:u] + initial_attempt[v:] - if self.consider_new_tree(try_with_deleted): + if self.consider_new_nodes(try_with_deleted): return True return False @@ -1255,16 +1253,16 @@ def remove_discarded(self): for u, v in reversed(discarded): del attempt[u:v] - if not self.consider_new_tree(tuple(attempt)): + if not self.consider_new_nodes(tuple(attempt)): return False return True @derived_value # type: ignore def duplicated_nodes(self): - """Returns a list of nodes grouped (ir_type, value).""" + """Returns a list of nodes grouped (choice_type, value).""" duplicates = defaultdict(list) for node in self.nodes: - duplicates[(node.ir_type, choice_key(node.value))].append(node) + duplicates[(node.type, choice_key(node.value))].append(node) return list(duplicates.values()) @defines_shrink_pass() @@ -1313,8 +1311,8 @@ def can_choose_node(node): # The motivation for the last condition is to avoid trying weird # non-shrinks where we raise one node and think we lowered another # (but didn't). - return node.ir_type in {"integer", "float"} and not ( - node.ir_type == "float" + return node.type in {"integer", "float"} and not ( + node.type == "float" and (math.isnan(node.value) or abs(node.value) >= MAX_PRECISE_INTEGER) ) @@ -1350,10 +1348,10 @@ def boost(k: int) -> bool: # if we've increased node2 to the point that we're past max precision, # give up - things have become too unstable. - if node1.ir_type == "float" and v2 >= MAX_PRECISE_INTEGER: + if node1.type == "float" and v2 >= MAX_PRECISE_INTEGER: return False - return self.consider_new_tree( + return self.consider_new_nodes( self.nodes[: node1.index] + (node1.copy(with_value=v1),) + self.nodes[node1.index + 1 : node2.index] @@ -1366,13 +1364,13 @@ def boost(k: int) -> bool: @defines_shrink_pass() def lower_integers_together(self, chooser): node1 = chooser.choose( - self.nodes, lambda n: n.ir_type == "integer" and not n.trivial + self.nodes, lambda n: n.type == "integer" and not n.trivial ) # Search up to 3 nodes ahead, to avoid quadratic time. node2 = self.nodes[ chooser.choose( range(node1.index + 1, min(len(self.nodes), node1.index + 3 + 1)), - lambda i: self.nodes[i].ir_type == "integer" + lambda i: self.nodes[i].type == "integer" and not self.nodes[i].was_forced, ) ] @@ -1385,7 +1383,7 @@ def lower_integers_together(self, chooser): shrink_towards = node1.kwargs["shrink_towards"] def consider(n): - return self.consider_new_tree( + return self.consider_new_nodes( self.nodes[: node1.index] + (node1.copy(with_value=node1.value - n),) + self.nodes[node1.index + 1 : node2.index] @@ -1397,9 +1395,9 @@ def consider(n): find_integer(lambda n: consider(n - shrink_towards)) def minimize_nodes(self, nodes): - ir_type = nodes[0].ir_type + choice_type = nodes[0].type value = nodes[0].value - # unlike ir_type and value, kwargs are *not* guaranteed to be equal among all + # unlike choice_type and value, kwargs are *not* guaranteed to be equal among all # passed nodes. We arbitrarily use the kwargs of the first node. I think # this is unsound (= leads to us trying shrinks that could not have been # generated), but those get discarded at test-time, and this enables useful @@ -1407,11 +1405,11 @@ def minimize_nodes(self, nodes): # same operation on both basically just works. kwargs = nodes[0].kwargs assert all( - node.ir_type == ir_type and choice_equal(node.value, value) + node.type == choice_type and choice_equal(node.value, value) for node in nodes ) - if ir_type == "integer": + if choice_type == "integer": shrink_towards = kwargs["shrink_towards"] # try shrinking from both sides towards shrink_towards. # we're starting from n = abs(shrink_towards - value). Because the @@ -1426,7 +1424,7 @@ def minimize_nodes(self, nodes): abs(shrink_towards - value), lambda n: self.try_shrinking_nodes(nodes, shrink_towards - n), ) - elif ir_type == "float": + elif choice_type == "float": self.try_shrinking_nodes(nodes, abs(value)) Float.shrink( abs(value), @@ -1436,18 +1434,18 @@ def minimize_nodes(self, nodes): abs(value), lambda val: self.try_shrinking_nodes(nodes, -val), ) - elif ir_type == "boolean": + elif choice_type == "boolean": # must be True, otherwise would be trivial and not selected. assert value is True # only one thing to try: false! self.try_shrinking_nodes(nodes, False) - elif ir_type == "bytes": + elif choice_type == "bytes": Bytes.shrink( value, lambda val: self.try_shrinking_nodes(nodes, val), min_size=kwargs["min_size"], ) - elif ir_type == "string": + elif choice_type == "string": String.shrink( value, lambda val: self.try_shrinking_nodes(nodes, val), @@ -1471,7 +1469,7 @@ def try_trivial_examples(self, chooser): node if node.was_forced else node.copy( - with_value=choice_from_index(0, node.ir_type, node.kwargs) + with_value=choice_from_index(0, node.type, node.kwargs) ) ) for node in nodes[ex.start : ex.end] @@ -1486,7 +1484,7 @@ def try_trivial_examples(self, chooser): if isinstance(attempt, ConjectureResult): new_ex = attempt.examples[i] new_replacement = attempt.nodes[new_ex.start : new_ex.end] - self.consider_new_tree(prefix + new_replacement + suffix) + self.consider_new_nodes(prefix + new_replacement + suffix) @defines_shrink_pass() def minimize_individual_nodes(self, chooser): @@ -1527,7 +1525,7 @@ def minimize_individual_nodes(self, chooser): # the size of the generated input, we'll try deleting things after that # node and see if the resulting attempt works. - if node.ir_type != "integer": + if node.type != "integer": # Only try this fixup logic on integer draws. Almost all size # dependencies are on integer draws, and if it's not, it's doing # something convoluted enough that it is unlikely to shrink well anyway. @@ -1584,10 +1582,10 @@ def first_example_after_node(): lambda i: self.examples[i].choice_count > 0, ) ] - self.consider_new_tree(lowered[: ex.start] + lowered[ex.end :]) + self.consider_new_nodes(lowered[: ex.start] + lowered[ex.end :]) else: node = self.nodes[chooser.choose(range(node.index + 1, len(self.nodes)))] - self.consider_new_tree(lowered[: node.index] + lowered[node.index + 1 :]) + self.consider_new_nodes(lowered[: node.index] + lowered[node.index + 1 :]) @defines_shrink_pass() def reorder_examples(self, chooser): @@ -1620,7 +1618,7 @@ def test_not_equal(x, y): Ordering.shrink( range(len(examples)), - lambda indices: self.consider_new_tree( + lambda indices: self.consider_new_nodes( replace_all( st.nodes, [ @@ -1665,7 +1663,7 @@ def run_node_program(self, i, description, original, repeats=1): else: raise NotImplementedError(f"Unrecognised command {command!r}") - return self.consider_new_tree(attempt) + return self.consider_new_nodes(attempt) def shrink_pass_family(f): diff --git a/hypothesis-python/src/hypothesis/internal/escalation.py b/hypothesis-python/src/hypothesis/internal/escalation.py index d612701ece..468f4cf7ea 100644 --- a/hypothesis-python/src/hypothesis/internal/escalation.py +++ b/hypothesis-python/src/hypothesis/internal/escalation.py @@ -24,19 +24,21 @@ from hypothesis.internal.compat import BaseExceptionGroup from hypothesis.utils.dynamicvariables import DynamicVariable +FILE_CACHE: dict[ModuleType, dict[str, bool]] = {} + def belongs_to(package: ModuleType) -> Callable[[str], bool]: if getattr(package, "__file__", None) is None: # pragma: no cover return lambda filepath: False assert package.__file__ is not None + FILE_CACHE.setdefault(package, {}) + cache = FILE_CACHE[package] root = Path(package.__file__).resolve().parent - cache: dict[type, dict[str, bool]] = {str: {}, bytes: {}} def accept(filepath: str) -> bool: - ftype = type(filepath) try: - return cache[ftype][filepath] + return cache[filepath] except KeyError: pass try: @@ -44,16 +46,13 @@ def accept(filepath: str) -> bool: result = True except Exception: result = False - cache[ftype][filepath] = result + cache[filepath] = result return result accept.__name__ = f"is_{package.__name__}_file" return accept -FILE_CACHE: dict[bytes, bool] = {} - - is_hypothesis_file = belongs_to(hypothesis) diff --git a/hypothesis-python/src/hypothesis/internal/reflection.py b/hypothesis-python/src/hypothesis/internal/reflection.py index c735c9a663..dffc5ab587 100644 --- a/hypothesis-python/src/hypothesis/internal/reflection.py +++ b/hypothesis-python/src/hypothesis/internal/reflection.py @@ -21,7 +21,7 @@ import textwrap import types import warnings -from collections.abc import MutableMapping +from collections.abc import MutableMapping, Sequence from functools import partial, wraps from io import StringIO from keyword import iskeyword @@ -214,7 +214,9 @@ def convert_keyword_arguments(function, args, kwargs): return bound.args, bound.kwargs -def convert_positional_arguments(function, args, kwargs): +def convert_positional_arguments( + function: Any, args: Sequence[object], kwargs: dict[str, object] +) -> tuple[tuple[object, ...], dict[str, object]]: """Return a tuple (new_args, new_kwargs) where all possible arguments have been moved to kwargs. diff --git a/hypothesis-python/tests/conjecture/common.py b/hypothesis-python/tests/conjecture/common.py index 79ccb7fc3f..83bcb7f174 100644 --- a/hypothesis-python/tests/conjecture/common.py +++ b/hypothesis-python/tests/conjecture/common.py @@ -18,8 +18,8 @@ from hypothesis.control import current_build_context from hypothesis.errors import InvalidArgument from hypothesis.internal.conjecture import engine as engine_module -from hypothesis.internal.conjecture.choice import ChoiceT -from hypothesis.internal.conjecture.data import ConjectureData, IRNode, Status +from hypothesis.internal.conjecture.choice import ChoiceNode, ChoiceT +from hypothesis.internal.conjecture.data import ConjectureData, Status from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.internal.conjecture.providers import COLLECTION_DEFAULT_MAX_SIZE from hypothesis.internal.conjecture.utils import calc_label_from_name @@ -349,20 +349,20 @@ def boolean_kwargs(draw, *, use_forced=False): return {"p": p, "forced": forced} -def kwargs_strategy(ir_type, strategy_kwargs=None, *, use_forced=False): +def kwargs_strategy(choice_type, strategy_kwargs=None, *, use_forced=False): strategy = { "boolean": boolean_kwargs, "integer": integer_kwargs, "float": float_kwargs, "bytes": bytes_kwargs, "string": string_kwargs, - }[ir_type] + }[choice_type] if strategy_kwargs is None: strategy_kwargs = {} - return strategy(**strategy_kwargs.get(ir_type, {}), use_forced=use_forced) + return strategy(**strategy_kwargs.get(choice_type, {}), use_forced=use_forced) -def ir_types_and_kwargs(strategy_kwargs=None, *, use_forced=False): +def choice_types_kwargs(strategy_kwargs=None, *, use_forced=False): options = ["boolean", "integer", "float", "bytes", "string"] return st.one_of( st.tuples( @@ -372,30 +372,32 @@ def ir_types_and_kwargs(strategy_kwargs=None, *, use_forced=False): ) -def draw_value(ir_type, kwargs): +def draw_value(choice_type, kwargs): data = fresh_data() - return getattr(data, f"draw_{ir_type}")(**kwargs) + return getattr(data, f"draw_{choice_type}")(**kwargs) @st.composite -def nodes(draw, *, was_forced=None, ir_types=None): - if ir_types is None: - (ir_type, kwargs) = draw(ir_types_and_kwargs()) +def nodes(draw, *, was_forced=None, choice_types=None): + if choice_types is None: + (choice_type, kwargs) = draw(choice_types_kwargs()) else: - ir_type = draw(st.sampled_from(ir_types)) - kwargs = draw(kwargs_strategy(ir_type)) + choice_type = draw(st.sampled_from(choice_types)) + kwargs = draw(kwargs_strategy(choice_type)) # ir nodes don't include forced in their kwargs. see was_forced attribute del kwargs["forced"] - value = draw_value(ir_type, kwargs) + value = draw_value(choice_type, kwargs) was_forced = draw(st.booleans()) if was_forced is None else was_forced - return IRNode(ir_type=ir_type, value=value, kwargs=kwargs, was_forced=was_forced) + return ChoiceNode( + type=choice_type, value=value, kwargs=kwargs, was_forced=was_forced + ) -def ir(*values: list[ChoiceT]) -> list[IRNode]: +def ir(*values: list[ChoiceT]) -> list[ChoiceNode]: """ For inline-creating an ir node or list of ir nodes, where you don't care about the - kwargs. This uses maximally-permissable kwargs and infers the ir_type you meant + kwargs. This uses maximally-permissable kwargs and infers the choice_type you meant based on the type of the value. You can optionally pass (value, kwargs) to as an element in order to override @@ -439,11 +441,11 @@ def ir(*values: list[ChoiceT]) -> list[IRNode]: if override_kwargs is None: override_kwargs = {} - (ir_type, kwargs) = mapping[type(value)] + (choice_type, kwargs) = mapping[type(value)] nodes.append( - IRNode( - ir_type=ir_type, + ChoiceNode( + type=choice_type, value=value, kwargs=kwargs | override_kwargs, was_forced=False, diff --git a/hypothesis-python/tests/conjecture/test_alt_backend.py b/hypothesis-python/tests/conjecture/test_alt_backend.py index 8fe0d8f568..f15dc2d297 100644 --- a/hypothesis-python/tests/conjecture/test_alt_backend.py +++ b/hypothesis-python/tests/conjecture/test_alt_backend.py @@ -252,9 +252,9 @@ def test_backend_can_shrink_floats(): # mostly a shoehorned coverage test until the shrinker is migrated to the ir # and calls cached_test_function_ir with backends consistently. @given(nodes()) -def test_new_conjecture_data_ir_with_backend(node): +def test_new_conjecture_data_with_backend(node): def test(data): - getattr(data, f"draw_{node.ir_type}")(**node.kwargs) + getattr(data, f"draw_{node.type}")(**node.kwargs) with temp_register_backend("prng", PrngProvider): runner = ConjectureRunner(test, settings=settings(backend="prng")) diff --git a/hypothesis-python/tests/conjecture/test_data_tree.py b/hypothesis-python/tests/conjecture/test_data_tree.py index 571ac07f9a..cefe014df7 100644 --- a/hypothesis-python/tests/conjecture/test_data_tree.py +++ b/hypothesis-python/tests/conjecture/test_data_tree.py @@ -405,51 +405,55 @@ def test_low_probabilities_are_still_explored(): assert prefix[0] -def _test_observed_draws_are_recorded_in_tree(ir_type): - @given(kwargs_strategy(ir_type)) +def _test_observed_draws_are_recorded_in_tree(choice_type): + @given(kwargs_strategy(choice_type)) def test(kwargs): # we currently split pseudo-choices with a single child into their # own transition, which clashes with our asserts below. If we ever # change this (say, by not writing pseudo choices to the ir at all), # this restriction can be relaxed. - assume(compute_max_children(ir_type, kwargs) > 1) + assume(compute_max_children(choice_type, kwargs) > 1) tree = DataTree() data = fresh_data(observer=tree.new_observer()) - draw_func = getattr(data, f"draw_{ir_type}") + draw_func = getattr(data, f"draw_{choice_type}") draw_func(**kwargs) assert tree.root.transition is None - assert tree.root.ir_types == [ir_type] + assert tree.root.choice_types == [choice_type] test() -def _test_non_observed_draws_are_not_recorded_in_tree(ir_type): - @given(kwargs_strategy(ir_type)) +def _test_non_observed_draws_are_not_recorded_in_tree(choice_type): + @given(kwargs_strategy(choice_type)) def test(kwargs): - assume(compute_max_children(ir_type, kwargs) > 1) + assume(compute_max_children(choice_type, kwargs) > 1) tree = DataTree() data = fresh_data(observer=tree.new_observer()) - draw_func = getattr(data, f"draw_{ir_type}") + draw_func = getattr(data, f"draw_{choice_type}") draw_func(**kwargs, observe=False) root = tree.root assert root.transition is None - assert root.kwargs == root.values == root.ir_types == [] + assert root.kwargs == root.values == root.choice_types == [] test() -@pytest.mark.parametrize("ir_type", ["integer", "float", "boolean", "string", "bytes"]) -def test_observed_ir_type_draw(ir_type): - _test_observed_draws_are_recorded_in_tree(ir_type) +@pytest.mark.parametrize( + "choice_type", ["integer", "float", "boolean", "string", "bytes"] +) +def test_observed_choice_type_draw(choice_type): + _test_observed_draws_are_recorded_in_tree(choice_type) -@pytest.mark.parametrize("ir_type", ["integer", "float", "boolean", "string", "bytes"]) -def test_non_observed_ir_type_draw(ir_type): - _test_non_observed_draws_are_not_recorded_in_tree(ir_type) +@pytest.mark.parametrize( + "choice_type", ["integer", "float", "boolean", "string", "bytes"] +) +def test_non_observed_choice_type_draw(choice_type): + _test_non_observed_draws_are_not_recorded_in_tree(choice_type) def test_can_generate_hard_values(): @@ -556,10 +560,10 @@ def test_datatree_repr(bool_kwargs, int_kwargs): def _draw(data, node, *, forced=None): - return getattr(data, f"draw_{node.ir_type}")(**node.kwargs, forced=forced) + return getattr(data, f"draw_{node.type}")(**node.kwargs, forced=forced) -@given(nodes(was_forced=True, ir_types=["float"])) +@given(nodes(was_forced=True, choice_types=["float"])) def test_simulate_forced_floats(node): tree = DataTree() diff --git a/hypothesis-python/tests/conjecture/test_engine.py b/hypothesis-python/tests/conjecture/test_engine.py index 077e8b3602..ceb60fe360 100644 --- a/hypothesis-python/tests/conjecture/test_engine.py +++ b/hypothesis-python/tests/conjecture/test_engine.py @@ -1523,12 +1523,12 @@ def test_too_slow_report(): def _draw(cd, node): - return getattr(cd, f"draw_{node.ir_type}")(**node.kwargs) + return getattr(cd, f"draw_{node.type}")(**node.kwargs) @given(nodes(was_forced=False)) def test_overruns_with_extend_are_not_cached(node): - assume(compute_max_children(node.ir_type, node.kwargs) > 100) + assume(compute_max_children(node.type, node.kwargs) > 100) def test(cd): _draw(cd, node) diff --git a/hypothesis-python/tests/conjecture/test_forced.py b/hypothesis-python/tests/conjecture/test_forced.py index a5ba9a9e56..f70bedf46c 100644 --- a/hypothesis-python/tests/conjecture/test_forced.py +++ b/hypothesis-python/tests/conjecture/test_forced.py @@ -19,7 +19,7 @@ from hypothesis.internal.conjecture.data import ConjectureData from hypothesis.internal.floats import SIGNALING_NAN, SMALLEST_SUBNORMAL -from tests.conjecture.common import fresh_data, ir_types_and_kwargs +from tests.conjecture.common import choice_types_kwargs, fresh_data @given(st.data()) @@ -130,11 +130,11 @@ def test_forced_many(data): {"min_value": -1 * math.inf, "max_value": -1 * math.inf, "forced": math.nan}, ) ) -@given(ir_types_and_kwargs(use_forced=True)) -def test_forced_values(ir_type_and_kwargs): - (ir_type, kwargs) = ir_type_and_kwargs +@given(choice_types_kwargs(use_forced=True)) +def test_forced_values(choice_type_and_kwargs): + (choice_type, kwargs) = choice_type_and_kwargs - if ir_type == "float": + if choice_type == "float": # TODO intentionally avoid triggering a bug with forcing nan values # while both min and max value have the opposite sign. # Once we fix the aforementioned bug we can remove this intentional @@ -143,13 +143,13 @@ def test_forced_values(ir_type_and_kwargs): forced = kwargs["forced"] data = fresh_data() - assert choice_equal(getattr(data, f"draw_{ir_type}")(**kwargs), forced) + assert choice_equal(getattr(data, f"draw_{choice_type}")(**kwargs), forced) # now make sure the written buffer reproduces the forced value, even without # specifying forced=. del kwargs["forced"] data = ConjectureData.for_choices(data.choices) - assert choice_equal(getattr(data, f"draw_{ir_type}")(**kwargs), forced) + assert choice_equal(getattr(data, f"draw_{choice_type}")(**kwargs), forced) @pytest.mark.parametrize("sign", [1, -1]) diff --git a/hypothesis-python/tests/conjecture/test_ir.py b/hypothesis-python/tests/conjecture/test_ir.py index e649680c4f..a4b908ee4b 100644 --- a/hypothesis-python/tests/conjecture/test_ir.py +++ b/hypothesis-python/tests/conjecture/test_ir.py @@ -25,6 +25,7 @@ ) from hypothesis.errors import StopTest from hypothesis.internal.conjecture.choice import ( + ChoiceNode, ChoiceTemplate, choice_equal, choice_from_index, @@ -35,7 +36,6 @@ from hypothesis.internal.conjecture.data import ( COLLECTION_DEFAULT_MAX_SIZE, ConjectureData, - IRNode, Status, choices_size, ) @@ -50,13 +50,13 @@ from tests.common.debug import minimal from tests.conjecture.common import ( + choice_types_kwargs, clamped_shrink_towards, draw_value, float_kw, fresh_data, integer_kw, integer_kwargs, - ir_types_and_kwargs, nodes, ) @@ -67,14 +67,14 @@ @example(("integer", integer_kw(max_value=-(2**200)))) @example(("integer", integer_kw(min_value=2**200))) @example(("integer", integer_kw(-(2**200), 2**200))) -@given(ir_types_and_kwargs()) -def test_compute_max_children_is_positive(ir_type_and_kwargs): - (ir_type, kwargs) = ir_type_and_kwargs - assert compute_max_children(ir_type, kwargs) >= 0 +@given(choice_types_kwargs()) +def test_compute_max_children_is_positive(choice_type_and_kwargs): + (choice_type, kwargs) = choice_type_and_kwargs + assert compute_max_children(choice_type, kwargs) >= 0 @pytest.mark.parametrize( - "ir_type, kwargs, count_children", + "choice_type, kwargs, count_children", [ ("integer", {"min_value": 1, "max_value": 2, "weights": {1: 0.1, 2: 0.1}}, 2), # only possibility is the empty string @@ -168,8 +168,8 @@ def test_compute_max_children_is_positive(ir_type_and_kwargs): ("float", float_kw(-3, -2, smallest_nonzero_magnitude=4.0), 0), ], ) -def test_compute_max_children(ir_type, kwargs, count_children): - assert compute_max_children(ir_type, kwargs) == count_children +def test_compute_max_children(choice_type, kwargs, count_children): + assert compute_max_children(choice_type, kwargs) == count_children @given(st.text(min_size=1, max_size=1), st.integers(0, 100)) @@ -203,11 +203,11 @@ def test_draw_string_single_interval_with_equal_bounds(s, n): # using a smallest_nonzero_magnitude which happens to filter out everything @example(("float", float_kw(1.0, 2.0, smallest_nonzero_magnitude=3.0))) @example(("integer", integer_kw(1, 2, weights={1: 0.2, 2: 0.4}))) -@given(ir_types_and_kwargs()) +@given(choice_types_kwargs()) @settings(suppress_health_check=[HealthCheck.filter_too_much]) -def test_compute_max_children_and_all_children_agree(ir_type_and_kwargs): - (ir_type, kwargs) = ir_type_and_kwargs - max_children = compute_max_children(ir_type, kwargs) +def test_compute_max_children_and_all_children_agree(choice_type_and_kwargs): + (choice_type, kwargs) = choice_type_and_kwargs + max_children = compute_max_children(choice_type, kwargs) # avoid slowdowns / OOM when reifying extremely large all_children generators. # We also hard cap at MAX_CHILDREN_EFFECTIVELY_INFINITE, because max_children @@ -215,7 +215,7 @@ def test_compute_max_children_and_all_children_agree(ir_type_and_kwargs): # all_children. cap = min(100_000, MAX_CHILDREN_EFFECTIVELY_INFINITE) assume(max_children < cap) - assert len(list(all_children(ir_type, kwargs))) == max_children + assert len(list(all_children(choice_type, kwargs))) == max_children # it's very hard to test that unbounded integer ranges agree with @@ -246,17 +246,17 @@ def test_nodes(random): data.freeze() expected_tree_nodes = ( - IRNode( - ir_type="float", value=5.0, kwargs=float_kw(-10.0, 10.0), was_forced=True + ChoiceNode( + type="float", value=5.0, kwargs=float_kw(-10.0, 10.0), was_forced=True ), - IRNode( - ir_type="boolean", + ChoiceNode( + type="boolean", value=True, kwargs={"p": 0.5}, was_forced=True, ), - IRNode( - ir_type="string", + ChoiceNode( + type="string", value="abbcccdddd", kwargs={ "intervals": IntervalSet.from_string("abcd"), @@ -265,23 +265,25 @@ def test_nodes(random): }, was_forced=True, ), - IRNode( - ir_type="bytes", + ChoiceNode( + type="bytes", value=bytes(8), kwargs={"min_size": 8, "max_size": 8}, was_forced=True, ), - IRNode(ir_type="integer", value=50, kwargs=integer_kw(0, 100), was_forced=True), + ChoiceNode( + type="integer", value=50, kwargs=integer_kw(0, 100), was_forced=True + ), ) assert data.nodes == expected_tree_nodes @given(nodes()) -def test_copy_ir_node(node): +def test_copy_choice_node(node): assert node == node assume(not node.was_forced) - new_value = draw_value(node.ir_type, node.kwargs) + new_value = draw_value(node.type, node.kwargs) # if we drew the same value as before, the node should still be equal assert (node.copy(with_value=new_value) == node) is ( choice_equal(new_value, node.value) @@ -289,7 +291,7 @@ def test_copy_ir_node(node): @given(nodes()) -def test_ir_node_equality(node): +def test_choice_node_equality(node): assert node == node # for coverage on our NotImplemented return, more than anything. assert node != 42 @@ -301,7 +303,7 @@ def test_cannot_modify_forced_nodes(node): node.copy(with_value=42) -def test_data_with_empty_ir_tree_is_overrun(): +def test_data_with_empty_choices_is_overrun(): data = ConjectureData.for_choices([]) with pytest.raises(StopTest): data.draw_integer() @@ -320,30 +322,30 @@ def test_data_with_changed_forced_value(node): data = ConjectureData.for_choices([node.value]) - draw_func = getattr(data, f"draw_{node.ir_type}") + draw_func = getattr(data, f"draw_{node.type}") kwargs = deepcopy(node.kwargs) - kwargs["forced"] = draw_value(node.ir_type, node.kwargs) + kwargs["forced"] = draw_value(node.type, node.kwargs) assume(not choice_equal(kwargs["forced"], node.value)) assert choice_equal(draw_func(**kwargs), kwargs["forced"]) # ensure we hit bare-minimum coverage for all ir types. -@example(IRNode(ir_type="float", value=0.0, kwargs=float_kw(), was_forced=True)) +@example(ChoiceNode(type="float", value=0.0, kwargs=float_kw(), was_forced=True)) @example( - IRNode( - ir_type="boolean", + ChoiceNode( + type="boolean", value=False, kwargs={"p": 0.5}, was_forced=True, ) ) @example( - IRNode(ir_type="integer", value=50, kwargs=integer_kw(50, 100), was_forced=True) + ChoiceNode(type="integer", value=50, kwargs=integer_kw(50, 100), was_forced=True) ) @example( - IRNode( - ir_type="string", + ChoiceNode( + type="string", value="aaaa", kwargs={ "intervals": IntervalSet.from_string("bcda"), @@ -354,8 +356,8 @@ def test_data_with_changed_forced_value(node): ) ) @example( - IRNode( - ir_type="bytes", + ChoiceNode( + type="bytes", value=bytes(8), kwargs={"min_size": 8, "max_size": 8}, was_forced=True, @@ -368,24 +370,24 @@ def test_data_with_same_forced_value_is_valid(node): # ir tree: v1 [was_forced=True] # drawing: [forced=v1] data = ConjectureData.for_choices([node.value]) - draw_func = getattr(data, f"draw_{node.ir_type}") + draw_func = getattr(data, f"draw_{node.type}") kwargs = deepcopy(node.kwargs) kwargs["forced"] = node.value assert choice_equal(draw_func(**kwargs), kwargs["forced"]) -@given(ir_types_and_kwargs()) +@given(choice_types_kwargs()) @settings(suppress_health_check=[HealthCheck.filter_too_much]) -def test_all_children_are_permitted_values(ir_type_and_kwargs): - (ir_type, kwargs) = ir_type_and_kwargs - max_children = compute_max_children(ir_type, kwargs) +def test_all_children_are_permitted_values(choice_type_and_kwargs): + (choice_type, kwargs) = choice_type_and_kwargs + max_children = compute_max_children(choice_type, kwargs) cap = min(100_000, MAX_CHILDREN_EFFECTIVELY_INFINITE) assume(max_children < cap) # test that all_children -> choice_permitted (but not necessarily the converse.) - for value in all_children(ir_type, kwargs): + for value in all_children(choice_type, kwargs): assert choice_permitted(value, kwargs), value @@ -458,18 +460,18 @@ def test_forced_nodes_are_trivial(node): @pytest.mark.parametrize( "node", [ - IRNode( - ir_type="float", value=5.0, kwargs=float_kw(5.0, 10.0), was_forced=False - ), - IRNode( - ir_type="float", value=0.0, kwargs=float_kw(-5.0, 5.0), was_forced=False - ), - IRNode(ir_type="float", value=0.0, kwargs=float_kw(), was_forced=False), - IRNode(ir_type="boolean", value=False, kwargs={"p": 0.5}, was_forced=False), - IRNode(ir_type="boolean", value=True, kwargs={"p": 1.0}, was_forced=False), - IRNode(ir_type="boolean", value=False, kwargs={"p": 0.0}, was_forced=False), - IRNode( - ir_type="string", + ChoiceNode( + type="float", value=5.0, kwargs=float_kw(5.0, 10.0), was_forced=False + ), + ChoiceNode( + type="float", value=0.0, kwargs=float_kw(-5.0, 5.0), was_forced=False + ), + ChoiceNode(type="float", value=0.0, kwargs=float_kw(), was_forced=False), + ChoiceNode(type="boolean", value=False, kwargs={"p": 0.5}, was_forced=False), + ChoiceNode(type="boolean", value=True, kwargs={"p": 1.0}, was_forced=False), + ChoiceNode(type="boolean", value=False, kwargs={"p": 0.0}, was_forced=False), + ChoiceNode( + type="string", value="", kwargs={ "intervals": IntervalSet.from_string("abcd"), @@ -478,8 +480,8 @@ def test_forced_nodes_are_trivial(node): }, was_forced=False, ), - IRNode( - ir_type="string", + ChoiceNode( + type="string", value="aaaa", kwargs={ "intervals": IntervalSet.from_string("bcda"), @@ -488,57 +490,57 @@ def test_forced_nodes_are_trivial(node): }, was_forced=False, ), - IRNode( - ir_type="bytes", + ChoiceNode( + type="bytes", value=bytes(8), kwargs={"min_size": 8, "max_size": 8}, was_forced=False, ), - IRNode( - ir_type="bytes", + ChoiceNode( + type="bytes", value=bytes(2), kwargs={"min_size": 2, "max_size": COLLECTION_DEFAULT_MAX_SIZE}, was_forced=False, ), - IRNode( - ir_type="integer", value=50, kwargs=integer_kw(50, 100), was_forced=False + ChoiceNode( + type="integer", value=50, kwargs=integer_kw(50, 100), was_forced=False ), - IRNode( - ir_type="integer", value=0, kwargs=integer_kw(-10, 10), was_forced=False + ChoiceNode( + type="integer", value=0, kwargs=integer_kw(-10, 10), was_forced=False ), - IRNode( - ir_type="integer", + ChoiceNode( + type="integer", value=2, kwargs=integer_kw(-10, 10, shrink_towards=2), was_forced=False, ), - IRNode( - ir_type="integer", + ChoiceNode( + type="integer", value=-10, kwargs=integer_kw(-10, 10, shrink_towards=-12), was_forced=False, ), - IRNode( - ir_type="integer", + ChoiceNode( + type="integer", value=10, kwargs=integer_kw(-10, 10, shrink_towards=12), was_forced=False, ), - IRNode(ir_type="integer", value=0, kwargs=integer_kw(), was_forced=False), - IRNode( - ir_type="integer", + ChoiceNode(type="integer", value=0, kwargs=integer_kw(), was_forced=False), + ChoiceNode( + type="integer", value=1, kwargs=integer_kw(min_value=-10, shrink_towards=1), was_forced=False, ), - IRNode( - ir_type="integer", + ChoiceNode( + type="integer", value=1, kwargs=integer_kw(max_value=10, shrink_towards=1), was_forced=False, ), - IRNode( - ir_type="integer", + ChoiceNode( + type="integer", value=1, kwargs={ "min_value": None, @@ -556,7 +558,7 @@ def test_trivial_nodes(node): @st.composite def values(draw): data = draw(st.data()).conjecture_data - return getattr(data, f"draw_{node.ir_type}")(**node.kwargs) + return getattr(data, f"draw_{node.type}")(**node.kwargs) # if we're trivial, then shrinking should produce the same value. assert choice_equal(minimal(values()), node.value) @@ -565,17 +567,17 @@ def values(draw): @pytest.mark.parametrize( "node", [ - IRNode( - ir_type="float", value=6.0, kwargs=float_kw(5.0, 10.0), was_forced=False + ChoiceNode( + type="float", value=6.0, kwargs=float_kw(5.0, 10.0), was_forced=False ), - IRNode( - ir_type="float", value=-5.0, kwargs=float_kw(-5.0, 5.0), was_forced=False + ChoiceNode( + type="float", value=-5.0, kwargs=float_kw(-5.0, 5.0), was_forced=False ), - IRNode(ir_type="float", value=1.0, kwargs=float_kw(), was_forced=False), - IRNode(ir_type="boolean", value=True, kwargs={"p": 0.5}, was_forced=False), - IRNode(ir_type="boolean", value=True, kwargs={"p": 0.99}, was_forced=False), - IRNode( - ir_type="string", + ChoiceNode(type="float", value=1.0, kwargs=float_kw(), was_forced=False), + ChoiceNode(type="boolean", value=True, kwargs={"p": 0.5}, was_forced=False), + ChoiceNode(type="boolean", value=True, kwargs={"p": 0.99}, was_forced=False), + ChoiceNode( + type="string", value="d", kwargs={ "intervals": IntervalSet.from_string("abcd"), @@ -584,28 +586,28 @@ def values(draw): }, was_forced=False, ), - IRNode( - ir_type="bytes", + ChoiceNode( + type="bytes", value=b"\x01", kwargs={"min_size": 1, "max_size": 1}, was_forced=False, ), - IRNode( - ir_type="bytes", + ChoiceNode( + type="bytes", value=bytes(1), kwargs={"min_size": 0, "max_size": COLLECTION_DEFAULT_MAX_SIZE}, was_forced=False, ), - IRNode( - ir_type="bytes", + ChoiceNode( + type="bytes", value=bytes(2), kwargs={"min_size": 1, "max_size": 10}, was_forced=False, ), - IRNode( - ir_type="integer", value=-10, kwargs=integer_kw(-10, 10), was_forced=False + ChoiceNode( + type="integer", value=-10, kwargs=integer_kw(-10, 10), was_forced=False ), - IRNode(ir_type="integer", value=42, kwargs=integer_kw(), was_forced=False), + ChoiceNode(type="integer", value=42, kwargs=integer_kw(), was_forced=False), ], ) def test_nontrivial_nodes(node): @@ -614,7 +616,7 @@ def test_nontrivial_nodes(node): @st.composite def values(draw): data = draw(st.data()).conjecture_data - return getattr(data, f"draw_{node.ir_type}")(**node.kwargs) + return getattr(data, f"draw_{node.type}")(**node.kwargs) # if we're nontrivial, then shrinking should produce something different. assert not choice_equal(minimal(values()), node.value) @@ -623,32 +625,32 @@ def values(draw): @pytest.mark.parametrize( "node", [ - IRNode( - ir_type="float", + ChoiceNode( + type="float", value=1.5, kwargs=float_kw(1.1, 1.6), was_forced=False, ), - IRNode( - ir_type="float", + ChoiceNode( + type="float", value=float(math.floor(sys.float_info.max)), kwargs=float_kw(sys.float_info.max - 1, math.inf), was_forced=False, ), - IRNode( - ir_type="float", + ChoiceNode( + type="float", value=float(math.ceil(-sys.float_info.max)), kwargs=float_kw(-math.inf, -sys.float_info.max + 1), was_forced=False, ), - IRNode( - ir_type="float", + ChoiceNode( + type="float", value=math.inf, kwargs=float_kw(math.inf, math.inf), was_forced=False, ), - IRNode( - ir_type="float", + ChoiceNode( + type="float", value=-math.inf, kwargs=float_kw(-math.inf, -math.inf), was_forced=False, @@ -663,14 +665,14 @@ def test_conservative_nontrivial_nodes(node): @st.composite def values(draw): data = draw(st.data()).conjecture_data - return getattr(data, f"draw_{node.ir_type}")(**node.kwargs) + return getattr(data, f"draw_{node.type}")(**node.kwargs) assert choice_equal(minimal(values()), node.value) @given(nodes()) -def test_ir_node_is_hashable(ir_node): - hash(ir_node) +def test_choice_node_is_hashable(node): + hash(node) @given(st.lists(nodes())) @@ -707,19 +709,19 @@ def test_node_template_single_node_overruns(): @given(nodes()) def test_node_template_simplest_is_actually_trivial(node): # TODO_IR node.trivial is sound but not complete for floats. - assume(node.ir_type != "float") + assume(node.type != "float") data = ConjectureData.for_choices((ChoiceTemplate("simplest", count=1),)) - getattr(data, f"draw_{node.ir_type}")(**node.kwargs) + getattr(data, f"draw_{node.type}")(**node.kwargs) assert len(data.nodes) == 1 assert data.nodes[0].trivial -@given(ir_types_and_kwargs()) +@given(choice_types_kwargs()) @example(("boolean", {"p": 0})) @example(("boolean", {"p": 1})) -def test_choice_indices_are_positive(ir_type_and_kwargs): - (ir_type, kwargs) = ir_type_and_kwargs - v = draw_value(ir_type, kwargs) +def test_choice_indices_are_positive(choice_type_and_kwargs): + (choice_type, kwargs) = choice_type_and_kwargs + v = draw_value(choice_type, kwargs) assert choice_to_index(v, kwargs) >= 0 @@ -731,17 +733,17 @@ def test_shrink_towards_has_index_0(kwargs): assert choice_from_index(0, "integer", kwargs) == shrink_towards -@given(ir_types_and_kwargs()) -def test_choice_to_index_injective(ir_type_and_kwargs): +@given(choice_types_kwargs()) +def test_choice_to_index_injective(choice_type_and_kwargs): # ir ordering should be injective both ways. - (ir_type, kwargs) = ir_type_and_kwargs + (choice_type, kwargs) = choice_type_and_kwargs # ...except for floats, which are hard to order bijectively. - assume(ir_type != "float") + assume(choice_type != "float") # cap to 10k so this test finishes in a reasonable amount of time - cap = min(compute_max_children(ir_type, kwargs), 10_000) + cap = min(compute_max_children(choice_type, kwargs), 10_000) indices = set() - for i, choice in enumerate(all_children(ir_type, kwargs)): + for i, choice in enumerate(all_children(choice_type, kwargs)): if i >= cap: break index = choice_to_index(choice, kwargs) @@ -749,36 +751,36 @@ def test_choice_to_index_injective(ir_type_and_kwargs): indices.add(index) -@given(ir_types_and_kwargs()) +@given(choice_types_kwargs()) @example( ( "string", {"min_size": 0, "max_size": 10, "intervals": IntervalSet.from_string("a")}, ) ) -def test_choice_from_value_injective(ir_type_and_kwargs): - (ir_type, kwargs) = ir_type_and_kwargs - assume(ir_type != "float") - cap = min(compute_max_children(ir_type, kwargs), 10_000) +def test_choice_from_value_injective(choice_type_and_kwargs): + (choice_type, kwargs) = choice_type_and_kwargs + assume(choice_type != "float") + cap = min(compute_max_children(choice_type, kwargs), 10_000) choices = set() for index in range(cap): - choice = choice_from_index(index, ir_type, kwargs) + choice = choice_from_index(index, choice_type, kwargs) assert choice not in choices choices.add(choice) -@given(ir_types_and_kwargs()) -def test_choice_index_and_value_are_inverses(ir_type_and_kwargs): - (ir_type, kwargs) = ir_type_and_kwargs - v = draw_value(ir_type, kwargs) +@given(choice_types_kwargs()) +def test_choice_index_and_value_are_inverses(choice_type_and_kwargs): + (choice_type, kwargs) = choice_type_and_kwargs + v = draw_value(choice_type, kwargs) index = choice_to_index(v, kwargs) note({"v": v, "index": index}) - choice_equal(choice_from_index(index, ir_type, kwargs), v) + choice_equal(choice_from_index(index, choice_type, kwargs), v) @pytest.mark.parametrize( - "ir_type, kwargs, choices", + "choice_type, kwargs, choices", [ ("boolean", {"p": 1}, [True]), ("boolean", {"p": 0}, [False]), @@ -800,10 +802,10 @@ def test_choice_index_and_value_are_inverses(ir_type_and_kwargs): ), ], ) -def test_choice_index_and_value_are_inverses_explicit(ir_type, kwargs, choices): +def test_choice_index_and_value_are_inverses_explicit(choice_type, kwargs, choices): for choice in choices: index = choice_to_index(choice, kwargs) - assert choice_equal(choice_from_index(index, ir_type, kwargs), choice) + assert choice_equal(choice_from_index(index, choice_type, kwargs), choice) @pytest.mark.parametrize( @@ -840,7 +842,7 @@ def test_integer_choice_index(kwargs, choices): def test_drawing_directly_matches_for_choices(nodes): data = ConjectureData.for_choices([n.value for n in nodes]) for node in nodes: - value = getattr(data, f"draw_{node.ir_type}")(**node.kwargs) + value = getattr(data, f"draw_{node.type}")(**node.kwargs) assert choice_equal(node.value, value) diff --git a/hypothesis-python/tests/conjecture/test_optimiser.py b/hypothesis-python/tests/conjecture/test_optimiser.py index 8fe8505f2d..aa6e6c37bf 100644 --- a/hypothesis-python/tests/conjecture/test_optimiser.py +++ b/hypothesis-python/tests/conjecture/test_optimiser.py @@ -13,7 +13,8 @@ import pytest from hypothesis import assume, example, given, settings -from hypothesis.internal.conjecture.data import IRNode, Status +from hypothesis.internal.conjecture.choice import ChoiceNode +from hypothesis.internal.conjecture.data import Status from hypothesis.internal.conjecture.datatree import compute_max_children from hypothesis.internal.conjecture.engine import ConjectureRunner, RunIsComplete from hypothesis.internal.entropy import deterministic_PRNG @@ -226,16 +227,16 @@ def test(data): @given(nodes()) @example( - IRNode( - ir_type="bytes", + ChoiceNode( + type="bytes", value=b"\xb1", kwargs={"min_size": 1, "max_size": 1}, was_forced=False, ) ) @example( - IRNode( - ir_type="string", + ChoiceNode( + type="string", value="aaaa", kwargs={ "min_size": 0, @@ -246,10 +247,10 @@ def test(data): ) ) @example( - IRNode(ir_type="integer", value=1, kwargs=integer_kw(0, 200), was_forced=False) + ChoiceNode(type="integer", value=1, kwargs=integer_kw(0, 200), was_forced=False) ) def test_optimising_all_nodes(node): - assume(compute_max_children(node.ir_type, node.kwargs) > 50) + assume(compute_max_children(node.type, node.kwargs) > 50) size_function = { "integer": lambda n: n, "float": lambda f: f if math.isfinite(f) else 0, @@ -260,8 +261,8 @@ def test_optimising_all_nodes(node): with deterministic_PRNG(): def test(data): - v = getattr(data, f"draw_{node.ir_type}")(**node.kwargs) - data.target_observations["v"] = size_function[node.ir_type](v) + v = getattr(data, f"draw_{node.type}")(**node.kwargs) + data.target_observations["v"] = size_function[node.type](v) runner = ConjectureRunner( test, settings=settings(TEST_SETTINGS, max_examples=50) diff --git a/hypothesis-python/tests/conjecture/test_provider_contract.py b/hypothesis-python/tests/conjecture/test_provider_contract.py index 065206d153..5b38544d69 100644 --- a/hypothesis-python/tests/conjecture/test_provider_contract.py +++ b/hypothesis-python/tests/conjecture/test_provider_contract.py @@ -20,9 +20,9 @@ from hypothesis.internal.intervalsets import IntervalSet from tests.conjecture.common import ( + choice_types_kwargs, float_kw, integer_kw, - ir_types_and_kwargs, nodes, string_kw, ) @@ -41,8 +41,8 @@ @example(b"\x00" * 100, [("float", float_kw())]) @example(b"\x00" * 100, [("bytes", {"min_size": 0, "max_size": 10})]) @example(b"\x00", [("integer", integer_kw())]) -@given(st.binary(min_size=200), st.lists(ir_types_and_kwargs())) -def test_provider_contract_bytestring(bytestring, ir_type_and_kwargs): +@given(st.binary(min_size=200), st.lists(choice_types_kwargs())) +def test_provider_contract_bytestring(bytestring, choice_type_and_kwargs): data = ConjectureData( random=None, observer=None, @@ -50,16 +50,16 @@ def test_provider_contract_bytestring(bytestring, ir_type_and_kwargs): provider_kw={"bytestring": bytestring}, ) - for ir_type, kwargs in ir_type_and_kwargs: + for choice_type, kwargs in choice_type_and_kwargs: try: - value = getattr(data, f"draw_{ir_type}")(**kwargs) + value = getattr(data, f"draw_{choice_type}")(**kwargs) except StopTest: return assert choice_permitted(value, kwargs) - kwargs["forced"] = choice_from_index(0, ir_type, kwargs) + kwargs["forced"] = choice_from_index(0, choice_type, kwargs) assert choice_equal( - kwargs["forced"], getattr(data, f"draw_{ir_type}")(**kwargs) + kwargs["forced"], getattr(data, f"draw_{choice_type}")(**kwargs) ) @@ -67,5 +67,5 @@ def test_provider_contract_bytestring(bytestring, ir_type_and_kwargs): def test_provider_contract_hypothesis(nodes, random): data = ConjectureData(random=random) for node in nodes: - value = getattr(data, f"draw_{node.ir_type}")(**node.kwargs) + value = getattr(data, f"draw_{node.type}")(**node.kwargs) assert choice_permitted(value, node.kwargs) diff --git a/hypothesis-python/tests/conjecture/test_shrinker.py b/hypothesis-python/tests/conjecture/test_shrinker.py index bb487eac9f..8056aac11f 100644 --- a/hypothesis-python/tests/conjecture/test_shrinker.py +++ b/hypothesis-python/tests/conjecture/test_shrinker.py @@ -13,7 +13,7 @@ import pytest from hypothesis import HealthCheck, assume, example, given, settings, strategies as st -from hypothesis.internal.conjecture.data import ConjectureData, IRNode +from hypothesis.internal.conjecture.data import ChoiceNode, ConjectureData from hypothesis.internal.conjecture.datatree import compute_max_children from hypothesis.internal.conjecture.engine import ConjectureRunner from hypothesis.internal.conjecture.shrinker import ( @@ -199,7 +199,7 @@ def test_permits_but_ignores_raising_order(monkeypatch): lambda runner: runner.cached_test_function_ir((1,)), ) - monkeypatch.setattr(Shrinker, "shrink", lambda self: self.consider_new_tree(ir(2))) + monkeypatch.setattr(Shrinker, "shrink", lambda self: self.consider_new_nodes(ir(2))) @run_to_nodes def nodes(data): @@ -585,19 +585,19 @@ def test_silly_shrinker_subclass(): assert BadShrinker.shrink(10, lambda _: True) == 10 -numeric_nodes = nodes(ir_types=["integer", "float"]) +numeric_nodes = nodes(choice_types=["integer", "float"]) @given(numeric_nodes, numeric_nodes, st.integers() | st.floats(allow_nan=False)) @example( - IRNode( - ir_type="float", + ChoiceNode( + type="float", value=float(MAX_PRECISE_INTEGER - 1), kwargs=float_kw(), was_forced=False, ), - IRNode( - ir_type="float", + ChoiceNode( + type="float", value=float(MAX_PRECISE_INTEGER - 1), kwargs=float_kw(), was_forced=False, @@ -610,15 +610,15 @@ def test_redistribute_numeric_pairs(node1, node2, stop): # avoid exhausting the tree while generating, which causes @shrinking_from's # runner to raise assume( - compute_max_children(node1.ir_type, node1.kwargs) - + compute_max_children(node2.ir_type, node2.kwargs) + compute_max_children(node1.type, node1.kwargs) + + compute_max_children(node2.type, node2.kwargs) > 2 ) @shrinking_from([node1.value, node2.value]) def shrinker(data: ConjectureData): - v1 = getattr(data, f"draw_{node1.ir_type}")(**node1.kwargs) - v2 = getattr(data, f"draw_{node2.ir_type}")(**node2.kwargs) + v1 = getattr(data, f"draw_{node1.type}")(**node1.kwargs) + v2 = getattr(data, f"draw_{node2.type}")(**node2.kwargs) if v1 + v2 > stop: data.mark_interesting() diff --git a/hypothesis-python/tests/quality/test_poisoned_trees.py b/hypothesis-python/tests/quality/test_poisoned_trees.py index 8ee264b8fc..da67412148 100644 --- a/hypothesis-python/tests/quality/test_poisoned_trees.py +++ b/hypothesis-python/tests/quality/test_poisoned_trees.py @@ -87,7 +87,7 @@ def test_function(data): nodes = [ node for node in data.nodes - if node.ir_type == "integer" and node.kwargs["max_value"] == 2**16 - 1 + if node.type == "integer" and node.kwargs["max_value"] == 2**16 - 1 ] assert len(nodes) % 2 == 0