Repository: HypothesisWorks/hypothesis

Issue:
decimals() fails on a range entirely between two integers
It appears `decimals()` is passing its `min_value` and `max_value` down to a call to `integers()`, which fails when there couldn't possibly be an integer between them (but there could be a decimal).

minimal reproducible example:
```python
from decimal import Decimal
from hypothesis import given, strategies as st

@given(st.decimals(min_value=Decimal('0.1'), max_value=Decimal('0.3')))
def test_tight_decimal_range(num):
    pass
```

```
tmpproj.test_tight_decimal_range ... FAIL

======================================================================
FAIL: tmpproj.test_tight_decimal_range
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/usr/local/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/private/var/folders/fl/lt_vq2n17117s6n2qp9s7z2m0000gn/T/tmp.WjAPvqmP/tmpproj.py", line 5, in test_tight_decimal_range
    def test_tight_decimal_range(num):
  File "/usr/local/lib/python2.7/site-packages/hypothesis/core.py", line 634, in wrapped_test
    state.run()
  File "/usr/local/lib/python2.7/site-packages/hypothesis/core.py", line 478, in run
    runner.run()
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/engine.py", line 235, in run
    self._run()
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/engine.py", line 387, in _run
    self.test_function(data)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/engine.py", line 97, in test_function
    self._test_function(data)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/core.py", line 465, in evaluate_test_data
    escalate_hypothesis_internal_error()
  File "/usr/local/lib/python2.7/site-packages/hypothesis/core.py", line 448, in evaluate_test_data
    self.search_strategy, self.test,
  File "/usr/local/lib/python2.7/site-packages/hypothesis/executors.py", line 58, in default_new_style_executor
    return function(data)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/core.py", line 103, in run
    args, kwargs = data.draw(search_strategy)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/data.py", line 104, in draw
    return strategy.do_draw(self)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/deferred.py", line 117, in do_draw
    return data.draw(self.wrapped_strategy)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/data.py", line 104, in draw
    return strategy.do_draw(self)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/collections.py", line 63, in do_draw
    data.draw(e) for e in self.element_strategies
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/collections.py", line 59, in newtuple
    return tuple(xs)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/collections.py", line 63, in <genexpr>
    data.draw(e) for e in self.element_strategies
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/data.py", line 104, in draw
    return strategy.do_draw(self)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/strategies.py", line 279, in do_draw
    return self.pack(self.mapped_strategy.do_draw(data))
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/deferred.py", line 117, in do_draw
    return data.draw(self.wrapped_strategy)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/data.py", line 104, in draw
    return strategy.do_draw(self)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/strategies.py", line 279, in do_draw
    return self.pack(self.mapped_strategy.do_draw(data))
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/collections.py", line 63, in do_draw
    data.draw(e) for e in self.element_strategies
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/collections.py", line 59, in newtuple
    return tuple(xs)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/collections.py", line 63, in <genexpr>
    data.draw(e) for e in self.element_strategies
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/data.py", line 104, in draw
    return strategy.do_draw(self)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/deferred.py", line 117, in do_draw
    return data.draw(self.wrapped_strategy)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/data.py", line 104, in draw
    return strategy.do_draw(self)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/strategies.py", line 225, in do_draw
    return data.draw(self.element_strategies[i])
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/data.py", line 104, in draw
    return strategy.do_draw(self)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/strategies.py", line 279, in do_draw
    return self.pack(self.mapped_strategy.do_draw(data))
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/deferred.py", line 117, in do_draw
    return data.draw(self.wrapped_strategy)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/data.py", line 104, in draw
    return strategy.do_draw(self)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/flatmapped.py", line 44, in do_draw
    return data.draw(self.expand(source))
  File "/usr/local/lib/python2.7/site-packages/hypothesis/internal/conjecture/data.py", line 104, in draw
    return strategy.do_draw(self)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/deferred.py", line 117, in do_draw
    return data.draw(self.wrapped_strategy)
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/deferred.py", line 79, in wrapped_strategy
    *[unwrap_strategies(s) for s in self.__args],
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/deferred.py", line 39, in unwrap_strategies
    s = s.wrapped_strategy
  File "/usr/local/lib/python2.7/site-packages/hypothesis/searchstrategy/deferred.py", line 81, in wrapped_strategy
    for k, v in self.__kwargs.items()})
  File "/usr/local/lib/python2.7/site-packages/hypothesis/strategies.py", line 232, in integers
    assert min_int_value <= max_int_value
AssertionError

----------------------------------------------------------------------
Ran 1 test in 0.014s

FAILED (failures=1)
```


Discussion:
for now the trick of widening the range and then mapping it down tighter is a viable workaround:

```python
st.decimals(min_value=10, max_value=30).map(lambda d: d / 100)
```
Tracing so far: this can happen when `decimals()` uses `fractions()` to provide the data.  `fractions()` then passes through the bounding values (which are `Decimal` objects) to `integers()`, which multiplies by the denominator then casts them to `int` and thus collapses ranges of  integer `i .. i + 1/denom` (denom being any natural number).

