Repository: HypothesisWorks/hypothesis

Issue:
FailedHealthCheck for decimals(places=X, allow_nan=False, allow_infinity=False)
My tests have suddenly started failing with v3.12.0 due to failing health checks. Worked in v.3.11.6

Minimum test code:
```python
from hypothesis.core import given
from hypothesis.strategies import decimals

@given(x=decimals(places=3, allow_nan=False, allow_infinity=False))
def test_asd(x):
    print(x)
```

If I remove `places`, this works again.

Output:
```
============================================================ FAILURES ============================================================
____________________________________________________________ test_asd ____________________________________________________________
test.py:527: in test_asd
    def test_asd(x):
E   FailedHealthCheck: It looks like your strategy is filtering out a lot of data. Health check found 50 filtered examples but only 0 good ones. This will make your tests much slower, and also will probably distort the data generation quite a lot. You should adapt your strategy to filter less. This can also be caused by a low max_leaves parameter in recursive() calls
E   See https://hypothesis.readthedocs.io/en/latest/healthchecks.html for more information about this. If you want to disable just this health check, add HealthCheck.filter_too_much to the suppress_health_check settings for this test.
==================================================== 1 failed in 3.29 seconds ====================================================
ERROR: InvocationError: 'pytest -s test.py::test_asd'
____________________________________________________________ summary _____________________________________________________________
ERROR:   py27: commands failed
```



Discussion:
@DRMacIver, I'm pretty sure that #710 broke this.  Any ideas?
@Zac-HD it is indeed #710 that broke it, but I think it's a bug that's been latent since #508 that is now being triggered consistently by the new larger integers (which are particularly egregious in health checks because it runs in pure random mode). The specific problem is that we're now hitting the InvalidOperation branch [here](https://github.com/HypothesisWorks/hypothesis-python/blob/master/src/hypothesis/strategies.py#L890) 100% of the time in health checks (why do we have a no cover there anyway?).

I think the problem is that we're using an unrestricted `integers()` but quantize fails for sufficiently large decimals, so we should probably be setting lower and upper bounds to some suitable values.

Do you want to look into doing that or should I? (I'm totally happy to but don't have a lot of bandwidth to do so for the next few days)
I'm certainly happy to take a look, though probably also not in the next few days - I really want to finish #643 before my talk at the August meetup and the PyConAU sprints so that's got priority.
@goodspark it looks like we probably won't to be able to get to this super soon, sorry. If you want to take a look at the code linked above and try to put together a patch, we can help answer questions etc. No worries if not though.

In the meantime as a workaround, my guess (which I've not validated) is that this will work better than the health checks are advertising. If you add `suppress_health_check=[HealthCheck.filter_too_much]` to your settings as described in the message the test will probably start working (though I expect it won't be testing as well as it used to).
@Zac-HD Totally reasonable! I'll let you know if I start working on it before you do. 
FWIW when I recreate the (equivalent) code locally and generate examples, there are only a few cases where `quantize` fails:

```python
from hypothesis.strategies import integers
from hypothesis.strategies import sampled_from
from decimal import InvalidOperation
from decimal import Decimal

def try_quantize(d):
     try:
         return d.quantize(factor)
     except InvalidOperation:
         return None

factor = Decimal(10) ** -3
# same code but without last filter to see how many quantize failures are happening
strat = integers().map(lambda d: try_quantize(d * factor)) | sampled_from([])

vals = [strat.example() for _ in range(10000)]
nones = [x for x in vals if x is None]
float(len(nones))/len(vals) * 100
> 3.09
```
Yeah, there's a difference in how the health checks run from how things run in the normal engine. Unifying the two is something I'll probably be looking into in the near future if things go according to plan.
Ah, got it - this happens when the underlying decimal (draw integer, multiply by Decimal(10)^-places) has greater precision than allowed in the decimal context.  The engine changes in 3.12 made large integers (~ 10^34 ) much more common, and thus we have issues.  This is easily fixed by making the default bounds +- 10^prec when a max or min value is not given.

I'll also add validation for user-supplied enormous inputs, and make a pull request after investigating #739.

Resolving patch:
diff --git a/src/hypothesis/strategies.py b/src/hypothesis/strategies.py
--- a/src/hypothesis/strategies.py
+++ b/src/hypothesis/strategies.py
@@ -20,7 +20,7 @@
 import math
 import datetime as dt
 import operator
-from decimal import Decimal, InvalidOperation
+from decimal import Context, Decimal
 from inspect import isclass
 from numbers import Rational
 from fractions import Fraction
