Repository: HypothesisWorks/hypothesis

Issue:
Give floats() and decimals() allow_nan and allow_infinity flags.
In order to preserve the current behaviour these should default to True. When set to False, the strategies should not generate NaN or infinite values respectively.

A decent first pass at this would be to just use these flags to filter after the existing generation. It would be good to add these flags to the NastyFloats constructor explicitly though.

decimals() can just pass this through to floats() I think.



Discussion:
I'm having a go at this, just one design question:

> In order to preserve the current behaviour these should default to True.

If the signature is `def floats(min_value=None, max_value=None, allow_inf=True, allow_nan=True)`, then I think a call like `floats(0, 1000)` becomes self contradictory - `allow_inf==True`, but +/-inf is outside the range specified. Would you be content with `def floats(... allow_inf=None, allow_nan=None)`, plus logic inside to validate on true/false or initialise on None?

Personally I think it makes sense to keep the signature and just enforce the constraints at runtime (e.g. raise `ValueError` or something if you have `max_value` and `allow_inf`)

> keep the signature ... raise ValueError or something if you have max_value and allow_inf

I presume by singature you mean `def floats(... allow_inf=True ...)`. If so, that would break backward compatibility - since existing calls (e.g. `floats(max_value=10)`) would start defaulting to `allow_inf=True`, and hence raise an exception.

@moreati Note that I have just merged https://github.com/DRMacIver/hypothesis/pull/239 which does some of the work.

I think the suggestion of having them default to None and work out the values from the other parameters is a good one though, and I agree that raising an error with contradictory flags is also good (Hypothesis has an InvalidArgument error for that). If you wanted to have a go at adding that I'd be very grateful. :-)


Resolving patch:
diff --git a/src/hypothesis/strategies.py b/src/hypothesis/strategies.py
--- a/src/hypothesis/strategies.py
+++ b/src/hypothesis/strategies.py
@@ -803,21 +803,25 @@ def dm_func(denom):
 
 @cacheable
 @defines_strategy
-def decimals(min_value=None, max_value=None):
+def decimals(
+    min_value=None, max_value=None, allow_nan=None, allow_infinity=None
+):
     """Generates instances of decimals.Decimal.
 
-    If min_value is not None then all generated values are no less than
-    min_value.
+    - If min_value is not None, all values will be >= min_value.
+    - If max_value is not None, all values will be <= max_value.
+    - If min_value or max_value is not None, it is an error to enable
+      allow_nan.
+    - If both min_value and max_value are not None, it is an error to enable
+      allow_infinity.
 
-    If max_value is not None then all generated values are no greater than
-    max_value.
+    Where not explicitly ruled out by the bounds, all of infinity, -infinity
+    and NaN are possible values generated by this strategy.
 
     """