I think this is actually best fixed in `fractions()`, which should accept `Fraction` objects as bounds, convert to safe integers, and divide the output (carefully handling the `max_denominator`, too...).  The call in `decimals()` would then simply cast its arguments to fractions.  `integers()` should use floor for it's upper bound and ceil for the lower bound.

Note that if `fractions()` is currently given non-integer bounds it may produce out-of-bounds examples and/or fail to produce in-bounds examples due to the casting mentioned above.

Meta-comment: we should write tests to ensure that when bounds are given to a strategy, the examples all lie within the given bounds.  I'd also like to test for completeness, but that's probably too expensive to do reliably.  

Resolving patch:
diff --git a/src/hypothesis/internal/compat.py b/src/hypothesis/internal/compat.py
--- a/src/hypothesis/internal/compat.py
+++ b/src/hypothesis/internal/compat.py
@@ -523,6 +523,12 @@ def ceil(x):
     ceil = math.ceil
 
 
+try:
+    from math import gcd
+except ImportError:
+    from fractions import gcd
+
+
 if PY2:
     def b64decode(s):
         from base64 import b64decode as base
diff --git a/src/hypothesis/strategies.py b/src/hypothesis/strategies.py
--- a/src/hypothesis/strategies.py
+++ b/src/hypothesis/strategies.py
@@ -29,8 +29,9 @@
 from hypothesis.control import assume
 from hypothesis._settings import note_deprecation
 from hypothesis.searchstrategy import SearchStrategy
-from hypothesis.internal.compat import ceil, floor, hrange, text_type, \
-    integer_types, get_type_hints, getfullargspec, implements_iterator
+from hypothesis.internal.compat import gcd, ceil, floor, hrange, \
+    text_type, integer_types, get_type_hints, getfullargspec, \
+    implements_iterator
 from hypothesis.internal.floats import is_negative, float_to_int, \
     int_to_float, count_between_floats
 from hypothesis.utils.conventions import infer, not_set
@@ -1005,31 +1006,86 @@ def fractions(min_value=None, max_value=None, max_denominator=None):
     """Returns a strategy which generates Fractions.
 
     If min_value is not None then all generated values are no less than
-    min_value.
+    min_value.  If max_value is not None then all generated values are no
+    greater than max_value.  min_value and max_value may be anything accepted
+    by the `python:`~fractions.Fraction` constructor.
 
-    If max_value is not None then all generated values are no greater than
-    max_value.
-
-    If max_denominator is not None then the absolute value of the denominator
-    of any generated values is no greater than max_denominator. Note that
-    max_denominator must be at least 1.
+    If max_denominator is not None then the denominator of any generated
+    values is no greater than max_denominator. Note that max_denominator must
+    be None or a positive integer.
 
     """
+    if min_value is not None:
+        min_value = Fraction(min_value)
+    if max_value is not None:
+        max_value = Fraction(max_value)
+
     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')
-
     check_valid_integer(max_denominator)
-    if max_denominator is not None and max_denominator < 1:
-        raise InvalidArgument(
-            u'Invalid denominator bound %s' % max_denominator
-        )
 
-    denominator_strategy = integers(min_value=1, max_value=max_denominator)
+    if max_denominator is not None:
+        if max_denominator < 1:
+            raise InvalidArgument(
+                'max_denominator=%r must be >= 1' % max_denominator)
+
+        def fraction_bounds(value):
+            """Find the best lower and upper approximation for value."""
+            # Adapted from CPython's Fraction.limit_denominator here:
+            # https://github.com/python/cpython/blob/3.6/Lib/fractions.py#L219
+            if value is None or value.denominator <= max_denominator:
+                return value, value
+            p0, q0, p1, q1 = 0, 1, 1, 0
+            n, d = value.numerator, value.denominator
+            while True:
+                a = n // d
+                q2 = q0 + a * q1
+                if q2 > max_denominator:
+                    break
+                p0, q0, p1, q1 = p1, q1, p0 + a * p1, q2
+                n, d = d, n - a * d
+            k = (max_denominator - q0) // q1
+            low, high = Fraction(p1, q1), Fraction(p0 + k * p1, q0 + k * q1)
+            assert low < value < high
+            return low, high
+
+        # Take the high approximation for min_value and low for max_value
+        bounds = (max_denominator, min_value, max_value)
+        _, min_value = fraction_bounds(min_value)
+        max_value, _ = fraction_bounds(max_value)
+
+        if None not in (min_value, max_value) and min_value > max_value:
+            raise InvalidArgument(
+                'There are no fractions with a denominator <= %r between '
+                'min_value=%r and max_value=%r' % bounds)
+
+    if min_value is not None and min_value == max_value:
+        return just(min_value)
 
     def dm_func(denom):