@@ -1110,7 +1110,7 @@ def decimals(min_value=None, max_value=None,
 
     - A finite rational number, between ``min_value`` and ``max_value``.
     - Not a Number, if ``allow_nan`` is True.  None means "allow NaN, unless
-      ``min__value`` and ``max_value`` are not None".
+      ``min_value`` and ``max_value`` are not None".
     - Positive or negative infinity, if ``max_value`` and ``min_value``
       respectively are None, and ``allow_infinity`` is not False.  None means
       "allow infinity, unless excluded by the min and max values".
@@ -1152,26 +1152,41 @@ def decimals(min_value=None, max_value=None,
 
     if allow_infinity and (None not in (min_value, max_value)):
         raise InvalidArgument('Cannot allow infinity between finite bounds')
-    # Set up a strategy for finite decimals
+    # Set up a strategy for finite decimals.  Note that both floating and
+    # fixed-point decimals require careful handling to remain isolated from
+    # any external precision context - in short, we always work out the
+    # required precision for lossless operation and use context methods.
     if places is not None:
         # Fixed-point decimals are basically integers with a scale factor
+        def ctx(val):
+            """Return a context in which this value is lossless."""
+            precision = ceil(math.log10(abs(val) or 1)) + places + 1
+            return Context(prec=max([precision, 1]))
+
+        def int_to_decimal(val):
+            context = ctx(val)
+            return context.quantize(context.multiply(val, factor), factor)
 
-        def try_quantize(d):
-            try:
-                return d.quantize(factor)
-            except InvalidOperation:  # pragma: no cover
-                return None
         factor = Decimal(10) ** -places
-        max_num = max_value / factor if max_value is not None else None
-        min_num = min_value / factor if min_value is not None else None
-        strat = integers(min_value=min_num, max_value=max_num)\
-            .map(lambda d: try_quantize(d * factor))\
-            .filter(lambda d: d is not None)
+        min_num, max_num = None, None
+        if min_value is not None:
+            min_num = ceil(ctx(min_value).divide(min_value, factor))
+        if max_value is not None:
+            max_num = floor(ctx(max_value).divide(max_value, factor))
+        if None not in (min_num, max_num) and min_num > max_num:
+            raise InvalidArgument(
+                'There are no decimals with %d places between min_value=%r '
+                'and max_value=%r ' % (places, min_value, max_value))
+        strat = integers(min_num, max_num).map(int_to_decimal)
     else:
         # Otherwise, they're like fractions featuring a power of ten
-        strat = fractions(
-            min_value=min_value, max_value=max_value
-        ).map(lambda f: Decimal(f.numerator) / f.denominator)
+        def fraction_to_decimal(val):
+            precision = ceil(math.log10(abs(val.numerator) or 1) +
+                             math.log10(val.denominator)) + 1
+            return Context(prec=precision or 1).divide(
+                Decimal(val.numerator), val.denominator)
+
+        strat = fractions(min_value, max_value).map(fraction_to_decimal)
     # Compose with sampled_from for infinities and NaNs as appropriate
     special = []
     if allow_nan or (allow_nan is None and (None in (min_value, max_value))):


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
@@ -88,6 +88,7 @@ def fn_ktest(*fnkwargs):
     (ds.decimals, {'min_value': '-inf', 'allow_infinity': False}),
     (ds.decimals, {'max_value': 'inf', 'allow_infinity': False}),
     (ds.decimals, {'min_value': complex(1, 2)}),
+    (ds.decimals, {'min_value': '0.1', 'max_value': '0.9', 'places': 0}),
     (ds.dictionaries, {
         'keys': ds.booleans(), 'values': ds.booleans(),
         'min_size': 10, 'max_size': 1}),
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
@@ -32,14 +32,13 @@
 
 @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)
+    denom = data.draw(none() | integers(1, 100), label='denominator')
+    fracs = none() | fractions(max_denominator=denom)
+    low, high = data.draw(tuples(fracs, fracs), label='low, high')
+    if low is not None and high is not None and low > high:
+        low, high = high, low
     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))
+        val = data.draw(fractions(low, high, denom), label='value')
     except InvalidArgument:
         reject()  # fractions too close for given max_denominator
     if low is not None:
@@ -50,6 +49,30 @@ def test_fuzz_fractions_bounds(data):
         assert 1 <= val.denominator <= denom
 
 
+@given(data())
+def test_fuzz_decimals_bounds(data):
+    places = data.draw(none() | integers(0, 20), label='places')
+    finite_decs = decimals(allow_nan=False, allow_infinity=False,
+                           places=places) | none()
+    low, high = data.draw(tuples(finite_decs, finite_decs), label='low, high')
+    if low is not None and high is not None and low > high:
+        low, high = high, low
+    ctx = decimal.Context(prec=data.draw(integers(1, 100), label='precision'))
+    try:
+        with decimal.localcontext(ctx):
+            strat = decimals(low, high, allow_nan=False,
+                             allow_infinity=False, places=places)
+            val = data.draw(strat, label='value')
+    except InvalidArgument:
+        reject()  # decimals too close for given places
+    if low is not None:
+        assert low <= val
+    if high is not None:
+        assert val <= high
+    if places is not None:
+        assert val.as_tuple().exponent == -places
+
+
 @fails
 @given(decimals())
 def test_all_decimals_can_be_exact_floats(x):
@@ -99,6 +122,11 @@ def test_works_with_few_values(dec):
     assert dec in (decimal.Decimal('0.1'), decimal.Decimal('0.2'))
 
 
+@given(decimals(places=3, allow_nan=False, allow_infinity=False))
+def test_issue_725_regression(x):
+    pass
+
+
 @given(decimals(min_value='0.1', max_value='0.3'))
 def test_issue_739_regression(x):
     pass