Repository: HypothesisWorks/hypothesis

Issue:
Float strategy can generate infinite values, even if explicitly excluded
When specifying infinity as the max of min value for a float, and then excluding this value, it can still be generated.

This is easy to fix with simple strategies, but when generating min and max values from another strategy, and relying on the `exclude` parameter to specify open or closed intervals to draw a float, it may cause problems.

Here is a sample code to reproduce the problem : 
```py
import pytest
from hypothesis import given, settings, Verbosity
from hypothesis.strategies import composite, floats, one_of

@given(val=floats(max_value=float('-inf'), allow_nan=False, exclude_max=True))
@settings(verbosity=Verbosity.verbose)
def test_should_be_always_true(val):
    assert val != float('-inf')

@given(val=floats(min_value=float('inf'), allow_nan=False, exclude_min=True))
@settings(verbosity=Verbosity.verbose)
def test_should_be_always_true_aswell(val):
    assert val != float('inf')
        
if __name__ == "__main__":
    pytest.main(["-s", __file__])

```
I would have expected the strategy to fail somehow... but it succeeds and generate only infinity, even tho it should be excluded.

So running this script I get :
```
================================================================================================================================= FAILURES =================================================================================================================================
________________________________________________________________________________________________________________________ test_should_be_always_true ________________________________________________________________________________________________________________________

    @given(val=floats(max_value=float('-inf'), allow_nan=False, exclude_max=True))
>   @settings(verbosity=Verbosity.verbose)
    def test_should_be_always_true(val):

float_inf_strat.py:6: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

val = -inf

    @given(val=floats(max_value=float('-inf'), allow_nan=False, exclude_max=True))
    @settings(verbosity=Verbosity.verbose)
    def test_should_be_always_true(val):
>       assert val != float('-inf')
E       AssertionError: assert -inf != -inf
E        +  where -inf = float('-inf')

float_inf_strat.py:8: AssertionError
-------------------------------------------------------------------------------------------------------------------------------- Hypothesis --------------------------------------------------------------------------------------------------------------------------------
Trying example: test_should_be_always_true(val=-inf)
Traceback (most recent call last):
  File "/home/alexv/Projects/tmp/float_inf_strat.py", line 8, in test_should_be_always_true
    assert val != float('-inf')
AssertionError: assert -inf != -inf
 +  where -inf = float('-inf')

Trying example: test_should_be_always_true(val=-inf)
Traceback (most recent call last):
  File "/home/alexv/Projects/tmp/float_inf_strat.py", line 8, in test_should_be_always_true
    assert val != float('-inf')
AssertionError: assert -inf != -inf
 +  where -inf = float('-inf')

Falsifying example: test_should_be_always_true(val=-inf)
____________________________________________________________________________________________________________________ test_should_be_always_true_aswell _____________________________________________________________________________________________________________________

    @given(val=floats(min_value=float('inf'), allow_nan=False, exclude_min=True))
>   @settings(verbosity=Verbosity.verbose)
    def test_should_be_always_true_aswell(val):

float_inf_strat.py:11: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

val = inf

    @given(val=floats(min_value=float('inf'), allow_nan=False, exclude_min=True))
    @settings(verbosity=Verbosity.verbose)
    def test_should_be_always_true_aswell(val):
>       assert val != float('inf')
E       AssertionError: assert inf != inf
E        +  where inf = float('inf')

float_inf_strat.py:13: AssertionError
-------------------------------------------------------------------------------------------------------------------------------- Hypothesis --------------------------------------------------------------------------------------------------------------------------------
Trying example: test_should_be_always_true_aswell(val=inf)
Traceback (most recent call last):
  File "/home/alexv/Projects/tmp/float_inf_strat.py", line 13, in test_should_be_always_true_aswell
    assert val != float('inf')
AssertionError: assert inf != inf
 +  where inf = float('inf')

Trying example: test_should_be_always_true_aswell(val=inf)
Traceback (most recent call last):
  File "/home/alexv/Projects/tmp/float_inf_strat.py", line 13, in test_should_be_always_true_aswell
    assert val != float('inf')
AssertionError: assert inf != inf
 +  where inf = float('inf')

Falsifying example: test_should_be_always_true_aswell(val=inf)
============================================================================================================================= warnings summary =============================================================================================================================
/home/alexv/.virtualenvs/tmptesthyp/lib/python3.6/site-packages/_pytest/config/__init__.py:752
  /home/alexv/.virtualenvs/tmptesthyp/lib/python3.6/site-packages/_pytest/config/__init__.py:752: PytestWarning: Module already imported so cannot be rewritten: hypothesis
    self._mark_plugins_for_rewrite(hook)

-- Docs: https://docs.pytest.org/en/latest/warnings.html
=================================================================================================================== 2 failed, 1 warnings in 0.08 seconds ===================================================================================================================

```


