Repository: HypothesisWorks/hypothesis

Issue:
Check for overflow when casting non-int/float types in Numpy arrays
In [this StackOverflow question](https://stackoverflow.com/questions/52410975/hypothesis-strategy-generating-inf-when-specifically-asked-not-to), the lack of overflow warnings for complex numbers led to some confusion.  

We should add a check for overflow in arrays of dtype `complex64` (`complex128` and larger can't overflow, because we only generate 64-bit floats).  Here's the code that currently handles this - it's probably sufficient to apply the floats check to both real and imaginary parts:

https://github.com/HypothesisWorks/hypothesis/blob/b4a064f0d922c04a53cc8a8b498260875110c2a4/hypothesis-python/src/hypothesis/extra/numpy.py#L135-L148

If possible we should also check for truncation in string arrays.  


Discussion:

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/extra/numpy.py b/hypothesis-python/src/hypothesis/extra/numpy.py
--- a/hypothesis-python/src/hypothesis/extra/numpy.py
+++ b/hypothesis-python/src/hypothesis/extra/numpy.py
@@ -133,7 +133,7 @@ def __init__(self, element_strategy, shape, dtype, fill, unique):
         self.unique = unique
 
         # Used by self.insert_element to check that the value can be stored
-        # in the array without e.g. overflowing.  See issue #1385.
+        # in the array without e.g. overflowing.  See issues #1385 and #1591.
         if dtype.kind in (u'i', u'u'):
             self.check_cast = lambda x: np.can_cast(x, self.dtype, 'safe')
         elif dtype.kind == u'f' and dtype.itemsize == 2:
@@ -144,24 +144,36 @@ def __init__(self, element_strategy, shape, dtype, fill, unique):
             max_f4 = (2. - 2 ** -23) * 2 ** 127
             self.check_cast = lambda x: \
                 (not np.isfinite(x)) or (-max_f4 <= x <= max_f4)
+        elif dtype.kind == u'c' and dtype.itemsize == 8:
+            max_f4 = (2. - 2 ** -23) * 2 ** 127
+            self.check_cast = lambda x: (not np.isfinite(x)) or (
+                -max_f4 <= x.real <= max_f4 and -max_f4 <= x.imag <= max_f4
+            )
+        elif dtype.kind == u'U':
+            length = dtype.itemsize // 4
+            self.check_cast = \
+                lambda x: len(x) <= length and u'\0' not in x[length:]
+        elif dtype.kind in (u'S', u'a'):
+            self.check_cast = lambda x: len(x) <= dtype.itemsize and \
+                b'\0' not in x[dtype.itemsize:]
         else:
             self.check_cast = lambda x: True
 
     def set_element(self, data, result, idx, strategy=None):
         strategy = strategy or self.element_strategy
         val = data.draw(strategy)
+        result[idx] = val
         if self._report_overflow and not self.check_cast(val):
             note_deprecation(
                 'Generated array element %r from %r cannot be represented as '
-                'dtype %r without overflow or underflow.  Consider using a '
-                'more precise strategy, as this will be an error in a future '
-                'version of Hypothesis.' % (val, strategy, self.dtype)
+                'dtype %r - instead it becomes %r .  Consider using a more '
+                'precise strategy, as this will be an error in a future '
+                'version.' % (val, strategy, self.dtype, result[idx])
             )
             # Because the message includes the value of the generated element,
             # it would be easy to spam users with thousands of warnings.
             # We therefore only warn once per draw, unless in verbose mode.
             self._report_overflow = current_verbosity() >= Verbosity.verbose
-        result[idx] = val
 
     def do_draw(self, data):
         if 0 in self.shape:


Test patch:
diff --git a/hypothesis-python/tests/numpy/test_gen_data.py b/hypothesis-python/tests/numpy/test_gen_data.py
--- a/hypothesis-python/tests/numpy/test_gen_data.py
+++ b/hypothesis-python/tests/numpy/test_gen_data.py
@@ -316,14 +316,29 @@ def test_overflowing_integers_are_deprecated(fill, data):
 
 
 @pytest.mark.parametrize('fill', [False, True])
+@pytest.mark.parametrize('dtype,strat', [
+    ('float16', st.floats(min_value=65520, allow_infinity=False)),
+    ('complex64', st.complex_numbers(10**300, allow_infinity=False)),
+    ('U1', st.text(min_size=2, max_size=2)),
+    ('S1', st.binary(min_size=2, max_size=2)),
+])
 @checks_deprecated_behaviour
-@given(st.data())
-def test_overflowing_floats_are_deprecated(fill, data):
-    kw = dict(elements=st.floats(min_value=65520, allow_infinity=False))
+@given(data=st.data())
+def test_unrepresentable_elements_are_deprecated(fill, dtype, strat, data):
     if fill:
-        kw = dict(elements=st.nothing(), fill=kw['elements'])
-    arr = data.draw(nps.arrays(dtype='float16', shape=(1,), **kw))
-    assert np.isinf(arr[0])
+        kw = dict(elements=st.nothing(), fill=strat)
+    else:
+        kw = dict(elements=strat)
+    arr = data.draw(nps.arrays(dtype=dtype, shape=(1,), **kw))
+    try:
+        # This is a float or complex number, and has overflowed to infinity,
+        # triggering our deprecation for overflow.
+        assert np.isinf(arr[0])
+    except TypeError:
+        # We tried to call isinf on a string.  The string was generated at
+        # length two, then truncated by the dtype of size 1 - deprecation
+        # again.  If the first character was \0 it is now the empty string.
+        assert len(arr[0]) <= 1
 
 
 @given(nps.arrays(dtype='float16', shape=(1,)))