Repository: HypothesisWorks/hypothesis

Issue:
Numpy strategy fails with timedelta64 dtype
I'm getting an error I can't make sense of when generating a numpy array with `dtype=np.dtype("m8")`, which corresponds to `dtype=timedelta64`, and `shape=(1,1)`.
The trace is given below.

Any help would be greatly appreciated!

If it's useful to know, the context is that I'm trying to write a strategy which will generate an arbitrary numpy array of any valid type and shape, within reasonable memory constraints.

```
============================= test session starts ==============================
platform linux -- Python 3.7.6, pytest-5.3.2, py-1.8.1, pluggy-0.13.1
rootdir: /home/whitaker/pkgs/asta
plugins: hypothesis-4.57.1
collected 1 item

asta/tests/test_array.py F                                               [100%]

=================================== FAILURES ===================================
_____________________ test_array_passes_generic_isinstance _____________________

    @given(hnp.arrays(dtype=strats.dtypes(), shape=strats.shapes()))
>   def test_array_passes_generic_isinstance(arr: Array) -> None:

asta/tests/test_array.py:14: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/core.py:698: in _execute_once_for_engine
    escalate_hypothesis_internal_error()
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/core.py:669: in _execute_once_for_engine
    result = self.execute_once(data)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/core.py:624: in execute_once
    result = self.test_runner(data, run)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/executors.py:56: in default_new_style_executor
    return function(data)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/core.py:565: in run
    args, kwargs = data.draw(self.search_strategy)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:887: in draw
    return strategy.do_draw(self)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:61: in do_draw
    return tuple(data.draw(e) for e in self.element_strategies)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:61: in <genexpr>
    return tuple(data.draw(e) for e in self.element_strategies)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:882: in draw
    return strategy.do_draw(self)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/strategies.py:669: in do_draw
    result = self.pack(data.draw(self.mapped_strategy))
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:882: in draw
    return strategy.do_draw(self)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/lazy.py:156: in do_draw
    return data.draw(self.wrapped_strategy)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:882: in draw
    return strategy.do_draw(self)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/strategies.py:669: in do_draw
    result = self.pack(data.draw(self.mapped_strategy))
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:882: in draw
    return strategy.do_draw(self)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:61: in do_draw
    return tuple(data.draw(e) for e in self.element_strategies)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:61: in <genexpr>
    return tuple(data.draw(e) for e in self.element_strategies)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:882: in draw
    return strategy.do_draw(self)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/lazy.py:156: in do_draw
    return data.draw(self.wrapped_strategy)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:882: in draw
    return strategy.do_draw(self)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/flatmapped.py:46: in do_draw
    return data.draw(expanded_source)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:882: in draw
    return strategy.do_draw(self)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/lazy.py:156: in do_draw
    return data.draw(self.wrapped_strategy)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:882: in draw
    return strategy.do_draw(self)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/flatmapped.py:46: in do_draw
    return data.draw(expanded_source)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:882: in draw
    return strategy.do_draw(self)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/strategies/_internal/lazy.py:156: in do_draw
    return data.draw(self.wrapped_strategy)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:882: in draw
    return strategy.do_draw(self)
../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/extra/numpy.py:275: in do_draw
    self.set_element(data, one_element, 0, self.fill)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <hypothesis.extra.numpy.ArrayStrategy object at 0x7f370ea63bd0>
data = ConjectureData(VALID, 14 bytes, frozen)
result = array([0], dtype=timedelta64), idx = 0
strategy = from_dtype(dtype('<m8'))

    def set_element(self, data, result, idx, strategy=None):
        strategy = strategy or self.element_strategy
        val = data.draw(strategy)
>       result[idx] = val
E       TypeError: Cannot cast NumPy timedelta64 scalar from metadata [Y] to  according to the rule 'same_kind'

../../conda3/envs/bees/lib/python3.7/site-packages/hypothesis/extra/numpy.py:161: TypeError
----------------------------- Captured stdout call -----------------------------
Type id: m8
---------------------------------- Hypothesis ----------------------------------
You can add @seed(122549914271068826677002920208694012959) to this test or run pytest with --hypothesis-seed=122549914271068826677002920208694012959 to reproduce this failure.
============================== 1 failed in 0.35s ===============================
```


