Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade integers() backend and filter rewriting #2878

Merged
merged 3 commits into from
Mar 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Engine changes need to be approved by DRMacIver, as per
# https://github.com/HypothesisWorks/hypothesis/blob/master/guides/review.rst#engine-changes
/hypothesis-python/src/hypothesis/internal/conjecture/ @DRMacIver
/hypothesis-python/src/hypothesis/internal/conjecture/ @DRMacIver @Zac-HD

# Changes to the paper also need to be approved by DRMacIver
/paper.md @DRMacIver
Expand Down
4 changes: 4 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
RELEASE_TYPE: patch

This patch lays more groundwork for filter rewriting (:issue:`2701`).
There is no user-visible change... yet.
1 change: 1 addition & 0 deletions hypothesis-python/src/hypothesis/extra/ghostwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ def _get_strategies(

This dict is used to construct our call to the `@given(...)` decorator.
"""
assert funcs, "Must pass at least one function"
given_strategies: Dict[str, st.SearchStrategy] = {}
for i, f in enumerate(funcs):
params = _get_params(f)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def dominance(left, right):
return DominanceRelation.EQUAL

if sort_key(right.buffer) < sort_key(left.buffer):
result = dominance(right, left)
result = dominance(left=right, right=left)
if result == DominanceRelation.LEFT_DOMINATES:
return DominanceRelation.RIGHT_DOMINATES
else:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,6 @@
from hypothesis.internal.conjecture.shrinking import Float, Integer, Lexical, Ordering
from hypothesis.internal.conjecture.shrinking.learned_dfas import SHRINKING_DFAS

if False:
from typing import Dict # noqa


def sort_key(buffer):
"""Returns a sort key such that "simpler" buffers are smaller than
Expand Down
22 changes: 17 additions & 5 deletions hypothesis-python/src/hypothesis/internal/conjecture/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ def combine_labels(*labels: int) -> int:
SAMPLE_IN_SAMPLER_LABLE = calc_label_from_name("a sample() in Sampler")
ONE_FROM_MANY_LABEL = calc_label_from_name("one more from many()")

INT_SIZES = (8, 16, 32, 64, 128)
INT_WEIGHTS = (4.0, 8.0, 1.0, 1.0, 0.5)


def unbounded_integers(data):
size = INT_SIZES[Sampler(INT_WEIGHTS).sample(data)]
r = data.draw_bits(size)
sign = r & 1
r >>= 1
if sign:
r = -r
return int(r)


def integer_range(data, lower, upper, center=None):
assert lower <= upper
Expand Down Expand Up @@ -90,11 +103,10 @@ def integer_range(data, lower, upper, center=None):

if bits > 24 and data.draw_bits(3):
# For large ranges, we combine the uniform random distribution from draw_bits
# with the weighting scheme used by WideRangeIntStrategy with moderate chance.
# Cutoff at 2 ** 24 so unicode choice is uniform but 32bit distribution is not.
idx = Sampler([4.0, 8.0, 1.0, 1.0, 0.5]).sample(data)
sizes = [8, 16, 32, 64, 128]
bits = min(bits, sizes[idx])
# with a weighting scheme with moderate chance. Cutoff at 2 ** 24 so that our
# choice of unicode characters is uniform but the 32bit distribution is not.
idx = Sampler(INT_WEIGHTS).sample(data)
bits = min(bits, INT_SIZES[idx])

while probe > gap:
data.start_example(INTEGER_RANGE_DRAW_LABEL)
Expand Down
6 changes: 4 additions & 2 deletions hypothesis-python/src/hypothesis/internal/filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,10 @@ def get_numeric_predicate_bounds(predicate: Predicate) -> ConstructivePredicate:
and not predicate.keywords
):
arg = predicate.args[0]
if (isinstance(arg, Decimal) and Decimal.is_snan(arg)) or not isinstance(
arg, (int, float, Fraction, Decimal)
if (
(isinstance(arg, Decimal) and Decimal.is_snan(arg))
or not isinstance(arg, (int, float, Fraction, Decimal))
or math.isnan(arg)
):
return ConstructivePredicate.unchanged(predicate)
options = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def ip_addresses(
"""
if v is not None:
check_type(int, v, "v")
if v != 4 and v != 6:
if v not in (4, 6):
raise InvalidArgument(f"v={v!r}, but only v=4 or v=6 are valid")
if network is None:
# We use the reserved-address registries to boost the chance
Expand Down
100 changes: 47 additions & 53 deletions hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,48 +44,65 @@

# See https://github.com/python/mypy/issues/3186 - numbers.Real is wrong!
Real = Union[int, float, Fraction, Decimal]
ONE_BOUND_INTEGERS_LABEL = d.calc_label_from_name("trying a one-bound int allowing 0")


class WideRangeIntStrategy(SearchStrategy):

distribution = d.Sampler([4.0, 8.0, 1.0, 1.0, 0.5])

sizes = [8, 16, 32, 64, 128]

def __repr__(self):
return "WideRangeIntStrategy()"

def do_draw(self, data):
size = self.sizes[self.distribution.sample(data)]
r = data.draw_bits(size)
sign = r & 1
r >>= 1
if sign:
r = -r
return int(r)


class BoundedIntStrategy(SearchStrategy):
"""A strategy for providing integers in some interval with inclusive
endpoints."""

class IntegersStrategy(SearchStrategy):
def __init__(self, start, end):
SearchStrategy.__init__(self)
assert isinstance(start, int) or start is None
assert isinstance(end, int) or end is None
assert start is None or end is None or start <= end
self.start = start
self.end = end

def __repr__(self):
if self.start is None and self.end is None:
return "integers()"
if self.end is None:
return f"integers(min_value={self.start})"
if self.start is None:
return f"integers(max_value={self.end})"
return f"integers({self.start}, {self.end})"

def do_draw(self, data):
return d.integer_range(data, self.start, self.end)
if self.start is None and self.end is None:
return d.unbounded_integers(data)

if self.start is None:
if self.end <= 0:
return self.end - abs(d.unbounded_integers(data))
else:
probe = self.end + 1
while self.end < probe:
data.start_example(ONE_BOUND_INTEGERS_LABEL)
probe = d.unbounded_integers(data)
data.stop_example(discard=self.end < probe)
return probe

if self.end is None:
if self.start >= 0:
return self.start + abs(d.unbounded_integers(data))
else:
probe = self.start - 1
while probe < self.start:
data.start_example(ONE_BOUND_INTEGERS_LABEL)
probe = d.unbounded_integers(data)
data.stop_example(discard=probe < self.start)
return probe

return d.integer_range(data, self.start, self.end, center=0)

def filter(self, condition):
kwargs, pred = get_integer_predicate_bounds(condition)
start = max(self.start, kwargs.get("min_value", self.start))
end = min(self.end, kwargs.get("max_value", self.end))
if start > self.start or end < self.end:
if start > end:

start, end = self.start, self.end
if "min_value" in kwargs:
start = max(kwargs["min_value"], -math.inf if start is None else start)
if "max_value" in kwargs:
end = min(kwargs["max_value"], math.inf if end is None else end)

if start != self.start or end != self.end:
if start is not None and end is not None and start > end:
return nothing()
self = type(self)(start, end)
if pred is None:
Expand Down Expand Up @@ -126,30 +143,7 @@ def integers(
)
max_value = int(max_value)

if min_value is None:
if max_value is None:
return WideRangeIntStrategy()
else:
if max_value > 0:
return WideRangeIntStrategy().filter(lambda x: x <= max_value)
return WideRangeIntStrategy().map(lambda x: max_value - abs(x))
else:
if max_value is None:
if min_value < 0:
return WideRangeIntStrategy().filter(lambda x: x >= min_value)
return WideRangeIntStrategy().map(lambda x: min_value + abs(x))
else:
assert min_value <= max_value
if min_value == max_value:
return just(min_value)
elif min_value >= 0:
return BoundedIntStrategy(min_value, max_value)
elif max_value <= 0:
return BoundedIntStrategy(-max_value, -min_value).map(lambda t: -t)
else:
return integers(min_value=0, max_value=max_value) | integers(
min_value=min_value, max_value=0
)
return IntegersStrategy(min_value, max_value)


NASTY_FLOATS = sorted(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,12 @@ def branches(self):
else:
return [self]

def filter(self, condition):
return FilteredStrategy(
OneOfStrategy([s.filter(condition) for s in self.original_strategies]),
conditions=(),
)


@overload
def one_of(args: Sequence[SearchStrategy[Any]]) -> SearchStrategy[Any]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,7 @@ def is_a_new_type(thing):
# than an actual type, but we can check whether that thing matches.
return (
hasattr(thing, "__supertype__")
and (
getattr(thing, "__module__", None) == "typing"
or getattr(thing, "__module__", None) == "typing_extensions"
)
and getattr(thing, "__module__", None) in ("typing", "typing_extensions")
and inspect.isfunction(thing)
)

Expand Down
6 changes: 3 additions & 3 deletions hypothesis-python/src/hypothesis/vendor/pretty.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,7 @@ def inner(obj, p, cycle):

if cycle:
return p.text(start + "..." + end)
if len(obj) == 0:
if not obj:
# Special case.
p.text(basetype.__name__ + "()")
else:
Expand Down Expand Up @@ -815,7 +815,7 @@ def _ordereddict_pprint(obj, p, cycle):
with p.group(len(name) + 1, name + "(", ")"):
if cycle:
p.text("...")
elif len(obj):
elif obj:
p.pretty(list(obj.items()))


Expand All @@ -833,7 +833,7 @@ def _counter_pprint(obj, p, cycle):
with p.group(len(name) + 1, name + "(", ")"):
if cycle:
p.text("...")
elif len(obj):
elif obj:
p.pretty(dict(obj))


Expand Down
4 changes: 1 addition & 3 deletions hypothesis-python/tests/conjecture/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,6 @@ def accept(data):

@pytest.mark.parametrize("n", [1, 5])
def test_terminates_shrinks(n, monkeypatch):
from hypothesis.internal.conjecture import engine

db = InMemoryExampleDatabase()

def generate_new_examples(self):
Expand All @@ -123,7 +121,7 @@ def generate_new_examples(self):
monkeypatch.setattr(
ConjectureRunner, "generate_new_examples", generate_new_examples
)
monkeypatch.setattr(engine, "MAX_SHRINKS", n)
monkeypatch.setattr(engine_module, "MAX_SHRINKS", n)

runner = ConjectureRunner(
slow_shrinker(),
Expand Down
8 changes: 3 additions & 5 deletions hypothesis-python/tests/cover/test_direct_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,15 +363,13 @@ def test_decimal_is_in_bounds(x):


def test_float_can_find_max_value_inf():
assert minimal(ds.floats(max_value=math.inf), lambda x: math.isinf(x)) == float(
"inf"
)
assert minimal(ds.floats(min_value=0.0), lambda x: math.isinf(x)) == math.inf
assert minimal(ds.floats(max_value=math.inf), math.isinf) == float("inf")
assert minimal(ds.floats(min_value=0.0), math.isinf) == math.inf


def test_float_can_find_min_value_inf():
minimal(ds.floats(), lambda x: x < 0 and math.isinf(x))
minimal(ds.floats(min_value=-math.inf, max_value=0.0), lambda x: math.isinf(x))
minimal(ds.floats(min_value=-math.inf, max_value=0.0), math.isinf)


def test_can_find_none_list():
Expand Down
30 changes: 27 additions & 3 deletions hypothesis-python/tests/cover/test_filter_rewriting.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
from hypothesis import given, strategies as st
from hypothesis.errors import Unsatisfiable
from hypothesis.strategies._internal.lazy import LazyStrategy
from hypothesis.strategies._internal.numbers import BoundedIntStrategy
from hypothesis.strategies._internal.numbers import IntegersStrategy
from hypothesis.strategies._internal.strategies import FilteredStrategy

from tests.common.utils import fails_with
Expand All @@ -45,13 +45,24 @@
(st.integers(1, 5), partial(operator.gt, 3.5), 1, 3),
(st.integers(1, 5), partial(operator.lt, -math.inf), 1, 5),
(st.integers(1, 5), partial(operator.gt, math.inf), 1, 5),
# Integers with only one bound
(st.integers(min_value=1), partial(operator.lt, 3), 4, None),
(st.integers(min_value=1), partial(operator.le, 3), 3, None),
(st.integers(max_value=5), partial(operator.ge, 3), None, 3),
(st.integers(max_value=5), partial(operator.gt, 3), None, 2),
# Unbounded integers
(st.integers(), partial(operator.lt, 3), 4, None),
(st.integers(), partial(operator.le, 3), 3, None),
(st.integers(), partial(operator.eq, 3), 3, 3),
(st.integers(), partial(operator.ge, 3), None, 3),
(st.integers(), partial(operator.gt, 3), None, 2),
],
)
@given(data=st.data())
def test_filter_rewriting(data, strategy, predicate, start, end):
s = strategy.filter(predicate)
assert isinstance(s, LazyStrategy)
assert isinstance(s.wrapped_strategy, BoundedIntStrategy)
assert isinstance(s.wrapped_strategy, IntegersStrategy)
assert s.wrapped_strategy.start == start
assert s.wrapped_strategy.end == end
value = data.draw(s)
Expand Down Expand Up @@ -87,6 +98,19 @@ def test_rewriting_does_not_compare_decimal_snan():
s.example()


@pytest.mark.parametrize(
"strategy, lo, hi",
[
(st.integers(0, 1), -1, 2),
],
ids=repr,
)
def test_applying_noop_filter_returns_self(strategy, lo, hi):
s = strategy.wrapped_strategy
s2 = s.filter(partial(operator.le, -1)).filter(partial(operator.ge, 2))
assert s is s2


def mod2(x):
return x % 2

Expand Down Expand Up @@ -117,5 +141,5 @@ def test_rewrite_filter_chains_with_some_unhandled(data, predicates):
# No matter the order of the filters, we get the same resulting structure
unwrapped = s.wrapped_strategy
assert isinstance(unwrapped, FilteredStrategy)
assert isinstance(unwrapped.filtered_strategy, BoundedIntStrategy)
assert isinstance(unwrapped.filtered_strategy, IntegersStrategy)
assert unwrapped.flat_conditions == (mod2,)
2 changes: 1 addition & 1 deletion hypothesis-python/tests/numpy/test_gen_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,7 +579,7 @@ def _broadcast_shapes(*shapes):
input shapes together.

Raises ValueError if the shapes are not broadcast-compatible"""
assert len(shapes)
assert shapes, "Must pass >=1 shapes to broadcast"
return reduce(_broadcast_two_shapes, shapes, ())


Expand Down
2 changes: 1 addition & 1 deletion hypothesis-python/tests/quality/test_discovery_ability.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

from hypothesis import HealthCheck, settings as Settings
from hypothesis.errors import UnsatisfiedAssumption
from hypothesis.internal import reflection as reflection
from hypothesis.internal import reflection
from hypothesis.internal.conjecture.engine import ConjectureRunner
from hypothesis.strategies import (
booleans,
Expand Down
Loading