Repository: HypothesisWorks/hypothesis

Issue:
Improve error message for unsupported `datetime` dtypes in Pandas extra
In [this StackOverflow question](https://stackoverflow.com/questions/74591552/hypothesis-create-column-with-pd-datetime-dtype-in-given-test-dataframe), the asker notes that `column('timestamp', dtype=pd.datetime)` doesn't actually create a datetime column in the generated dataframe.  This turns out to be a fairly general issue: passing `dtype=datetime.datetime` will actually create an *object* column too, and even passing `dtype="datetime64"` is problematic as Hypothesis will choose various time-resolution units.  I think we should therefore:

1. Ensure that if you get an error when we can tell that you probably wanted datetimes, we explicitly suggest `dtype="datetime64[ns]"` as part of the error message
2. `note_deprecation(...)` if we can detect this and it *wasn't* previously an error
3. `note_deprecation(...)` if the time-resolution unit is not `ns` - others are valid in Numpy but not Pandas
4. Investigate doing the same for `pd.Timedelta` and related dtype arguments ([see docs here](https://pandas.pydata.org/docs/user_guide/timeseries.html))
Issue #3518 fix attempt 1
Attempting to fix issue #3518 after clarification with @Zac-HD . Thanks!


Discussion:
I'm currently working on fixing this issue and wanted to ask for some guidance. I ran the [example in the stack overflow question](https://stackoverflow.com/questions/74591552/hypothesis-create-column-with-pd-datetime-dtype-in-given-test-dataframe) and got the following error:

```
practice.py:8: FutureWarning: The pandas.datetime class is deprecated and will be removed from pandas in a future version. Import from datetime module instead.
  @given(data_frames([column('A', dtype=str), column('B', dtype=int), column('Date', dtype=pd.datetime)]))
/home/spoderman94/basis_project/venv/lib/python3.8/site-packages/hypothesis/extra/pandas/impl.py:84: HypothesisDeprecationWarning: Passed dtype=<class 'datetime.datetime'> is not a valid Pandas dtype.  We'll treat it as dtype=object for now, but this will be an error in a future version.
  note_deprecation(
Traceback (most recent call last):
  File "practice.py", line 13, in <module>
    s.test_example_test_method()
  File "practice.py", line 9, in test_example_test_method
    def test_example_test_method(self, dataframe):
  File "/home/spoderman94/basis_project/venv/lib/python3.8/site-packages/hypothesis/core.py", line 1397, in wrapped_test
    raise the_error_hypothesis_found
  File "/home/spoderman94/basis_project/venv/lib/python3.8/site-packages/hypothesis/extra/numpy.py", line 183, in from_dtype
    raise InvalidArgument(f"No strategy inference for {dtype}")
hypothesis.errors.InvalidArgument: No strategy inference for object
```
I noticed that there was a deprecation message being sent out from the "extra/pandas/impl.py" file, specifically in a function called "elements_and_dtype." I was wondering: would it be appropriate to place a "dtype=pd.datetime"-specific deprecation notice in this area? This is what the function looks like: https://github.com/HypothesisWorks/hypothesis/blob/173de52291ea85c7a169b5c1f536efdd8beb2873/hypothesis-python/src/hypothesis/extra/pandas/impl.py#L60-L99

Additionally, the place where the error is actually raised is from "extras/numpy.py" in an area that looks like it's checking the different numpy "dtype.kind" properties of the given dtype. Would it be appropriate to place a "dtype=pd.datetime"-specific error message here? My thinking is that the deprecation notice that I mentioned above should be enough for the user to deduce that they may have used a deprecated datetime object as the dtype, but in your raised issue message you said that:

> Ensure that if you get an error when we can tell that you probably wanted datetimes, we explicitly suggest dtype="datetime64[ns]" as part of the error message

Does this mean to do it in the raised error message? There is another piece that I want to explore which is the "try_convert" method from the "hypothesis.internal.validation" part of the program but I wanted to reach out for guidance just in case I'm overcomplicating things (as I tend to do). Thanks!


Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/extra/pandas/impl.py b/hypothesis-python/src/hypothesis/extra/pandas/impl.py
--- a/hypothesis-python/src/hypothesis/extra/pandas/impl.py
+++ b/hypothesis-python/src/hypothesis/extra/pandas/impl.py
@@ -92,8 +92,15 @@ def elements_and_dtype(elements, dtype, source=None):
         )
 
     if isinstance(dtype, type) and np.dtype(dtype).kind == "O" and dtype is not object:
+        err_msg = f"Passed dtype={dtype!r} is not a valid Pandas dtype."
+        if issubclass(dtype, datetime):
+            err_msg += ' To generate valid datetimes, pass `dtype="datetime64[ns]"`'
+            raise InvalidArgument(err_msg)
+        elif issubclass(dtype, timedelta):
+            err_msg += ' To generate valid timedeltas, pass `dtype="timedelta64[ns]"`'
+            raise InvalidArgument(err_msg)
         note_deprecation(
-            f"Passed dtype={dtype!r} is not a valid Pandas dtype.  We'll treat it as "
+            f"{err_msg}  We'll treat it as "
             "dtype=object for now, but this will be an error in a future version.",
             since="2021-12-31",
             has_codemod=False,
diff --git a/hypothesis-python/src/hypothesis/version.py b/hypothesis-python/src/hypothesis/version.py
--- a/hypothesis-python/src/hypothesis/version.py
+++ b/hypothesis-python/src/hypothesis/version.py
@@ -8,5 +8,5 @@
 # v. 2.0. If a copy of the MPL was not distributed with this file, You can
 # obtain one at https://mozilla.org/MPL/2.0/.
 
-__version_info__ = (6, 73, 1)
+__version_info__ = (6, 74, 0)
 __version__ = ".".join(map(str, __version_info__))
diff --git a/tooling/src/hypothesistooling/projects/hypothesispython.py b/tooling/src/hypothesistooling/projects/hypothesispython.py
--- a/tooling/src/hypothesistooling/projects/hypothesispython.py
+++ b/tooling/src/hypothesistooling/projects/hypothesispython.py
@@ -97,6 +97,9 @@ def update_changelog_and_version():
             assert CHANGELOG_BORDER.match(lines[i + 2]), repr(lines[i + 2])
             assert CHANGELOG_HEADER.match(lines[i + 3]), repr(lines[i + 3])
             assert CHANGELOG_BORDER.match(lines[i + 4]), repr(lines[i + 4])
+            assert lines[i + 3].startswith(
+                __version__
+            ), f"{__version__=}   {lines[i + 3]=}"
             beginning = "\n".join(lines[:i])
             rest = "\n".join(lines[i:])
             assert "\n".join((beginning, rest)) == contents


Test patch:
diff --git a/hypothesis-python/tests/pandas/test_argument_validation.py b/hypothesis-python/tests/pandas/test_argument_validation.py
--- a/hypothesis-python/tests/pandas/test_argument_validation.py
+++ b/hypothesis-python/tests/pandas/test_argument_validation.py
@@ -8,7 +8,8 @@
 # v. 2.0. If a copy of the MPL was not distributed with this file, You can
 # obtain one at https://mozilla.org/MPL/2.0/.
 
-from datetime import datetime
+import re
+from datetime import datetime, timedelta
 
 import pandas as pd
 import pytest
@@ -126,3 +127,17 @@ def test_pandas_nullable_types_class():
     ):
         st = pdst.series(dtype=pd.core.arrays.integer.Int8Dtype)
         find_any(st, lambda s: s.isna().any())
+
+
+@pytest.mark.parametrize(
+    "dtype_,expected_unit",
+    [
+        (datetime, "datetime64[ns]"),
+        (pd.Timestamp, "datetime64[ns]"),
+        (timedelta, "timedelta64[ns]"),
+        (pd.Timedelta, "timedelta64[ns]"),
+    ],
+)
+def test_invalid_datetime_or_timedelta_dtype_raises_error(dtype_, expected_unit):
+    with pytest.raises(InvalidArgument, match=re.escape(expected_unit)):
+        pdst.series(dtype=dtype_).example()