Discussion:
Gah, I'd handled the `max_value=-inf, allow_infinity=False` cases in #1720, but didn't see the similar problem when adding `exclude_max`.

Unfortunately the solution will involve `raise InvalidArgument(...)`, so while the invariants will be upheld you'll still have to ensure that you don't call `floats()` with such a combination of arguments.
There is therefore a related question : 
How can I avoid this : 
```
E           KeyError: builds(MPFloat, floats(max_value=-1.7976931348623157e+308, allow_nan=False, allow_infinity=False, exclude_max=True))
-- snip --
E           hypothesis.errors.InvalidArgument: allow_infinity=False excludes max_value=-inf
```
It seems somewhere along the way my large float (likely around 64bits representable float limitations) has been reinterpreted as infinite.
Is there anyway to anticipate that ? preventively with an `assume()` call for example ?
FWIW, the float was generated with a simple `draw(hypothesis.strategies.floats(allow_nan=False))`


Your problem here is that `max_value=-1.7976931348623157e+308` is the most-negative finite number representable as a 64-bit float.  `exclude_max=True` therefore means that there are no allowed finite numbers, and `allow_infinity=False` means that *nothing* is allowed.  (We should really improve this error message)

If you want a finite float that is always valid as an exclusive finite bound, you can draw one from `floats(sys.float_info.min, sys.float_info.max, exclude_min=True, exclude_max=True)`.  A filter would also work, but is tricky to implement without e.g. [Numpy's `nextafter` function](https://docs.scipy.org/doc/numpy/reference/generated/numpy.nextafter.html) and slightly less efficient.
Thanks I managed to cover my use case for now.
I am trying to test some [mpmath](https://github.com/fredrik-johansson/mpmath) related code with hypothesis.

In case it helps someone later, I first generate a mpmath.mpi (multiprecision interval) with : 
```py
@composite
def mpi_strat(draw)
        low_bound = draw(hypothesis.strategies.floats(allow_nan=False))
        high_bound = draw(
            hypothesis.strategies.floats(allow_nan=False, min_value=low_bound)
        )
        return mpmath.mpi(low_bound, high_bound)
```

And then to draw a float outside that interval when possible (with potentially infinite bounds).
`a` is the lower bound of the mpi : 
```py
@composite
def underbounds(draw,):
    b = draw(mpi_strat())
    assume(isfinite(b.a) and sys.float_info.min < float(b.a) < sys.float_info.max)
    v = draw(floats(max_value=float(b.a), exclude_max=True, allow_infinity=False))
    return v, b
```

I cannot test outside the representable float range, but it should be enough for my use-case (I m using mpmath for precision tracking purposes, not large numbers) so I'm fine with that.


Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/_strategies.py b/hypothesis-python/src/hypothesis/_strategies.py
--- a/hypothesis-python/src/hypothesis/_strategies.py
+++ b/hypothesis-python/src/hypothesis/_strategies.py
@@ -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)
diff --git a/hypothesis-python/src/hypothesis/internal/floats.py b/hypothesis-python/src/hypothesis/internal/floats.py
--- a/hypothesis-python/src/hypothesis/internal/floats.py
+++ b/hypothesis-python/src/hypothesis/internal/floats.py
@@ -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()


Test patch:
diff --git a/hypothesis-python/tests/cover/test_float_nastiness.py b/hypothesis-python/tests/cover/test_float_nastiness.py
--- a/hypothesis-python/tests/cover/test_float_nastiness.py
+++ b/hypothesis-python/tests/cover/test_float_nastiness.py
@@ -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
 
@@ -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))
@@ -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
 
 
@@ -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()
diff --git a/hypothesis-python/tests/nocover/test_pretty_repr.py b/hypothesis-python/tests/nocover/test_pretty_repr.py
--- a/hypothesis-python/tests/nocover/test_pretty_repr.py
+++ b/hypothesis-python/tests/nocover/test_pretty_repr.py
@@ -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
 
 
@@ -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)