-    check_valid_bound(min_value, 'min_value')
-    check_valid_bound(max_value, 'max_value')
-    check_valid_interval(min_value, max_value, 'min_value', 'max_value')
-
-    floats_strategy = floats(min_value=min_value, max_value=max_value)
+    floats_strategy = floats(
+        min_value=min_value, max_value=max_value,
+        allow_nan=allow_nan, allow_infinity=allow_infinity)
     fractions_strategy = fractions(min_value=min_value, max_value=max_value)
 
     return (


Test patch:
diff --git a/tests/cover/test_direct_strategies.py b/tests/cover/test_direct_strategies.py
--- a/tests/cover/test_direct_strategies.py
+++ b/tests/cover/test_direct_strategies.py
@@ -59,6 +59,11 @@ def fn_ktest(*fnkwargs):
     (ds.integers, {'min_value': 2, 'max_value': 1}),
     (ds.decimals, {'min_value': float('nan')}),
     (ds.decimals, {'min_value': 2, 'max_value': 1}),
+    (ds.decimals, {'max_value': 0.0, 'min_value': 1.0}),
+    (ds.decimals, {'min_value': 0.0, 'allow_nan': True}),
+    (ds.decimals, {'max_value': 0.0, 'allow_nan': True}),
+    (ds.decimals, {
+        'min_value': 0.0, 'max_value': 1.0, 'allow_infinity': True}),
     (ds.fractions, {'min_value': float('nan')}),
     (ds.fractions, {'min_value': 2, 'max_value': 1}),
     (ds.fractions, {'max_denominator': 0}),
@@ -79,6 +84,9 @@ def fn_ktest(*fnkwargs):
     (ds.binary, {'max_size': 10, 'average_size': 20}),
     (ds.floats, {'min_value': float('nan')}),
     (ds.floats, {'max_value': 0.0, 'min_value': 1.0}),
+    (ds.floats, {'min_value': 0.0, 'allow_nan': True}),
+    (ds.floats, {'max_value': 0.0, 'allow_nan': True}),
+    (ds.floats, {'min_value': 0.0, 'max_value': 1.0, 'allow_infinity': True}),
     (ds.fixed_dictionaries, {'mapping': 'fish'}),
     (ds.fixed_dictionaries, {'mapping': {1: 'fish'}}),
     (ds.dictionaries, {'keys': ds.integers(), 'values': 1}),
@@ -100,6 +108,11 @@ def test_validates_keyword_arguments(fn, kwargs):
     (ds.decimals, {'min_value': 1.0, 'max_value': 1.5}),
     (ds.decimals, {'min_value': decimal.Decimal('1.5')}),
     (ds.decimals, {'min_value': fractions.Fraction(1, 3)}),
+    (ds.decimals, {
+        'max_value': 1.0, 'min_value': -1.0, 'allow_infinity': False}),
+    (ds.decimals, {'min_value': 1.0, 'allow_nan': False}),
+    (ds.decimals, {'max_value': 1.0, 'allow_nan': False}),
+    (ds.decimals, {'max_value': 1.0, 'min_value': -1.0, 'allow_nan': False}),
     (ds.fractions, {
         'min_value': -1, 'max_value': 1, 'max_denominator': 1000}),
     (ds.fractions, {'min_value': 1.0}),
@@ -122,6 +135,11 @@ def test_validates_keyword_arguments(fn, kwargs):
     (ds.floats, {'min_value': 1.0}),
     (ds.floats, {'max_value': 1.0}),
     (ds.floats, {'max_value': 1.0, 'min_value': -1.0}),
+    (ds.floats, {
+        'max_value': 1.0, 'min_value': -1.0, 'allow_infinity': False}),
+    (ds.floats, {'min_value': 1.0, 'allow_nan': False}),
+    (ds.floats, {'max_value': 1.0, 'allow_nan': False}),
+    (ds.floats, {'max_value': 1.0, 'min_value': -1.0, 'allow_nan': False}),
     (ds.sampled_from, {'elements': [1]}),
     (ds.sampled_from, {'elements': [1, 2, 3]}),
     (ds.fixed_dictionaries, {'mapping': {1: ds.integers()}}),
diff --git a/tests/cover/test_numerics.py b/tests/cover/test_numerics.py
--- a/tests/cover/test_numerics.py
+++ b/tests/cover/test_numerics.py
@@ -17,8 +17,10 @@
 
 from __future__ import division, print_function, absolute_import
 
+import math
+
 from hypothesis import given, assume
-from tests.common.utils import fails
+from tests.common.utils import fails, fails_with
 from hypothesis.strategies import decimals, fractions, float_to_decimal
 
 
@@ -32,3 +34,25 @@ def test_all_decimals_can_be_exact_floats(x):
 @given(fractions(), fractions(), fractions())
 def test_fraction_addition_is_well_behaved(x, y, z):
     assert x + y + z == y + x + z
+
+
+@fails_with(AssertionError)
+@given(decimals())
+def test_decimals_include_nan(x):
+    assert not math.isnan(x)
+
+
+@fails_with(AssertionError)
+@given(decimals())
+def test_decimals_include_inf(x):
+    assert not math.isinf(x)
+
+
+@given(decimals(allow_nan=False))
+def test_decimals_can_disallow_nan(x):
+    assert not math.isnan(x)
+
+
+@given(decimals(allow_infinity=False))
+def test_decimals_can_disallow_inf(x):
+    assert not math.isinf(x)