-        max_num = max_value * denom if max_value is not None else None
-        min_num = min_value * denom if min_value is not None else None
+        """Take denom, construct numerator strategy, and build fraction."""
+        # Four cases of algebra to get integer bounds and scale factor.
+        min_num, max_num = None, None
+        if max_value is None and min_value is None:
+            pass
+        elif min_value is None:
+            max_num = denom * max_value.numerator
+            denom *= max_value.denominator
+        elif max_value is None:
+            min_num = denom * min_value.numerator
+            denom *= min_value.denominator
+        else:
+            low = min_value.numerator * max_value.denominator
+            high = max_value.numerator * min_value.denominator
+            scale = min_value.denominator * max_value.denominator
+            # After calculating our integer bounds and scale factor, we remove
+            # the gcd to avoid drawing more bytes for the example than needed.
+            # Note that `div` can be at most equal to `scale`.
+            div = gcd(scale, gcd(low, high))
+            min_num = denom * low // div
+            max_num = denom * high // div
+            denom *= scale // div
 
         return builds(
             Fraction,
@@ -1037,7 +1093,11 @@ def dm_func(denom):
             just(denom)
         )
 
-    return denominator_strategy.flatmap(dm_func)
+    if max_denominator is None:
+        return integers(min_value=1).flatmap(dm_func)
+
+    return integers(1, max_denominator).flatmap(dm_func).map(
+        lambda f: f.limit_denominator(max_denominator))
 
 
 @cacheable


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
@@ -70,9 +70,10 @@ def fn_ktest(*fnkwargs):
     (ds.decimals, {'max_value': '-inf'}),
     (ds.decimals, {'min_value': '-inf', 'allow_infinity': False}),
     (ds.decimals, {'max_value': 'inf', 'allow_infinity': False}),
-    (ds.fractions, {'min_value': float('nan')}),
     (ds.fractions, {'min_value': 2, 'max_value': 1}),
     (ds.fractions, {'max_denominator': 0}),
+    (ds.fractions, {'min_value': '1/3', 'max_value': '1/3',
+                    'max_denominator': 2}),
     (ds.lists, {}),
     (ds.lists, {'average_size': '5'}),
     (ds.lists, {'average_size': float('nan')}),
@@ -123,9 +124,13 @@ def test_validates_keyword_arguments(fn, kwargs):
     (ds.decimals, {'max_value': 'inf'}),
     (ds.fractions, {
         'min_value': -1, 'max_value': 1, 'max_denominator': 1000}),
+    (ds.fractions, {'min_value': 1, 'max_value': 1}),
+    (ds.fractions, {'min_value': 1, 'max_value': 1, 'max_denominator': 2}),
     (ds.fractions, {'min_value': 1.0}),
     (ds.fractions, {'min_value': decimal.Decimal('1.0')}),
     (ds.fractions, {'min_value': fractions.Fraction(1, 2)}),
+    (ds.fractions, {'min_value': '1/2', 'max_denominator': 1}),
+    (ds.fractions, {'max_value': '1/2', 'max_denominator': 1}),
     (ds.lists, {'max_size': 0}),
     (ds.lists, {'elements': ds.integers()}),
     (ds.lists, {'elements': ds.integers(), 'max_size': 5}),
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
@@ -22,12 +22,34 @@
 
 import pytest
 
-from hypothesis import given, assume
+from hypothesis import given, assume, reject
+from hypothesis.errors import InvalidArgument
 from tests.common.utils import fails
-from hypothesis.strategies import decimals, fractions
+from hypothesis.strategies import data, none, tuples, decimals, integers, \
+    fractions
 from hypothesis.internal.compat import float_to_decimal
 
 
+@given(data())
+def test_fuzz_fractions_bounds(data):
+    denom = data.draw(none() | integers(1, 100))
+    fracs = none() | fractions(max_denominator=denom) \
+        | fractions('1/99', '1/2', denom)
+    try:
+        low, high = data.draw(tuples(fracs, fracs))
+        if low is not None and high is not None and low > high:
+            low, high = high, low
+        val = data.draw(fractions(low, high, denom))
+    except InvalidArgument:
+        reject()  # fractions too close for given max_denominator
+    if low is not None:
+        assert low <= val
+    if high is not None:
+        assert val <= high
+    if denom is not None:
+        assert 1 <= val.denominator <= denom
+
+
 @fails
 @given(decimals())
 def test_all_decimals_can_be_exact_floats(x):
@@ -75,3 +97,8 @@ def inner_tst(n):
 @given(decimals(min_value='0.1', max_value='0.2', allow_nan=False, places=1))
 def test_works_with_few_values(dec):
     assert dec in (decimal.Decimal('0.1'), decimal.Decimal('0.2'))
+
+
+@given(decimals(min_value='0.1', max_value='0.3'))
+def test_issue_739_regression(x):
+    pass