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

Error on excluded infinite endpoints #1860

Merged
merged 5 commits into from
Mar 18, 2019
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
11 changes: 11 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
RELEASE_TYPE: minor

This release makes it an explicit error to call
:func:`floats(min_value=inf, exclude_min=True) <hypothesis.strategies.floats>` or
:func:`floats(max_value=-inf, exclude_max=True) <hypothesis.strategies.floats>`,
as there are no possible values that can be generated (:issue:`1859`).

:func:`floats(min_value=0.0, max_value=-0.0) <hypothesis.strategies.floats>`
is now deprecated. While `0. == -0.` and we could thus generate either if
comparing by value, violating the sequence ordering of floats is a special
case we don't want or need.
51 changes: 34 additions & 17 deletions hypothesis-python/src/hypothesis/_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -517,32 +517,49 @@ def floats(
since="2018-10-10",
)

if exclude_min:
if min_value is None:
raise InvalidArgument("Cannot exclude min_value=None")
min_value = next_up(min_value, width=width)
if exclude_max:
if max_value is None:
raise InvalidArgument("Cannot exclude max_value=None")
max_value = next_down(max_value, width=width)
if exclude_min and (min_value is None or min_value == float("inf")):
raise InvalidArgument("Cannot exclude min_value=%r" % (min_value,))
if exclude_max and (max_value is None or max_value == float("-inf")):
raise InvalidArgument("Cannot exclude max_value=%r" % (max_value,))

if min_value is not None and (
exclude_min or (min_arg is not None and min_value < min_arg)
):
min_value = next_up(min_value, width)
assert min_value > min_arg or min_value == min_arg == 0 # type: ignore
if max_value is not None and (
exclude_max or (max_arg is not None and max_value > max_arg)
):
max_value = next_down(max_value, width)
assert max_value < max_arg or max_value == max_arg == 0 # type: ignore

check_valid_interval(min_value, max_value, "min_value", "max_value")
if min_value == float(u"-inf"):
min_value = None
if max_value == float(u"inf"):
max_value = None

