From 075a96abe39e9b46ea1a3701548daf76df7af60d Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 7 Mar 2025 21:58:49 -0500 Subject: [PATCH 1/3] rename Example to Span --- guides/strategies-that-shrink.rst | 2 +- .../src/hypothesis/extra/lark.py | 4 +- .../hypothesis/internal/conjecture/data.py | 284 +++++++++--------- .../internal/conjecture/datatree.py | 4 +- .../hypothesis/internal/conjecture/engine.py | 2 +- .../internal/conjecture/optimiser.py | 4 +- .../internal/conjecture/providers.py | 4 +- .../internal/conjecture/shrinker.py | 104 +++---- .../hypothesis/internal/conjecture/utils.py | 18 +- .../src/hypothesis/provisional.py | 2 +- hypothesis-python/src/hypothesis/stateful.py | 4 +- .../strategies/_internal/featureflags.py | 4 +- .../strategies/_internal/strategies.py | 12 +- .../tests/conjecture/test_data_tree.py | 8 +- .../tests/conjecture/test_engine.py | 44 ++- hypothesis-python/tests/conjecture/test_ir.py | 4 +- .../tests/conjecture/test_optimiser.py | 12 +- .../tests/conjecture/test_pareto.py | 4 +- .../tests/conjecture/test_shrinker.py | 56 ++-- .../tests/conjecture/test_test_data.py | 94 +++--- 20 files changed, 334 insertions(+), 336 deletions(-) diff --git a/guides/strategies-that-shrink.rst b/guides/strategies-that-shrink.rst index 6584dec53f..a49d4626b1 100644 --- a/guides/strategies-that-shrink.rst +++ b/guides/strategies-that-shrink.rst @@ -254,7 +254,7 @@ Explicit example boundaries ~~~~~~~~~~~~~~~~~~~~~~~~~~~ This is almost always handled implicitly, e.g. by ``cu.many``, but *sometimes* it can be useful to explicitly insert boundaries around draws that should be -deleted simultaneously using ``data.start_example``. This is used to group +deleted simultaneously using ``data.start_span``. This is used to group the value and sign of floating-point numbers, for example, which we split up in order to provide a more natural shrinking order. diff --git a/hypothesis-python/src/hypothesis/extra/lark.py b/hypothesis-python/src/hypothesis/extra/lark.py index 9fab2a002b..4dca883c9d 100644 --- a/hypothesis-python/src/hypothesis/extra/lark.py +++ b/hypothesis-python/src/hypothesis/extra/lark.py @@ -183,12 +183,12 @@ def draw_symbol( draw_state.append(data.draw(strategy)) else: assert isinstance(symbol, NonTerminal) - data.start_example(self.rule_label(symbol.name)) + data.start_span(self.rule_label(symbol.name)) expansion = data.draw(self.nonterminal_strategies[symbol.name]) for e in expansion: self.draw_symbol(data, e, draw_state) self.gen_ignore(data, draw_state) - data.stop_example() + data.stop_span() def gen_ignore(self, data: ConjectureData, draw_state: list[str]) -> None: if self.ignored_symbols and data.draw_boolean(1 / 4): diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index 73c0a4a1e4..c1ec93becb 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -132,69 +132,67 @@ def structural_coverage(label: int) -> StructuralCoverageTag: POOLED_KWARGS_CACHE = LRUCache(4096) -class Example: - """Examples track the hierarchical structure of draws from the byte stream, - within a single test run. - - Examples are created to mark regions of the byte stream that might be - useful to the shrinker, such as: - - The bytes used by a single draw from a strategy. - - Useful groupings within a strategy, such as individual list elements. - - Strategy-like helper functions that aren't first-class strategies. - - Each lowest-level draw of bits or bytes from the byte stream. - - A single top-level example that spans the entire input. - - Example-tracking allows the shrinker to try "high-level" transformations, - such as rearranging or deleting the elements of a list, without having - to understand their exact representation in the byte stream. - - Rather than store each ``Example`` as a rich object, it is actually - just an index into the ``Examples`` class defined below. This has two - purposes: Firstly, for most properties of examples we will never need +class Span: + """A span tracks the hierarchical structure of choices within a single test run. + + Spans are created to mark regions of the choice sequence that that are + logically related to each other. For instance, Hypothesis tracks: + - A single top-level span for the entire choice sequence + - A span for the choices made by each strategy + - Some strategies define additional spans within their choices. For instance, + st.lists() tracks the "should add another element" choice and the "add + another element" choices as separate spans. + + Spans provide useful information to the shrinker, mutator, targeted PBT, + and other subsystems of Hypothesis. + + Rather than store each ``Span`` as a rich object, it is actually + just an index into the ``Spans`` class defined below. This has two + purposes: Firstly, for most properties of spans we will never need to allocate storage at all, because most properties are not used on - most examples. Secondly, by storing the properties as compact lists + most spans. Secondly, by storing the spans as compact lists of integers, we save a considerable amount of space compared to Python's normal object size. This does have the downside that it increases the amount of allocation we do, and slows things down as a result, in some usage patterns because - we repeatedly allocate the same Example or int objects, but it will + we repeatedly allocate the same Span or int objects, but it will often dramatically reduce our memory usage, so is worth it. """ __slots__ = ("index", "owner") - def __init__(self, owner: "Examples", index: int) -> None: + def __init__(self, owner: "Spans", index: int) -> None: self.owner = owner self.index = index def __eq__(self, other: object) -> bool: if self is other: return True - if not isinstance(other, Example): + if not isinstance(other, Span): return NotImplemented return (self.owner is other.owner) and (self.index == other.index) def __ne__(self, other: object) -> bool: if self is other: return False - if not isinstance(other, Example): + if not isinstance(other, Span): return NotImplemented return (self.owner is not other.owner) or (self.index != other.index) def __repr__(self) -> str: - return f"examples[{self.index}]" + return f"spans[{self.index}]" @property def label(self) -> int: - """A label is an opaque value that associates each example with its + """A label is an opaque value that associates each span with its approximate origin, such as a particular strategy class or a particular kind of draw.""" return self.owner.labels[self.owner.label_indices[self.index]] @property def parent(self) -> Optional[int]: - """The index of the example that this one is nested directly within.""" + """The index of the span that this one is nested directly within.""" if self.index == 0: return None return self.owner.parentage[self.index] @@ -209,99 +207,100 @@ def end(self) -> int: @property def depth(self) -> int: - """Depth of this example in the example tree. The top-level example has a - depth of 0.""" + """ + Depth of this span in the span tree. The top-level span has a depth of 0. + """ return self.owner.depths[self.index] @property def discarded(self) -> bool: - """True if this is example's ``stop_example`` call had ``discard`` set to + """True if this is span's ``stop_span`` call had ``discard`` set to ``True``. This means we believe that the shrinker should be able to delete - this example completely, without affecting the value produced by its enclosing + this span completely, without affecting the value produced by its enclosing strategy. Typically set when a rejection sampler decides to reject a generated value and try again.""" return self.index in self.owner.discarded @property def choice_count(self) -> int: - """The number of choices in this example.""" + """The number of choices in this span.""" return self.end - self.start @property - def children(self) -> "list[Example]": - """The list of all examples with this as a parent, in increasing index + def children(self) -> "list[Span]": + """The list of all spans with this as a parent, in increasing index order.""" return [self.owner[i] for i in self.owner.children[self.index]] -class ExampleProperty: - """There are many properties of examples that we calculate by +class SpanProperty: + """There are many properties of spans that we calculate by essentially rerunning the test case multiple times based on the - calls which we record in ExampleRecord. + calls which we record in SpanProperty. This class defines a visitor, subclasses of which can be used to calculate these properties. """ - def __init__(self, examples: "Examples"): - self.example_stack: list[int] = [] - self.examples = examples - self.example_count = 0 + def __init__(self, spans: "Spans"): + self.span_stack: list[int] = [] + self.spans = spans + self.span_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: + for record in self.spans.trail: if record == CHOICE_RECORD: self.choice_count += 1 - elif record >= START_EXAMPLE_RECORD: - self.__push(record - START_EXAMPLE_RECORD) + elif record >= START_SPAN_RECORD: + self.__push(record - START_SPAN_RECORD) else: assert record in ( - STOP_EXAMPLE_DISCARD_RECORD, - STOP_EXAMPLE_NO_DISCARD_RECORD, + STOP_SPAN_DISCARD_RECORD, + STOP_SPAN_NO_DISCARD_RECORD, ) - self.__pop(discarded=record == STOP_EXAMPLE_DISCARD_RECORD) + self.__pop(discarded=record == STOP_SPAN_DISCARD_RECORD) return self.finish() def __push(self, label_index: int) -> None: - i = self.example_count - assert i < len(self.examples) - self.start_example(i, label_index=label_index) - self.example_count += 1 - self.example_stack.append(i) + i = self.span_count + assert i < len(self.spans) + self.start_span(i, label_index=label_index) + self.span_count += 1 + self.span_stack.append(i) def __pop(self, *, discarded: bool) -> None: - i = self.example_stack.pop() - self.stop_example(i, discarded=discarded) + i = self.span_stack.pop() + self.stop_span(i, discarded=discarded) - def start_example(self, i: int, label_index: int) -> None: - """Called at the start of each example, with ``i`` the - index of the example and ``label_index`` the index of - its label in ``self.examples.labels``.""" + def start_span(self, i: int, label_index: int) -> None: + """Called at the start of each span, with ``i`` the + index of the span and ``label_index`` the index of + its label in ``self.spans.labels``.""" - def stop_example(self, i: int, *, discarded: bool) -> None: - """Called at the end of each example, with ``i`` the - index of the example and ``discarded`` being ``True`` if ``stop_example`` + def stop_span(self, i: int, *, discarded: bool) -> None: + """Called at the end of each span, with ``i`` the + index of the span and ``discarded`` being ``True`` if ``stop_span`` was called with ``discard=True``.""" def finish(self) -> Any: raise NotImplementedError -STOP_EXAMPLE_DISCARD_RECORD = 1 -STOP_EXAMPLE_NO_DISCARD_RECORD = 2 -START_EXAMPLE_RECORD = 3 +STOP_SPAN_DISCARD_RECORD = 1 +STOP_SPAN_NO_DISCARD_RECORD = 2 +START_SPAN_RECORD = 3 CHOICE_RECORD = calc_label_from_name("ir draw record") -class ExampleRecord: - """Records the series of ``start_example``, ``stop_example``, and - ``draw_bits`` calls so that these may be stored in ``Examples`` and +class SpanRecord: + """Records the series of ``start_span``, ``stop_span``, and + ``draw_bits`` calls so that these may be stored in ``Spans`` and replayed when we need to know about the structure of individual - ``Example`` objects. + ``Span`` objects. Note that there is significant similarity between this class and ``DataObserver``, and the plan is to eventually unify them, but @@ -320,123 +319,123 @@ def freeze(self) -> None: def record_choice(self) -> None: self.trail.append(CHOICE_RECORD) - def start_example(self, label: int) -> None: + def start_span(self, label: int) -> None: assert self.__index_of_labels is not None try: i = self.__index_of_labels[label] except KeyError: i = self.__index_of_labels.setdefault(label, len(self.labels)) self.labels.append(label) - self.trail.append(START_EXAMPLE_RECORD + i) + self.trail.append(START_SPAN_RECORD + i) - def stop_example(self, *, discard: bool) -> None: + def stop_span(self, *, discard: bool) -> None: if discard: - self.trail.append(STOP_EXAMPLE_DISCARD_RECORD) + self.trail.append(STOP_SPAN_DISCARD_RECORD) else: - self.trail.append(STOP_EXAMPLE_NO_DISCARD_RECORD) + self.trail.append(STOP_SPAN_NO_DISCARD_RECORD) -class _starts_and_ends(ExampleProperty): - def __init__(self, examples: "Examples") -> None: - super().__init__(examples) - self.starts = IntList.of_length(len(self.examples)) - self.ends = IntList.of_length(len(self.examples)) +class _starts_and_ends(SpanProperty): + def __init__(self, spans: "Spans") -> None: + super().__init__(spans) + self.starts = IntList.of_length(len(self.spans)) + self.ends = IntList.of_length(len(self.spans)) - def start_example(self, i: int, label_index: int) -> None: + def start_span(self, i: int, label_index: int) -> None: self.starts[i] = self.choice_count - def stop_example(self, i: int, *, discarded: bool) -> None: + def stop_span(self, i: int, *, discarded: bool) -> None: self.ends[i] = self.choice_count def finish(self) -> tuple[IntList, IntList]: return (self.starts, self.ends) -class _discarded(ExampleProperty): - def __init__(self, examples: "Examples") -> None: - super().__init__(examples) +class _discarded(SpanProperty): + def __init__(self, spans: "Spans") -> None: + super().__init__(spans) self.result: set[int] = set() def finish(self) -> frozenset[int]: return frozenset(self.result) - def stop_example(self, i: int, *, discarded: bool) -> None: + def stop_span(self, i: int, *, discarded: bool) -> None: if discarded: self.result.add(i) -class _parentage(ExampleProperty): - def __init__(self, examples: "Examples") -> None: - super().__init__(examples) - self.result = IntList.of_length(len(self.examples)) +class _parentage(SpanProperty): + def __init__(self, spans: "Spans") -> None: + super().__init__(spans) + self.result = IntList.of_length(len(self.spans)) - def stop_example(self, i: int, *, discarded: bool) -> None: + def stop_span(self, i: int, *, discarded: bool) -> None: if i > 0: - self.result[i] = self.example_stack[-1] + self.result[i] = self.span_stack[-1] def finish(self) -> IntList: return self.result -class _depths(ExampleProperty): - def __init__(self, examples: "Examples") -> None: - super().__init__(examples) - self.result = IntList.of_length(len(self.examples)) +class _depths(SpanProperty): + def __init__(self, spans: "Spans") -> None: + super().__init__(spans) + self.result = IntList.of_length(len(self.spans)) - def start_example(self, i: int, label_index: int) -> None: - self.result[i] = len(self.example_stack) + def start_span(self, i: int, label_index: int) -> None: + self.result[i] = len(self.span_stack) def finish(self) -> IntList: return self.result -class _label_indices(ExampleProperty): - def __init__(self, examples: "Examples") -> None: - super().__init__(examples) - self.result = IntList.of_length(len(self.examples)) +class _label_indices(SpanProperty): + def __init__(self, spans: "Spans") -> None: + super().__init__(spans) + self.result = IntList.of_length(len(self.spans)) - def start_example(self, i: int, label_index: int) -> None: + def start_span(self, i: int, label_index: int) -> None: self.result[i] = label_index def finish(self) -> IntList: return self.result -class _mutator_groups(ExampleProperty): - def __init__(self, examples: "Examples") -> None: - super().__init__(examples) +class _mutator_groups(SpanProperty): + def __init__(self, spans: "Spans") -> None: + super().__init__(spans) self.groups: dict[int, set[tuple[int, int]]] = defaultdict(set) - def start_example(self, i: int, label_index: int) -> None: + def start_span(self, i: int, label_index: int) -> None: # TODO should we discard start == end cases? occurs for eg st.data() # which is conditionally or never drawn from. arguably swapping # nodes with the empty list is a useful mutation enabled by start == end? - key = (self.examples[i].start, self.examples[i].end) + key = (self.spans[i].start, self.spans[i].end) self.groups[label_index].add(key) def finish(self) -> Iterable[set[tuple[int, int]]]: - # Discard groups with only one example, since the mutator can't + # Discard groups with only one span, since the mutator can't # do anything useful with them. return [g for g in self.groups.values() if len(g) >= 2] -class Examples: - """A lazy collection of ``Example`` objects, derived from - the record of recorded behaviour in ``ExampleRecord``. +class Spans: + """A lazy collection of ``Span`` objects, derived from + the record of recorded behaviour in ``SpanRecord``. - Behaves logically as if it were a list of ``Example`` objects, + Behaves logically as if it were a list of ``Span`` objects, but actually mostly exists as a compact store of information for them to reference into. All properties on here are best - understood as the backing storage for ``Example`` and are + understood as the backing storage for ``Span`` and are described there. """ - def __init__(self, record: ExampleRecord) -> None: + def __init__(self, record: SpanRecord) -> None: self.trail = record.trail self.labels = record.labels - self.__length = self.trail.count( - STOP_EXAMPLE_DISCARD_RECORD - ) + record.trail.count(STOP_EXAMPLE_NO_DISCARD_RECORD) + self.__length = self.trail.count(STOP_SPAN_DISCARD_RECORD) + record.trail.count( + STOP_SPAN_NO_DISCARD_RECORD + ) self.__children: Optional[list[Sequence[int]]] = None @cached_property @@ -489,17 +488,17 @@ def children(self) -> list[Sequence[int]]: def __len__(self) -> int: return self.__length - def __getitem__(self, i: int) -> Example: + def __getitem__(self, i: int) -> Span: n = self.__length if i < -n or i >= n: raise IndexError(f"Index {i} out of range [-{n}, {n})") if i < 0: i += n - return Example(self, i) + return Span(self, i) # not strictly necessary as we have len/getitem, but required for mypy. # https://github.com/python/mypy/issues/9737 - def __iter__(self) -> Iterator[Example]: + def __iter__(self) -> Iterator[Span]: for i in range(len(self)): yield self[i] @@ -581,7 +580,7 @@ class ConjectureResult: has_discards: bool = attr.ib() target_observations: TargetObservations = attr.ib() tags: frozenset[StructuralCoverageTag] = attr.ib() - examples: Examples = attr.ib(repr=False, eq=False) + spans: Spans = attr.ib(repr=False, eq=False) arg_slices: set[tuple[int, int]] = attr.ib(repr=False) slice_comments: dict[tuple[int, int], str] = attr.ib(repr=False) misaligned_at: Optional[MisalignedAt] = attr.ib(repr=False) @@ -680,12 +679,12 @@ def __init__( # Normally unpopulated but we need this in the niche case # that self.as_result() is Overrun but we still want the # examples for reporting purposes. - self.__examples: Optional[Examples] = None + self.__spans: Optional[Spans] = None - # We want the top level example to have depth 0, so we start + # We want the top level span to have depth 0, so we start # at -1. self.depth = -1 - self.__example_record = ExampleRecord() + self.__span_record = SpanRecord() # Slice indices for discrete reportable parts that which-parts-matter can # try varying, to report if the minimal example always fails anyway. @@ -706,7 +705,7 @@ def __init__( self.prefix = prefix self.nodes: tuple[ChoiceNode, ...] = () self.misaligned_at: Optional[MisalignedAt] = None - self.start_example(TOP_LABEL) + self.start_span(TOP_LABEL) def __repr__(self) -> str: return "ConjectureData(%s, %d choices%s)" % ( @@ -788,7 +787,7 @@ def _draw(self, choice_type, kwargs, *, observe, forced): was_forced=was_forced, index=len(self.nodes), ) - self.__example_record.record_choice() + self.__span_record.record_choice() self.nodes += (node,) self.length += size @@ -1036,7 +1035,7 @@ def as_result(self) -> Union[ConjectureResult, _Overrun]: self.__result = ConjectureResult( status=self.status, interesting_origin=self.interesting_origin, - examples=self.examples, + spans=self.spans, nodes=self.nodes, length=self.length, output=self.output, @@ -1103,7 +1102,7 @@ def draw( if label is None: assert isinstance(strategy.label, int) label = strategy.label - self.start_example(label=label) + self.start_span(label=label) try: if not at_top_level: return strategy.do_draw(self) @@ -1128,11 +1127,11 @@ def draw( self._observability_args[key] = to_jsonable(v) return v finally: - self.stop_example() + self.stop_span() - def start_example(self, label: int) -> None: + def start_span(self, label: int) -> None: self.provider.span_start(label) - self.__assert_not_frozen("start_example") + self.__assert_not_frozen("start_span") self.depth += 1 # Logically it would make sense for this to just be # ``self.depth = max(self.depth, self.max_depth)``, which is what it used to @@ -1142,10 +1141,10 @@ def start_example(self, label: int) -> None: # to fix with this check. if self.depth > self.max_depth: self.max_depth = self.depth - self.__example_record.start_example(label) + self.__span_record.start_span(label) self.labels_for_structure_stack.append({label}) - def stop_example(self, *, discard: bool = False) -> None: + def stop_span(self, *, discard: bool = False) -> None: self.provider.span_end(discard) if self.frozen: return @@ -1153,7 +1152,7 @@ def stop_example(self, *, discard: bool = False) -> None: self.has_discards = True self.depth -= 1 assert self.depth >= -1 - self.__example_record.stop_example(discard=discard) + self.__span_record.stop_span(discard=discard) labels_for_structure = self.labels_for_structure_stack.pop() @@ -1164,10 +1163,10 @@ def stop_example(self, *, discard: bool = False) -> None: self.tags.update([structural_coverage(l) for l in labels_for_structure]) if discard: - # Once we've discarded an example, every test case starting with + # Once we've discarded a span, every test case starting with # this prefix contains discards. We prune the tree at that point so # as to avoid future test cases bothering with this region, on the - # assumption that some example that you could have used instead + # assumption that some span that you could have used instead # there would *not* trigger the discard. This greatly speeds up # test case generation in some cases, because it allows us to # ignore large swathes of the search space that are effectively @@ -1191,11 +1190,11 @@ def stop_example(self, *, discard: bool = False) -> None: self.observer.kill_branch() @property - def examples(self) -> Examples: + def spans(self) -> Spans: assert self.frozen - if self.__examples is None: - self.__examples = Examples(record=self.__example_record) - return self.__examples + if self.__spans is None: + self.__spans = Spans(record=self.__span_record) + return self.__spans def freeze(self) -> None: if self.frozen: @@ -1203,12 +1202,11 @@ def freeze(self) -> None: self.finish_time = time.perf_counter() self.gc_finish_time = gc_cumulative_time() - # Always finish by closing all remaining examples so that we have a - # valid tree. + # Always finish by closing all remaining spans so that we have a valid tree. while self.depth >= 0: - self.stop_example() + self.stop_span() - self.__example_record.freeze() + self.__span_record.freeze() self.frozen = True self.observer.conclude_test(self.status, self.interesting_origin) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/datatree.py b/hypothesis-python/src/hypothesis/internal/conjecture/datatree.py index 66106bbe72..c461f8cb35 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/datatree.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/datatree.py @@ -805,8 +805,8 @@ def rewrite(self, choices): def simulate_test_function(self, data: ConjectureData) -> None: """Run a simulated version of the test function recorded by - this tree. Note that this does not currently call ``stop_example`` - or ``start_example`` as these are not currently recorded in the + this tree. Note that this does not currently call ``stop_span`` + or ``start_span`` as these are not currently recorded in the tree. This will likely change in future.""" node = self.root diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py index 5ecca060f2..0cbee69b7d 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/engine.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/engine.py @@ -1157,7 +1157,7 @@ def generate_mutations_from( and self.call_count <= initial_calls + 5 and failed_mutations <= 5 ): - groups = data.examples.mutator_groups + groups = data.spans.mutator_groups if not groups: break diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/optimiser.py b/hypothesis-python/src/hypothesis/internal/conjecture/optimiser.py index 9ada9f2a94..33c58ceda9 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/optimiser.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/optimiser.py @@ -181,12 +181,12 @@ def attempt_replace(k: int) -> bool: if len(attempt.nodes) == len(self.current_data.nodes): return False - for j, ex in enumerate(self.current_data.examples): + for j, ex in enumerate(self.current_data.spans): if ex.start >= node.index + 1: break # pragma: no cover if ex.end <= node.index: continue - ex_attempt = attempt.examples[j] + ex_attempt = attempt.spans[j] if ex.choice_count == ex_attempt.choice_count: continue # pragma: no cover replacement = attempt.choices[ex_attempt.start : ex_attempt.end] diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/providers.py b/hypothesis-python/src/hypothesis/internal/conjecture/providers.py index ac91b04e67..f365e23ae8 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/providers.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/providers.py @@ -244,7 +244,7 @@ def span_start(self, label: int, /) -> None: # noqa: B027 # non-abstract noop `label` is an opaque integer, which will be shared by all spans drawn from a particular strategy. - This method is called from ConjectureData.start_example(). + This method is called from ConjectureData.start_span(). """ def span_end(self, discard: bool, /) -> None: # noqa: B027, FBT001 @@ -254,7 +254,7 @@ def span_end(self, discard: bool, /) -> None: # noqa: B027, FBT001 unlikely to contribute to the input data as seen by the user's test. Note however that side effects can make this determination unsound. - This method is called from ConjectureData.stop_example(). + This method is called from ConjectureData.stop_span(). """ diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py index f1f5c24f11..37a8009157 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py @@ -27,7 +27,7 @@ from hypothesis.internal.conjecture.data import ( ConjectureData, ConjectureResult, - Examples, + Spans, Status, _Overrun, draw_choice, @@ -187,7 +187,7 @@ class Shrinker: change in the underlying shrink target. It is generally safe to assume that the shrink target does not change prior to the point of first modification - e.g. if you change no bytes at - index ``i``, all examples whose start is ``<= i`` still exist, + index ``i``, all spans whose start is ``<= i`` still exist, as do all blocks, and the data object is still of length ``>= i + 1``. This can only be violated by bad user code which relies on an external source of non-determinism. @@ -548,7 +548,7 @@ def explain(self) -> None: len(attempt) == len(result.choices) and endswith(result.nodes, nodes[end:]) ): - for ex, res in zip(shrink_target.examples, result.examples): + for ex, res in zip(shrink_target.spans, result.spans): assert ex.start == res.start assert ex.start <= start assert ex.label == res.label @@ -627,14 +627,14 @@ def greedy_shrink(self) -> None: """ self.fixate_shrink_passes( [ - "try_trivial_examples", + "try_trivial_spans", node_program("X" * 5), node_program("X" * 4), node_program("X" * 3), node_program("X" * 2), node_program("X" * 1), "pass_to_descendant", - "reorder_examples", + "reorder_spans", "minimize_duplicated_choices", "minimize_individual_choices", "redistribute_numeric_pairs", @@ -657,10 +657,10 @@ def initial_coarse_reduction(self): self.reduce_each_alternative() @derived_value # type: ignore - def examples_starting_at(self): + def spans_starting_at(self): result = [[] for _ in self.shrink_target.nodes] - for i, ex in enumerate(self.examples): - # We can have zero-length examples that start at the end + for i, ex in enumerate(self.spans): + # We can have zero-length spans that start at the end if ex.start < len(result): result[ex.start].append(i) return tuple(map(tuple, result)) @@ -739,7 +739,7 @@ def try_lower_node_as_alternative(self, i, v): prefix = nodes[:i] + (nodes[i].copy(with_value=v),) initial = self.shrink_target - examples = self.examples_starting_at[i] + spans = self.spans_starting_at[i] for _ in range(3): random_attempt = self.engine.cached_test_function( [n.value for n in prefix], extend=len(nodes) @@ -747,9 +747,9 @@ def try_lower_node_as_alternative(self, i, v): if random_attempt.status < Status.VALID: continue self.incorporate_test_data(random_attempt) - for j in examples: - initial_ex = initial.examples[j] - attempt_ex = random_attempt.examples[j] + for j in spans: + initial_ex = initial.spans[j] + attempt_ex = random_attempt.spans[j] contents = random_attempt.nodes[attempt_ex.start : attempt_ex.end] self.consider_new_nodes(nodes[:i] + contents + nodes[initial_ex.end :]) if initial is not self.shrink_target: @@ -865,26 +865,28 @@ def choices(self) -> tuple[ChoiceT, ...]: return self.shrink_target.choices @property - def examples(self) -> Examples: - return self.shrink_target.examples + def spans(self) -> Spans: + return self.shrink_target.spans @derived_value # type: ignore - def examples_by_label(self): - """An index of all examples grouped by their label, with - the examples stored in their normal index order.""" + def spans_by_label(self): + """ + An index of all spans grouped by their label, with the spans stored in + their normal index order. + """ - examples_by_label = defaultdict(list) - for ex in self.examples: - examples_by_label[ex.label].append(ex) - return dict(examples_by_label) + spans_by_label = defaultdict(list) + for ex in self.spans: + spans_by_label[ex.label].append(ex) + return dict(spans_by_label) @derived_value # type: ignore def distinct_labels(self): - return sorted(self.examples_by_label, key=str) + return sorted(self.spans_by_label, key=str) @defines_shrink_pass() def pass_to_descendant(self, chooser): - """Attempt to replace each example with a descendant example. + """Attempt to replace each span with a descendant span. This is designed to deal with strategies that call themselves recursively. For example, suppose we had: @@ -903,10 +905,10 @@ def pass_to_descendant(self, chooser): """ label = chooser.choose( - self.distinct_labels, lambda l: len(self.examples_by_label[l]) >= 2 + self.distinct_labels, lambda l: len(self.spans_by_label[l]) >= 2 ) - ls = self.examples_by_label[label] + ls = self.spans_by_label[label] i = chooser.choose(range(len(ls) - 1)) ancestor = ls[i] @@ -1176,23 +1178,23 @@ def try_shrinking_nodes(self, nodes, n): # try to be more aggressive. regions_to_delete = {(end, end + lost_nodes)} - for ex in self.examples: + for ex in self.spans: if ex.start > start: continue if ex.end <= end: continue - if ex.index >= len(attempt.examples): + if ex.index >= len(attempt.spans): continue # pragma: no cover - replacement = attempt.examples[ex.index] + replacement = attempt.spans[ex.index] in_original = [c for c in ex.children if c.start >= end] in_replaced = [c for c in replacement.children if c.start >= end] if len(in_replaced) >= len(in_original) or not in_replaced: continue - # We've found an example where some of the children went missing + # We've found a span where some of the children went missing # as a result of this change, and just replacing it with the data # it would have had and removing the spillover didn't work. This # means that some of its children towards the right must be @@ -1228,7 +1230,7 @@ def remove_discarded(self): while self.shrink_target.has_discards: discarded = [] - for ex in self.shrink_target.examples: + for ex in self.shrink_target.spans: if ( ex.choice_count > 0 and ex.discarded @@ -1512,12 +1514,12 @@ def minimize_nodes(self, nodes): raise NotImplementedError @defines_shrink_pass() - def try_trivial_examples(self, chooser): - i = chooser.choose(range(len(self.examples))) + def try_trivial_spans(self, chooser): + i = chooser.choose(range(len(self.spans))) prev = self.shrink_target nodes = self.shrink_target.nodes - ex = self.examples[i] + ex = self.spans[i] prefix = nodes[: ex.start] replacement = tuple( [ @@ -1538,7 +1540,7 @@ def try_trivial_examples(self, chooser): return if isinstance(attempt, ConjectureResult): - new_ex = attempt.examples[i] + new_ex = attempt.spans[i] new_replacement = attempt.nodes[new_ex.start : new_ex.end] self.consider_new_nodes(prefix + new_replacement + suffix) @@ -1615,27 +1617,27 @@ def minimize_individual_choices(self, chooser): assert attempt is not self.shrink_target @self.cached(node.index) - def first_example_after_node(): + def first_span_after_node(): lo = 0 - hi = len(self.examples) + hi = len(self.spans) while lo + 1 < hi: mid = (lo + hi) // 2 - ex = self.examples[mid] + ex = self.spans[mid] if ex.start >= node.index: hi = mid else: lo = mid return hi - # we try deleting both entire examples, and single nodes. + # we try deleting both entire spans, and single nodes. # If we wanted to get more aggressive, we could try deleting n - # consecutive nodes (that don't cross an example boundary) for say + # consecutive nodes (that don't cross a span boundary) for say # n <= 2 or n <= 3. if chooser.choose([True, False]): - ex = self.examples[ + ex = self.spans[ chooser.choose( - range(first_example_after_node, len(self.examples)), - lambda i: self.examples[i].choice_count > 0, + range(first_span_after_node, len(self.spans)), + lambda i: self.spans[i].choice_count > 0, ) ] self.consider_new_nodes(lowered[: ex.start] + lowered[ex.end :]) @@ -1644,8 +1646,8 @@ def first_example_after_node(): self.consider_new_nodes(lowered[: node.index] + lowered[node.index + 1 :]) @defines_shrink_pass() - def reorder_examples(self, chooser): - """This pass allows us to reorder the children of each example. + def reorder_spans(self, chooser): + """This pass allows us to reorder the children of each span. For example, consider the following: @@ -1663,17 +1665,17 @@ def test_not_equal(x, y): ``x=""``, ``y="0"``, or the other way around. With reordering it will reliably fail with ``x=""``, ``y="0"``. """ - ex = chooser.choose(self.examples) + ex = chooser.choose(self.spans) label = chooser.choose(ex.children).label - examples = [c for c in ex.children if c.label == label] - if len(examples) <= 1: + spans = [c for c in ex.children if c.label == label] + if len(spans) <= 1: return st = self.shrink_target - endpoints = [(ex.start, ex.end) for ex in examples] + endpoints = [(ex.start, ex.end) for ex in spans] Ordering.shrink( - range(len(examples)), + range(len(spans)), lambda indices: self.consider_new_nodes( replace_all( st.nodes, @@ -1681,13 +1683,13 @@ def test_not_equal(x, y): ( u, v, - st.nodes[examples[i].start : examples[i].end], + st.nodes[spans[i].start : spans[i].end], ) for (u, v), i in zip(endpoints, indices) ], ) ), - key=lambda i: sort_key(st.nodes[examples[i].start : examples[i].end]), + key=lambda i: sort_key(st.nodes[spans[i].start : spans[i].end]), ) def run_node_program(self, i, description, original, repeats=1): diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py index 2d887cc65a..cf78bfeb99 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py @@ -185,7 +185,7 @@ def sample( forced: Optional[int] = None, ) -> int: if self.observe: - data.start_example(SAMPLE_IN_SAMPLER_LABEL) + data.start_span(SAMPLE_IN_SAMPLER_LABEL) forced_choice = ( # pragma: no branch # https://github.com/nedbat/coveragepy/issues/1617 None if forced is None @@ -214,7 +214,7 @@ def sample( observe=self.observe, ) if self.observe: - data.stop_example() + data.stop_span() if use_alternate: assert forced is None or alternate == forced, (forced, alternate) return alternate @@ -263,23 +263,23 @@ def __init__( self.rejected = False self.observe = observe - def stop_example(self): + def stop_span(self): if self.observe: - self.data.stop_example() + self.data.stop_span() - def start_example(self, label): + def start_span(self, label): if self.observe: - self.data.start_example(label) + self.data.start_span(label) def more(self) -> bool: """Should I draw another element to add to the collection?""" if self.drawn: - self.stop_example() + self.stop_span() self.drawn = True self.rejected = False - self.start_example(ONE_FROM_MANY_LABEL) + self.start_span(ONE_FROM_MANY_LABEL) if self.min_size == self.max_size: # if we have to hit an exact size, draw unconditionally until that # point, and no further. @@ -307,7 +307,7 @@ def more(self) -> bool: self.count += 1 return True else: - self.stop_example() + self.stop_span() return False def reject(self, why: Optional[str] = None) -> None: diff --git a/hypothesis-python/src/hypothesis/provisional.py b/hypothesis-python/src/hypothesis/provisional.py index 28e28226cf..cc32b92903 100644 --- a/hypothesis-python/src/hypothesis/provisional.py +++ b/hypothesis-python/src/hypothesis/provisional.py @@ -142,7 +142,7 @@ def do_draw(self, data: ConjectureData) -> str: # Generate a new valid subdomain using the regex strategy. sub_domain = data.draw(self.elem_strategy) if len(domain) + len(sub_domain) >= self.max_length: - data.stop_example(discard=True) + data.stop_span(discard=True) break domain = sub_domain + "." + domain return domain diff --git a/hypothesis-python/src/hypothesis/stateful.py b/hypothesis-python/src/hypothesis/stateful.py index 5ded133519..2ade095dbe 100644 --- a/hypothesis-python/src/hypothesis/stateful.py +++ b/hypothesis-python/src/hypothesis/stateful.py @@ -144,7 +144,7 @@ def output(s): # find a failing test case, so we stop with probability of # 2 ** -16 during normal operation but force a stop when we've # generated enough steps. - cd.start_example(STATE_MACHINE_RUN_LABEL) + cd.start_span(STATE_MACHINE_RUN_LABEL) must_stop = None if steps_run >= max_steps: must_stop = True @@ -227,7 +227,7 @@ def output(s): # then 'print_step' prints a multi-variable assignment. output(machine._repr_step(rule, data_to_print, result)) machine.check_invariants(settings, output, cd._stateful_run_times) - cd.stop_example() + cd.stop_span() finally: output("state.teardown()") machine.teardown() diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/featureflags.py b/hypothesis-python/src/hypothesis/strategies/_internal/featureflags.py index 98af8f087a..05fab28dcc 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/featureflags.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/featureflags.py @@ -77,7 +77,7 @@ def is_enabled(self, name): data = self.__data - data.start_example(label=FEATURE_LABEL) + data.start_span(label=FEATURE_LABEL) # If we've already decided on this feature then we don't actually # need to draw anything, but we do write the same decision to the @@ -99,7 +99,7 @@ def is_enabled(self, name): if name in oneof and not is_disabled: oneof.clear() oneof.discard(name) - data.stop_example() + data.stop_span() return not is_disabled def __repr__(self): diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py index fd0b739379..f32ac4b1b0 100644 --- a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py +++ b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py @@ -897,14 +897,14 @@ def do_draw(self, data: ConjectureData) -> MappedTo: warnings.simplefilter("ignore", BytesWarning) for _ in range(3): try: - data.start_example(MAPPED_SEARCH_STRATEGY_DO_DRAW_LABEL) + data.start_span(MAPPED_SEARCH_STRATEGY_DO_DRAW_LABEL) x = data.draw(self.mapped_strategy) result = self.pack(x) - data.stop_example() + data.stop_span() current_build_context().record_call(result, self.pack, [x], {}) return result except UnsatisfiedAssumption: - data.stop_example(discard=True) + data.stop_span(discard=True) raise UnsatisfiedAssumption @property @@ -1075,13 +1075,13 @@ def do_draw(self, data: ConjectureData) -> Ex: def do_filtered_draw(self, data: ConjectureData) -> Union[Ex, UniqueIdentifier]: for i in range(3): - data.start_example(FILTERED_SEARCH_STRATEGY_DO_DRAW_LABEL) + data.start_span(FILTERED_SEARCH_STRATEGY_DO_DRAW_LABEL) value = data.draw(self.filtered_strategy) if self.condition(value): - data.stop_example() + data.stop_span() return value else: - data.stop_example(discard=True) + data.stop_span(discard=True) if i == 0: data.events[f"Retried draw from {self!r} to satisfy filter"] = "" diff --git a/hypothesis-python/tests/conjecture/test_data_tree.py b/hypothesis-python/tests/conjecture/test_data_tree.py index 7eb2791983..c1f15879dd 100644 --- a/hypothesis-python/tests/conjecture/test_data_tree.py +++ b/hypothesis-python/tests/conjecture/test_data_tree.py @@ -358,18 +358,18 @@ def test_will_generate_novel_prefix_to_avoid_exhausted_branches(): def test_will_mark_changes_in_discard_as_flaky(): tree = DataTree() data = ConjectureData.for_choices((1, 1), observer=tree.new_observer()) - data.start_example(10) + data.start_span(10) data.draw_integer(0, 1) - data.stop_example() + data.stop_span() data.draw_integer(0, 1) data.freeze() data = ConjectureData.for_choices((1, 1), observer=tree.new_observer()) - data.start_example(10) + data.start_span(10) data.draw_integer(0, 1) with pytest.raises(Flaky): - data.stop_example(discard=True) + data.stop_span(discard=True) def test_is_not_flaky_on_positive_zero_and_negative_zero(): diff --git a/hypothesis-python/tests/conjecture/test_engine.py b/hypothesis-python/tests/conjecture/test_engine.py index f625358620..2d8d2b54ef 100644 --- a/hypothesis-python/tests/conjecture/test_engine.py +++ b/hypothesis-python/tests/conjecture/test_engine.py @@ -178,11 +178,11 @@ def test_variadic_draw(): def draw_list(data): result = [] while True: - data.start_example(SOME_LABEL) + data.start_span(SOME_LABEL) d = data.draw_integer(0, 2**8 - 1) & 7 if d: result.append(data.draw_bytes(d, d)) - data.stop_example() + data.stop_span() if not d: break return result @@ -513,8 +513,8 @@ def f(data): for choice in choices: if data.draw(st.integers(0, 100)) != choice: data.mark_invalid() - data.start_example(1) - data.stop_example() + data.start_span(1) + data.stop_span() data.mark_interesting() runner = ConjectureRunner( @@ -627,11 +627,11 @@ def test_discarding(monkeypatch): def nodes(data): count = 0 while count < 10: - data.start_example(SOME_LABEL) + data.start_span(SOME_LABEL) b = data.draw_boolean() if b: count += 1 - data.stop_example(discard=not b) + data.stop_span(discard=not b) data.mark_interesting() assert tuple(n.value for n in nodes) == (True,) * 10 @@ -641,9 +641,9 @@ def test_can_remove_discarded_data(): @shrinking_from((0,) * 10 + (11,)) def shrinker(data: ConjectureData): while True: - data.start_example(SOME_LABEL) + data.start_span(SOME_LABEL) b = data.draw_integer(0, 2**8 - 1) - data.stop_example(discard=(b == 0)) + data.stop_span(discard=(b == 0)) if b == 11: break data.mark_interesting() @@ -655,9 +655,9 @@ def shrinker(data: ConjectureData): def test_discarding_iterates_to_fixed_point(): @shrinking_from(list(range(100, -1, -1))) def shrinker(data: ConjectureData): - data.start_example(0) + data.start_span(0) data.draw_integer(0, 2**8 - 1) - data.stop_example(discard=True) + data.stop_span(discard=True) while data.draw_integer(0, 2**8 - 1): pass data.mark_interesting() @@ -670,8 +670,8 @@ def test_discarding_is_not_fooled_by_empty_discards(): @shrinking_from((1, 1)) def shrinker(data: ConjectureData): data.draw_integer(0, 2**1 - 1) - data.start_example(0) - data.stop_example(discard=True) + data.start_span(0) + data.stop_span(discard=True) data.draw_integer(0, 2**1 - 1) data.mark_interesting() @@ -682,15 +682,13 @@ def shrinker(data: ConjectureData): def test_discarding_can_fail(): @shrinking_from((1,)) def shrinker(data: ConjectureData): - data.start_example(0) + data.start_span(0) data.draw_boolean() - data.stop_example(discard=True) + data.stop_span(discard=True) data.mark_interesting() shrinker.remove_discarded() - assert any( - e.discarded and e.choice_count > 0 for e in shrinker.shrink_target.examples - ) + assert any(e.discarded and e.choice_count > 0 for e in shrinker.shrink_target.spans) def test_shrinking_from_mostly_zero(monkeypatch): @@ -720,12 +718,12 @@ def test_handles_nesting_of_discard_correctly(monkeypatch): @run_to_nodes def nodes(data): while True: - data.start_example(SOME_LABEL) + data.start_span(SOME_LABEL) succeeded = data.draw_boolean() - data.start_example(SOME_LABEL) + data.start_span(SOME_LABEL) data.draw_boolean() - data.stop_example(discard=not succeeded) - data.stop_example(discard=not succeeded) + data.stop_span(discard=not succeeded) + data.stop_span(discard=not succeeded) if succeeded: data.mark_interesting() @@ -1013,9 +1011,9 @@ def test_discards_kill_branches(): seen = set() def test(data: ConjectureData): - data.start_example(1) + data.start_span(1) n1 = data.draw_integer(0, 9) - data.stop_example(discard=n1 > 0) + data.stop_span(discard=n1 > 0) n2 = data.draw_integer(0, 9) n3 = data.draw_integer(0, 9) diff --git a/hypothesis-python/tests/conjecture/test_ir.py b/hypothesis-python/tests/conjecture/test_ir.py index a4b908ee4b..f1f0cf7937 100644 --- a/hypothesis-python/tests/conjecture/test_ir.py +++ b/hypothesis-python/tests/conjecture/test_ir.py @@ -237,10 +237,10 @@ def test_nodes(random): data.draw_float(min_value=-10.0, max_value=10.0, forced=5.0) data.draw_boolean(forced=True) - data.start_example(42) + data.start_span(42) data.draw_string(IntervalSet.from_string("abcd"), forced="abbcccdddd") data.draw_bytes(8, 8, forced=bytes(8)) - data.stop_example() + data.stop_span() data.draw_integer(0, 100, forced=50) diff --git a/hypothesis-python/tests/conjecture/test_optimiser.py b/hypothesis-python/tests/conjecture/test_optimiser.py index f8b35d82c4..06a67c70f4 100644 --- a/hypothesis-python/tests/conjecture/test_optimiser.py +++ b/hypothesis-python/tests/conjecture/test_optimiser.py @@ -71,8 +71,8 @@ def test_optimises_when_last_element_is_empty(): def test(data): data.target_observations["n"] = data.draw_integer(0, 2**8 - 1) - data.start_example(label=1) - data.stop_example() + data.start_span(label=1) + data.stop_span() runner = ConjectureRunner(test, settings=TEST_SETTINGS) runner.cached_test_function((250,)) @@ -92,8 +92,8 @@ def test(data): for _ in range(100): data.draw_integer(0, 3) data.target_observations[""] = data.draw_integer(0, 2**8 - 1) - data.start_example(1) - data.stop_example() + data.start_span(1) + data.stop_span() runner = ConjectureRunner( test, settings=settings(TEST_SETTINGS, max_examples=100) @@ -181,12 +181,12 @@ def test_can_patch_up_examples(): with deterministic_PRNG(): def test(data): - data.start_example(42) + data.start_span(42) m = data.draw_integer(0, 2**6 - 1) data.target_observations["m"] = m for _ in range(m): data.draw_boolean() - data.stop_example() + data.stop_span() for i in range(4): if i != data.draw_integer(0, 2**8 - 1): data.mark_invalid() diff --git a/hypothesis-python/tests/conjecture/test_pareto.py b/hypothesis-python/tests/conjecture/test_pareto.py index 38a570abc8..241581dc24 100644 --- a/hypothesis-python/tests/conjecture/test_pareto.py +++ b/hypothesis-python/tests/conjecture/test_pareto.py @@ -206,9 +206,9 @@ def test_uses_tags_in_calculating_pareto_front(): def test(data): data.target_observations[""] = 1 if data.draw_boolean(): - data.start_example(11) + data.start_span(11) data.draw_integer(0, 2**8 - 1) - data.stop_example() + data.stop_span() runner = ConjectureRunner( test, diff --git a/hypothesis-python/tests/conjecture/test_shrinker.py b/hypothesis-python/tests/conjecture/test_shrinker.py index fedd71eef3..615200606b 100644 --- a/hypothesis-python/tests/conjecture/test_shrinker.py +++ b/hypothesis-python/tests/conjecture/test_shrinker.py @@ -108,11 +108,11 @@ def test_can_zero_subintervals(): @shrinking_from((3, 0, 0, 0, 1) * 10) def shrinker(data: ConjectureData): for _ in range(10): - data.start_example(SOME_LABEL) + data.start_span(SOME_LABEL) n = data.draw_integer(0, 2**8 - 1) for _ in range(n): data.draw_integer(0, 2**8 - 1) - data.stop_example() + data.stop_span() if data.draw_integer(0, 2**8 - 1) != 1: return data.mark_interesting() @@ -123,13 +123,13 @@ def shrinker(data: ConjectureData): def test_can_pass_to_an_indirect_descendant(): def tree(data): - data.start_example(label=1) + data.start_span(label=1) n = data.draw_integer(0, 1) data.draw_integer(0, 2**8 - 1) if n: tree(data) tree(data) - data.stop_example(discard=True) + data.stop_span(discard=True) initial = (1, 10, 0, 0, 1, 0, 0, 10, 0, 0) target = (0, 10) @@ -163,11 +163,11 @@ def test_handle_empty_draws(): @run_to_nodes def nodes(data): while True: - data.start_example(SOME_LABEL) + data.start_span(SOME_LABEL) n = data.draw_integer(0, 1) - data.start_example(SOME_LABEL) - data.stop_example() - data.stop_example(discard=n > 0) + data.start_span(SOME_LABEL) + data.stop_span() + data.stop_span(discard=n > 0) if not n: break data.mark_interesting() @@ -175,20 +175,20 @@ def nodes(data): assert tuple(n.value for n in nodes) == (0,) -def test_can_reorder_examples(): +def test_can_reorder_spans(): # grouped by iteration: (1, 1) (1, 1) (0) (0) (0) @shrinking_from((1, 1, 1, 1, 0, 0, 0)) def shrinker(data: ConjectureData): total = 0 for _ in range(5): - data.start_example(label=0) + data.start_span(label=0) if data.draw_integer(0, 2**8 - 1): total += data.draw_integer(0, 2**9 - 1) - data.stop_example() + data.stop_span() if total == 2: data.mark_interesting() - shrinker.fixate_shrink_passes(["reorder_examples"]) + shrinker.fixate_shrink_passes(["reorder_spans"]) assert shrinker.choices == (0, 0, 0, 1, 1, 1, 1) @@ -251,14 +251,14 @@ def test_finding_a_minimal_balanced_binary_tree(): def tree(data): # Returns height of a binary tree and whether it is height balanced. - data.start_example(label=0) + data.start_span(label=0) if not data.draw_boolean(): result = (1, True) else: h1, b1 = tree(data) h2, b2 = tree(data) result = (1 + max(h1, h2), b1 and b2 and abs(h1 - h2) <= 1) - data.stop_example() + data.stop_span() return result # Starting from an unbalanced tree of depth six @@ -304,13 +304,13 @@ def test_zero_contained_examples(): @shrinking_from((1,) * 8) def shrinker(data: ConjectureData): for _ in range(4): - data.start_example(1) + data.start_span(1) if data.draw_integer(0, 2**8 - 1) == 0: data.mark_invalid() - data.start_example(1) + data.start_span(1) data.draw_integer(0, 2**8 - 1) - data.stop_example() - data.stop_example() + data.stop_span() + data.stop_span() data.mark_interesting() shrinker.shrink() @@ -372,15 +372,15 @@ def shrinker(data: ConjectureData): def test_zero_irregular_examples(): @shrinking_from((255,) * 6) def shrinker(data: ConjectureData): - data.start_example(1) + data.start_span(1) data.draw_integer(0, 2**8 - 1) data.draw_integer(0, 2**16 - 1) - data.stop_example() - data.start_example(1) + data.stop_span() + data.start_span(1) interesting = ( data.draw_integer(0, 2**8 - 1) > 0 and data.draw_integer(0, 2**16 - 1) > 0 ) - data.stop_example() + data.stop_span() if interesting: data.mark_interesting() @@ -425,17 +425,17 @@ def test_can_expand_deleted_region(): @shrinking_from((1, 2, 3, 4, 0, 0)) def shrinker(data: ConjectureData): def t(): - data.start_example(1) + data.start_span(1) - data.start_example(1) + data.start_span(1) m = data.draw_integer(0, 2**8 - 1) - data.stop_example() + data.stop_span() - data.start_example(1) + data.start_span(1) n = data.draw_integer(0, 2**8 - 1) - data.stop_example() + data.stop_span() - data.stop_example() + data.stop_span() return (m, n) v1 = t() diff --git a/hypothesis-python/tests/conjecture/test_test_data.py b/hypothesis-python/tests/conjecture/test_test_data.py index 3ab55d8a8d..7228a9ba38 100644 --- a/hypothesis-python/tests/conjecture/test_test_data.py +++ b/hypothesis-python/tests/conjecture/test_test_data.py @@ -98,7 +98,7 @@ def test_closes_interval_on_error_in_strategy(): with pytest.raises(ValueError): d.draw(BoomStrategy()) d.freeze() - assert not any(eg.end is None for eg in d.examples) + assert not any(eg.end is None for eg in d.spans) class BigStrategy(SearchStrategy): @@ -111,25 +111,25 @@ def test_does_not_double_freeze_in_interval_close(): with pytest.raises(StopTest): d.draw(BigStrategy()) assert d.frozen - assert not any(eg.end is None for eg in d.examples) + assert not any(eg.end is None for eg in d.spans) def test_triviality(): d = ConjectureData.for_choices((True, False, b"1")) - d.start_example(label=1) + d.start_span(label=1) d.draw(st.booleans()) d.draw(st.booleans()) - d.stop_example() + d.stop_span() - d.start_example(label=2) + d.start_span(label=2) d.draw_bytes(1, 1, forced=bytes([2])) - d.stop_example() + d.stop_span() d.freeze() def trivial(u, v): - ex = next(ex for ex in d.examples if ex.start == u and ex.end == v) + ex = next(ex for ex in d.spans if ex.start == u and ex.end == v) return all(node.trivial for node in d.nodes[ex.start : ex.end]) assert not trivial(0, 2) @@ -141,16 +141,16 @@ def trivial(u, v): def test_example_depth_marking(): d = ConjectureData.for_choices((0,) * 6) d.draw(st.integers()) # v1 - d.start_example("inner") + d.start_span("inner") d.draw(st.integers()) # v2 d.draw(st.integers()) # v3 - d.stop_example() + d.stop_span() d.draw(st.integers()) # v4 d.freeze() - assert len(d.examples) == 10 + assert len(d.spans) == 10 - depths = [(ex.choice_count, ex.depth) for ex in d.examples] + depths = [(ex.choice_count, ex.depth) for ex in d.spans] assert depths == [ (4, 0), # top (1, 1), # v1 @@ -169,21 +169,21 @@ def test_has_examples_even_when_empty(): d = ConjectureData.for_choices([]) d.draw(st.just(False)) d.freeze() - assert d.examples + assert d.spans def test_has_cached_examples_even_when_overrun(): d = ConjectureData.for_choices((False,)) - d.start_example(3) + d.start_span(3) d.draw_boolean() - d.stop_example() + d.stop_span() try: d.draw_boolean() except StopTest: pass assert d.status == Status.OVERRUN - assert any(ex.label == 3 and ex.choice_count == 1 for ex in d.examples) - assert d.examples is d.examples + assert any(ex.label == 3 and ex.choice_count == 1 for ex in d.spans) + assert d.spans is d.spans def test_can_observe_draws(): @@ -237,15 +237,15 @@ def conclude_test(self, status, reason): def test_examples_show_up_as_discarded(): d = ConjectureData.for_choices((True, False, True)) - d.start_example(1) + d.start_span(1) d.draw_boolean() - d.stop_example(discard=True) - d.start_example(1) + d.stop_span(discard=True) + d.start_span(1) d.draw_boolean() - d.stop_example() + d.stop_span() d.freeze() - assert len([ex for ex in d.examples if ex.discarded]) == 1 + assert len([ex for ex in d.spans if ex.discarded]) == 1 def test_examples_support_negative_indexing(): @@ -253,7 +253,7 @@ def test_examples_support_negative_indexing(): d.draw(st.booleans()) d.draw(st.booleans()) d.freeze() - assert d.examples[-1].choice_count == 1 + assert d.spans[-1].choice_count == 1 def test_examples_out_of_bounds_index(): @@ -261,14 +261,14 @@ def test_examples_out_of_bounds_index(): d.draw(st.booleans()) d.freeze() with pytest.raises(IndexError): - d.examples[10] + d.spans[10] def test_can_override_label(): d = ConjectureData.for_choices((False,)) d.draw(st.booleans(), label=7) d.freeze() - assert any(ex.label == 7 for ex in d.examples) + assert any(ex.label == 7 for ex in d.spans) def test_will_mark_too_deep_examples_as_invalid(): @@ -341,37 +341,37 @@ def test_events_are_noted(): def test_child_indices(): d = ConjectureData.for_choices((True,) * 4) - d.start_example(0) # examples[1] - d.start_example(1) # examples[2] + d.start_span(0) # examples[1] + d.start_span(1) # examples[2] d.draw(st.booleans()) # examples[3] (lazystrategy) + examples[4] (st.booleans) d.draw(st.booleans()) # examples[4] (lazystrategy) + examples[5] (st.booleans) - d.stop_example() - d.stop_example() + d.stop_span() + d.stop_span() d.draw(st.booleans()) # examples[7] (lazystrategy) + examples[8] (st.booleans) d.draw(st.booleans()) # examples[9] (lazystrategy) + examples[10] (st.booleans) d.freeze() - assert list(d.examples.children[0]) == [1, 7, 9] - assert list(d.examples.children[1]) == [2] - assert list(d.examples.children[2]) == [3, 5] + assert list(d.spans.children[0]) == [1, 7, 9] + assert list(d.spans.children[1]) == [2] + assert list(d.spans.children[2]) == [3, 5] - assert d.examples[0].parent is None - for ex in list(d.examples)[1:]: - assert ex in d.examples[ex.parent].children + assert d.spans[0].parent is None + for ex in list(d.spans)[1:]: + assert ex in d.spans[ex.parent].children def test_example_equality(): d = ConjectureData.for_choices((False, False)) - d.start_example(0) + d.start_span(0) d.draw_boolean() - d.stop_example() - d.start_example(0) + d.stop_span() + d.start_span(0) d.draw_boolean() - d.stop_example() + d.stop_span() d.freeze() - examples = list(d.examples) + examples = list(d.spans) for ex1, ex2 in itertools.combinations(examples, 2): assert ex1 != ex2 assert not (ex1 == ex2) # noqa @@ -390,26 +390,26 @@ def test_structural_coverage_is_cached(): def test_examples_create_structural_coverage(): data = ConjectureData.for_choices([]) - data.start_example(42) - data.stop_example() + data.start_span(42) + data.stop_span() data.freeze() assert structural_coverage(42) in data.tags def test_discarded_examples_do_not_create_structural_coverage(): data = ConjectureData.for_choices([]) - data.start_example(42) - data.stop_example(discard=True) + data.start_span(42) + data.stop_span(discard=True) data.freeze() assert structural_coverage(42) not in data.tags def test_children_of_discarded_examples_do_not_create_structural_coverage(): data = ConjectureData.for_choices([]) - data.start_example(10) - data.start_example(42) - data.stop_example() - data.stop_example(discard=True) + data.start_span(10) + data.start_span(42) + data.stop_span() + data.stop_span(discard=True) data.freeze() assert structural_coverage(42) not in data.tags assert structural_coverage(10) not in data.tags From 2e4b58151be4014a55fa6904771dd7e732a8ce08 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 9 Mar 2025 03:30:34 -0400 Subject: [PATCH 2/3] make a TrailType enum --- .../hypothesis/internal/conjecture/data.py | 36 +++++++++---------- .../internal/conjecture/shrinker.py | 4 +-- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py index c1ec93becb..a6cba5da3e 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py @@ -252,16 +252,16 @@ def run(self) -> Any: """Rerun the test case with this visitor and return the results of ``self.finish()``.""" for record in self.spans.trail: - if record == CHOICE_RECORD: + if record == TrailType.CHOICE: self.choice_count += 1 - elif record >= START_SPAN_RECORD: - self.__push(record - START_SPAN_RECORD) + elif record >= TrailType.START_SPAN: + self.__push(record - TrailType.START_SPAN) else: assert record in ( - STOP_SPAN_DISCARD_RECORD, - STOP_SPAN_NO_DISCARD_RECORD, + TrailType.STOP_SPAN_DISCARD, + TrailType.STOP_SPAN_NO_DISCARD, ) - self.__pop(discarded=record == STOP_SPAN_DISCARD_RECORD) + self.__pop(discarded=record == TrailType.STOP_SPAN_DISCARD) return self.finish() def __push(self, label_index: int) -> None: @@ -289,11 +289,11 @@ def finish(self) -> Any: raise NotImplementedError -STOP_SPAN_DISCARD_RECORD = 1 -STOP_SPAN_NO_DISCARD_RECORD = 2 -START_SPAN_RECORD = 3 - -CHOICE_RECORD = calc_label_from_name("ir draw record") +class TrailType(IntEnum): + STOP_SPAN_DISCARD = 1 + STOP_SPAN_NO_DISCARD = 2 + START_SPAN = 3 + CHOICE = calc_label_from_name("ir draw record") class SpanRecord: @@ -317,7 +317,7 @@ def freeze(self) -> None: self.__index_of_labels = None def record_choice(self) -> None: - self.trail.append(CHOICE_RECORD) + self.trail.append(TrailType.CHOICE) def start_span(self, label: int) -> None: assert self.__index_of_labels is not None @@ -326,13 +326,13 @@ def start_span(self, label: int) -> None: except KeyError: i = self.__index_of_labels.setdefault(label, len(self.labels)) self.labels.append(label) - self.trail.append(START_SPAN_RECORD + i) + self.trail.append(TrailType.START_SPAN + i) def stop_span(self, *, discard: bool) -> None: if discard: - self.trail.append(STOP_SPAN_DISCARD_RECORD) + self.trail.append(TrailType.STOP_SPAN_DISCARD) else: - self.trail.append(STOP_SPAN_NO_DISCARD_RECORD) + self.trail.append(TrailType.STOP_SPAN_NO_DISCARD) class _starts_and_ends(SpanProperty): @@ -433,9 +433,9 @@ class Spans: def __init__(self, record: SpanRecord) -> None: self.trail = record.trail self.labels = record.labels - self.__length = self.trail.count(STOP_SPAN_DISCARD_RECORD) + record.trail.count( - STOP_SPAN_NO_DISCARD_RECORD - ) + self.__length = self.trail.count( + TrailType.STOP_SPAN_DISCARD + ) + record.trail.count(TrailType.STOP_SPAN_NO_DISCARD) self.__children: Optional[list[Sequence[int]]] = None @cached_property diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py index 37a8009157..93cab7a519 100644 --- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py +++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py @@ -871,8 +871,8 @@ def spans(self) -> Spans: @derived_value # type: ignore def spans_by_label(self): """ - An index of all spans grouped by their label, with the spans stored in - their normal index order. + A mapping of labels to a list of spans with that label. Spans in the list + are ordered by their normal index order. """ spans_by_label = defaultdict(list) From 0f75a834e7ed3b763d8a1c2edc7a921046505572 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 9 Mar 2025 13:31:41 -0400 Subject: [PATCH 3/3] add release notes --- hypothesis-python/RELEASE.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 hypothesis-python/RELEASE.rst diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst new file mode 100644 index 0000000000..eac14b0f2b --- /dev/null +++ b/hypothesis-python/RELEASE.rst @@ -0,0 +1,3 @@ +RELEASE_TYPE: patch + +Rename a few internal classes for clarity.