Discussion:
I *think* what must be happening is that the [array and the scalar value have different units with no consistent conversion factor](https://numpy.org/doc/1.18/reference/arrays.datetime.html#datetime-and-timedelta-arithmetic) (e.g years to days, or days to months).

`arrays(dtype="m8", shape=1).example()` suffices to reproduce this for me 😅 
IMO we should either change our code so that this is not an error, or so that the error message explains what's happening and gives enough information to reproduce the problem.
Thanks for your input, I think for now I'll just exclude any timedelta scalar types from the strategy. I agree, it would be nice if the extras strategy worked for all dtypes in `np.sctypeDict`, or raised an error indicated the specified dtype was unsupported by hypothesis, or if there was an built-in option to generate all supported dtypes.
The `scalar_dtypes()` or `array_dtypes()` strategy will do that for you!

--------------------

The problematic `from_type()` code is:

https://github.com/HypothesisWorks/hypothesis/blob/f781f78cab528786fbc1a3aa27da0769b6af4109/hypothesis-python/src/hypothesis/extra/numpy.py#L90-L95

and I think the second branch should actually be an error, and explain that dtypes without a time unit can't be resolved to a strategy.

We don't have to hurt usability much if we then get *`arrays()`* to convert generic datetime64 or timedelta64 dtypes into a `sampled_from(...)` over each possible resolution.  Net result: working as intended via `arrays()`, forces explicit units to ensure validity via `from_dtype()`.
Ah fantastic, I should have read the entire docs page before deciding I needed to implement one myself. Thanks for the tip!

>to convert generic datetime64 or timedelta64 dtypes into a sampled_from(...) over each possible resolution.

Yes if I understand you correctly that seems like a nice solution that covers every case.

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
@@ -91,6 +91,9 @@ def from_dtype(dtype: np.dtype) -> st.SearchStrategy[Any]:
         if "[" in dtype.str:
             res = st.just(dtype.str.split("[")[-1][:-1])
         else:
+            # Note that this case isn't valid to pass to arrays(), but we support
+            # it here because we'd have to guard against equivalents in arrays()
+            # regardless and drawing scalars is a valid use-case.
             res = st.sampled_from(TIME_RESOLUTIONS)
         result = st.builds(dtype.type, st.integers(-(2 ** 63), 2 ** 63 - 1), res)
     else:
@@ -135,7 +138,13 @@ def __init__(self, element_strategy, shape, dtype, fill, unique):
     def set_element(self, data, result, idx, strategy=None):
         strategy = strategy or self.element_strategy
         val = data.draw(strategy)
-        result[idx] = val
+        try:
+            result[idx] = val
+        except TypeError as err:
+            raise InvalidArgument(
+                "Could not add element=%r of %r to array of %r - possible mismatch "
+                "of time units in dtypes?" % (val, val.dtype, result.dtype)
+            ) from err
         if self._check_elements and val != result[idx] and val == val:
             raise InvalidArgument(
                 "Generated array element %r from %r cannot be represented as "
@@ -380,7 +389,18 @@ def arrays(
     # From here on, we're only dealing with values and it's relatively simple.
     dtype = np.dtype(dtype)
     if elements is None:
+        if dtype.kind in ("m", "M") and "[" not in dtype.str:
+            # For datetime and timedelta dtypes, we have a tricky situation -
+            # because they *may or may not* specify a unit as part of the dtype.
+            # If not, we flatmap over the various resolutions so that array
+            # elements have consistent units but units may vary between arrays.
+            return (
+                st.sampled_from(TIME_RESOLUTIONS)
+                .map((dtype.str + "[{}]").format)
+                .flatmap(lambda d: arrays(d, shape=shape, fill=fill, unique=unique))
+            )
         elements = from_dtype(dtype)
+    check_type(SearchStrategy, elements, "elements")
     if isinstance(shape, int):
         shape = (shape,)
     shape = tuple(shape)


Test patch:
diff --git a/hypothesis-python/tests/numpy/test_argument_validation.py b/hypothesis-python/tests/numpy/test_argument_validation.py
--- a/hypothesis-python/tests/numpy/test_argument_validation.py
+++ b/hypothesis-python/tests/numpy/test_argument_validation.py
@@ -45,6 +45,7 @@ def e(a, **kwargs):
         e(nps.arrays, dtype=object, shape=1),
         e(nps.arrays, dtype=float, shape=1, fill=3),
         e(nps.arrays, dtype="U", shape=1, elements=st.just("abc\0\0")),
+        e(nps.arrays, dtype=int, shape=1, elements="not a strategy"),
         e(nps.byte_string_dtypes, min_len=-1),
         e(nps.byte_string_dtypes, min_len=2, max_len=1),
         e(nps.byte_string_dtypes, min_len=0, max_len=0),
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
@@ -1240,3 +1240,22 @@ def test_basic_indices_generate_valid_indexers(
         assert min_dims <= view.ndim <= (32 if max_dims is None else max_dims)
         if view.size:
             assert np.shares_memory(view, array)
+
+
+@pytest.mark.parametrize("dtype_str", ["m8", "M8"])
+@given(data=st.data())
+def test_from_dtype_works_without_time_unit(data, dtype_str):
+    arr = data.draw(nps.from_dtype(np.dtype(dtype_str)))
+    assert (dtype_str + "[") in arr.dtype.str
+
+
+@pytest.mark.parametrize("dtype_str", ["m8", "M8"])
+@given(data=st.data())
+def test_arrays_selects_consistent_time_unit(data, dtype_str):
+    arr = data.draw(nps.arrays(dtype_str, 10))
+    assert (dtype_str + "[") in arr.dtype.str
+
+
+def test_arrays_gives_useful_error_on_inconsistent_time_unit():
+    with pytest.raises(InvalidArgument, match="mismatch of time units in dtypes"):
+        nps.arrays("m8[Y]", 10, elements=nps.from_dtype(np.dtype("m8[D]"))).example()