if min_value is not None and min_arg is not None and min_value < min_arg:
min_value = next_up(min_value, width)
assert min_value > min_arg # type: ignore
if max_value is not None and max_arg is not None and max_value > max_arg:
max_value = next_down(max_value, width)
assert max_value < max_arg # type: ignore
if min_value is not None and max_value is not None and min_value > max_value:
raise InvalidArgument(
bad_zero_bounds = (
min_value == max_value == 0
and is_negative(max_value)
and not is_negative(min_value)
)
if (
min_value is not None
and max_value is not None
and (min_value > max_value or bad_zero_bounds)
):
# This is a custom alternative to check_valid_interval, because we want
# to include the bit-width and exclusion information in the message.
msg = (
"There are no %s-bit floating-point values between min_value=%r "
"and max_value=%r" % (width, min_arg, max_arg)
)
if exclude_min or exclude_max:
msg += ", exclude_min=%r and exclude_max=%r" % (exclude_min, exclude_max)
if bad_zero_bounds:
note_deprecation(msg, since="RELEASEDAY")
else:
raise InvalidArgument(msg)

if allow_infinity is None:
allow_infinity = bool(min_value is None or max_value is None)
Expand Down
4 changes: 2 additions & 2 deletions hypothesis-python/src/hypothesis/internal/floats.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ def next_up(value, width=64):
assert isinstance(value, float)
if math.isnan(value) or (math.isinf(value) and value > 0):
return value
if value == 0.0:
value = 0.0
if value == 0.0 and is_negative(value):
return 0.0
fmt_int, fmt_flt = STRUCT_FORMATS[width]
# Note: n is signed; float_to_int returns unsigned
fmt_int = fmt_int.lower()
Expand Down
54 changes: 48 additions & 6 deletions hypothesis-python/tests/cover/test_float_nastiness.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@
from hypothesis import assume, given
from hypothesis.errors import InvalidArgument
from hypothesis.internal.compat import CAN_PACK_HALF_FLOAT, WINDOWS
from hypothesis.internal.floats import float_to_int, int_to_float, next_down, next_up
from hypothesis.internal.floats import (
float_to_int,
int_to_float,
is_negative,
next_down,
next_up,
)
from tests.common.debug import find_any, minimal
from tests.common.utils import checks_deprecated_behaviour

Expand Down Expand Up @@ -144,14 +150,22 @@ def test(f):
def test_up_means_greater(x):
hi = next_up(x)
if not x < hi:
assert (math.isnan(x) and math.isnan(hi)) or (x > 0 and math.isinf(x))
assert (
(math.isnan(x) and math.isnan(hi))
or (x > 0 and math.isinf(x))
or (x == hi == 0 and is_negative(x) and not is_negative(hi))
)


@given(st.floats())
def test_down_means_lesser(x):
lo = next_down(x)
if not x > lo:
assert (math.isnan(x) and math.isnan(lo)) or (x < 0 and math.isinf(x))
assert (
(math.isnan(x) and math.isnan(lo))
or (x < 0 and math.isinf(x))
or (x == lo == 0 and is_negative(lo) and not is_negative(x))
)


@given(st.floats(allow_nan=False, allow_infinity=False))
Expand All @@ -161,13 +175,15 @@ def test_updown_roundtrip(val):


@checks_deprecated_behaviour
@given(st.data(), st.floats(allow_nan=False, allow_infinity=False))
def test_floats_in_tiny_interval_within_bounds(data, center):
@pytest.mark.parametrize("xhi", [True, False])
@pytest.mark.parametrize("xlo", [True, False])
@given(st.data(), st.floats(allow_nan=False, allow_infinity=False).filter(bool))
def test_floats_in_tiny_interval_within_bounds(xlo, xhi, data, center):
assume(not (math.isinf(next_down(center)) or math.isinf(next_up(center))))
lo = Decimal.from_float(next_down(center)).next_plus()
hi = Decimal.from_float(next_up(center)).next_minus()
assert float(lo) < lo < center < hi < float(hi)
val = data.draw(st.floats(lo, hi))
val = data.draw(st.floats(lo, hi, exclude_min=xlo, exclude_max=xhi))
assert lo < val < hi


Expand Down Expand Up @@ -261,3 +277,29 @@ def test_can_exclude_neg_infinite_endpoint(x):
@given(st.floats(1e307, float("inf"), exclude_max=True))
def test_can_exclude_pos_infinite_endpoint(x):
assert not math.isinf(x)


def test_exclude_infinite_endpoint_is_invalid():
with pytest.raises(InvalidArgument):
st.floats(min_value=float("inf"), exclude_min=True).validate()
with pytest.raises(InvalidArgument):
st.floats(max_value=float("-inf"), exclude_max=True).validate()


@pytest.mark.parametrize("lo,hi", [(True, False), (False, True), (True, True)])
@given(bound=st.floats(allow_nan=False, allow_infinity=False).filter(bool))
def test_exclude_entire_interval(lo, hi, bound):
with pytest.raises(InvalidArgument, match="exclude_min=.+ and exclude_max="):
st.floats(bound, bound, exclude_min=lo, exclude_max=hi).validate()


def test_exclude_zero_interval():
st.floats(-0.0, 0.0).validate()
st.floats(-0.0, 0.0, exclude_min=True).validate()
st.floats(-0.0, 0.0, exclude_max=True).validate()


@checks_deprecated_behaviour
def test_inverse_zero_interval_is_deprecated():
st.floats(0.0, -0.0).validate()
st.floats(-0.0, 0.0, exclude_min=True, exclude_max=True).validate()
4 changes: 2 additions & 2 deletions hypothesis-python/tests/nocover/test_pretty_repr.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import hypothesis.strategies as st
from hypothesis import given, settings
from hypothesis.control import reject
from hypothesis.errors import InvalidArgument
from hypothesis.errors import HypothesisDeprecationWarning, InvalidArgument
from hypothesis.internal.compat import OrderedDict


Expand All @@ -45,7 +45,7 @@ def splat(value):
result = target(*value[0], **value[1])
result.validate()
return result
except InvalidArgument:
except (HypothesisDeprecationWarning, InvalidArgument):
reject()

return st.tuples(st.tuples(*args), st.fixed_dictionaries(kwargs)).map(splat)
Expand Down