Skip to content

Commit

Permalink
Fix latent len-filter-rewrite bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
Zac-HD committed Feb 25, 2024
1 parent 2c2348a commit aebcdd9
Show file tree
Hide file tree
Showing 4 changed files with 40 additions and 5 deletions.
4 changes: 3 additions & 1 deletion hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
RELEASE_TYPE: patch

This patch implements filter-rewriting for most length filters on some
additional collection types (:issue:`3795`).
additional collection types (:issue:`3795`), and fixes several latent
bugs where unsatisfiable or partially-infeasible rewrites could trigger
internal errors.
13 changes: 10 additions & 3 deletions hypothesis-python/src/hypothesis/internal/filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@

from hypothesis.internal.compat import ceil, floor
from hypothesis.internal.floats import next_down, next_up
from hypothesis.internal.reflection import extract_lambda_source
from hypothesis.internal.reflection import (
extract_lambda_source,
get_pretty_function_description,
)

Ex = TypeVar("Ex")
Predicate = Callable[[Ex], bool]
Expand Down Expand Up @@ -64,6 +67,10 @@ class ConstructivePredicate(NamedTuple):
def unchanged(cls, predicate: Predicate) -> "ConstructivePredicate":
return cls({}, predicate)

def __repr__(self) -> str:
fn = get_pretty_function_description(self.predicate)
return f"{self.__class__.__name__}(kwargs={self.kwargs!r}, predicate={fn})"


ARG = object()

Expand Down Expand Up @@ -147,8 +154,8 @@ def merge_preds(*con_predicates: ConstructivePredicate) -> ConstructivePredicate
elif kw["max_value"] == base["max_value"]:
base["exclude_max"] |= kw.get("exclude_max", False)

has_len = {"len" in kw for kw, _ in con_predicates}
assert len(has_len) == 1, "can't mix numeric with length constraints"
has_len = {"len" in kw for kw, _ in con_predicates if kw}
assert len(has_len) <= 1, "can't mix numeric with length constraints"
if has_len == {True}:
base["len"] = True

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ def filter(self, condition):
new = copy.copy(self)
new.min_size = max(self.min_size, kwargs.get("min_value", self.min_size))
new.max_size = min(self.max_size, kwargs.get("max_value", self.max_size))
# Unsatisfiable filters are easiest to understand without rewriting.
if new.min_size > new.max_size:
return SearchStrategy.filter(self, condition)
# Recompute average size; this is cheaper than making it into a property.
new.average_size = min(
max(new.min_size * 2, new.min_size + 5),
Expand Down
25 changes: 24 additions & 1 deletion hypothesis-python/tests/cover/test_filter_rewriting.py
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,9 @@ def test_filter_rewriting_text_lambda_len(data, strategy, predicate, start, end)
assert predicate(value)


two = 2


@pytest.mark.parametrize(
"predicate, start, end",
[
Expand All @@ -525,6 +528,9 @@ def test_filter_rewriting_text_lambda_len(data, strategy, predicate, start, end)
(lambda x: len(x) < 1 and len(x) < 1, 0, 0),
(lambda x: len(x) > 1 and len(x) > 0, 2, 3), # input max element_count=3
(lambda x: len(x) < 1 and len(x) < 2, 0, 0),
# Comparisons involving one literal and one variable
(lambda x: 1 <= len(x) <= two, 1, 3),
(lambda x: two <= len(x) <= 4, 0, 3),
],
ids=get_pretty_function_description,
)
Expand All @@ -536,7 +542,7 @@ def test_filter_rewriting_text_lambda_len(data, strategy, predicate, start, end)
ids=get_pretty_function_description,
)
@given(data=st.data())
def test_filter_rewriting_text_lambda_len_unique_elements(
def test_filter_rewriting_lambda_len_unique_elements(
data, strategy, predicate, start, end
):
s = strategy.filter(predicate)
Expand All @@ -550,3 +556,20 @@ def test_filter_rewriting_text_lambda_len_unique_elements(
assert unwrapped.filtered_strategy.max_size == end
value = data.draw(s)
assert predicate(value)


@pytest.mark.parametrize(
"predicate",
[
(lambda x: len(x) < 3),
(lambda x: len(x) > 5),
],
ids=get_pretty_function_description,
)
def test_does_not_rewrite_unsatisfiable_len_filter(predicate):
strategy = st.lists(st.none(), min_size=4, max_size=4).filter(predicate)
with pytest.raises(Unsatisfiable):
check_can_generate_examples(strategy)
# Rewriting to nothing() would correctly express the constraint. However
# we don't want _only rewritable strategies_ to work in e.g. one_of, so:
assert not strategy.is_empty

0 comments on commit aebcdd9

Please sign in to comment.