Repository: HypothesisWorks/hypothesis (Python)

Issue:
`KeyError` in `draw_capped_multipart`
Hi,

A `KeyError` is raised in `draw_capped_multipart` in version 5.8.1 and 5.10.1 that is not raised in 5.8.0.

A reproducible example and full stacktrace are given below.

**Error**:

```python
data = ConjectureData(VALID, 1 bytes, frozen), min_value = Timestamp('2017-01-01 00:00:00'), max_value = Timestamp('2019-01-01 00:00:00')

    def draw_capped_multipart(data, min_value, max_value):
        assert isinstance(min_value, (dt.date, dt.time, dt.datetime))
        assert type(min_value) == type(max_value)
        assert min_value <= max_value
        result = {}
        cap_low, cap_high = True, True
        duration_names_by_type = {
            dt.date: DATENAMES,
            dt.time: TIMENAMES,
            dt.datetime: DATENAMES + TIMENAMES,
        }
>       for name in duration_names_by_type[type(min_value)]:
E       KeyError: <class 'pandas._libs.tslibs.timestamps.Timestamp'>
```
<details><summary>Reproducible code:</summary>
<p>

```python
import numpy as np
import pandas as pd

from hypothesis import given, settings
import hypothesis.strategies as hst
from hypothesis.extra import pandas as hpd

@given(
    hpd.data_frames(columns=[
        hpd.column(name='prog_start',
                   elements=hst.datetimes(
                       min_value=pd.Timestamp(2017, 1, 1),
                       max_value=pd.Timestamp(2019, 1, 1)
                   ), unique=True),
        hpd.column(name='mat_code',
                   elements=hst.integers(min_value=100)),
        hpd.column(name='SOME***@#!@%*FLoat',
                   elements=hst.floats(max_value=1000.0)
                   ),
        hpd.column(name='useful_STR',
                   elements=hst.text()
                   ),
        ], index=hpd.range_indexes(min_size=3)
    )
)
@settings(deadline=None)
def test_hypothesis_testing(df):
    pass
```
</p>
</details>

<details><summary>full stacktrace:</summary>
<p>

```python
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/core.py:678: in _execute_once_for_engine
    escalate_hypothesis_internal_error()
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/core.py:649: in _execute_once_for_engine
    result = self.execute_once(data)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/core.py:604: in execute_once
    result = self.test_runner(data, run)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/executors.py:52: in default_new_style_executor
    return function(data)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/core.py:545: in run
    args, kwargs = data.draw(self.search_strategy)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:893: in draw
    return strategy.do_draw(self)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:57: in do_draw
    return tuple(data.draw(e) for e in self.element_strategies)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:57: in <genexpr>
    return tuple(data.draw(e) for e in self.element_strategies)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:888: in draw
    return strategy.do_draw(self)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/strategies.py:658: in do_draw
    result = self.pack(data.draw(self.mapped_strategy))
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:888: in draw
    return strategy.do_draw(self)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/lazy.py:150: in do_draw
    return data.draw(self.wrapped_strategy)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:888: in draw
    return strategy.do_draw(self)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/strategies.py:658: in do_draw
    result = self.pack(data.draw(self.mapped_strategy))
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:888: in draw
    return strategy.do_draw(self)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:57: in do_draw
    return tuple(data.draw(e) for e in self.element_strategies)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/collections.py:57: in <genexpr>
    return tuple(data.draw(e) for e in self.element_strategies)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:888: in draw
    return strategy.do_draw(self)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/lazy.py:150: in do_draw
    return data.draw(self.wrapped_strategy)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:888: in draw
    return strategy.do_draw(self)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/lazy.py:150: in do_draw
    return data.draw(self.wrapped_strategy)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:888: in draw
    return strategy.do_draw(self)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/core.py:1722: in do_draw
    return self.definition(data.draw, *self.args, **self.kwargs)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/extra/pandas/impl.py:584: in just_draw_columns
    value = draw(c.elements)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:888: in draw
    return strategy.do_draw(self)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/lazy.py:150: in do_draw
    return data.draw(self.wrapped_strategy)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:888: in draw
    return strategy.do_draw(self)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/datetime.py:131: in do_draw
    result = self.draw_naive_datetime_and_combine(data, tz)
../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/datetime.py:145: in draw_naive_datetime_and_combine
    result = draw_capped_multipart(data, self.min_value, self.max_value)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

data = ConjectureData(VALID, 1 bytes, frozen), min_value = Timestamp('2017-01-01 00:00:00'), max_value = Timestamp('2019-01-01 00:00:00')

    def draw_capped_multipart(data, min_value, max_value):
        assert isinstance(min_value, (dt.date, dt.time, dt.datetime))
        assert type(min_value) == type(max_value)
        assert min_value <= max_value
        result = {}
        cap_low, cap_high = True, True
        duration_names_by_type = {
            dt.date: DATENAMES,
            dt.time: TIMENAMES,
            dt.datetime: DATENAMES + TIMENAMES,
        }
>       for name in duration_names_by_type[type(min_value)]:
E       KeyError: <class 'pandas._libs.tslibs.timestamps.Timestamp'>

../../../../.pyenv/versions/3.7.5/envs/TOOLS/lib/python3.7/site-packages/hypothesis/strategies/_internal/datetime.py:92: KeyError
========================================================================================================== short test summary info ===========================================================================================================
FAILED hypothesis_bug.py::test_hypothesis_testing - KeyError: <class 'pandas._libs.tslibs.timestamps.Timestamp'>
```
</p>
</details>


Discussion:
```python
import pandas as pd
from hypothesis import given, strategies as st

@given(st.datetimes(min_value=pd.Timestamp(2017, 1, 1)))
def test_hypothesis_testing(dt):
    pass
```

Looks like we didn't have any tests which use `datetime` subclasses for the bounds to `datetimes()`.  At least it'll be easy to fix!
Cool, thanks for the quick fix!



Patch:
diff --git a/hypothesis-python/RELEASE.rst b/hypothesis-python/RELEASE.rst
new file mode 100644
index 0000000000..078a1cc7ca
--- /dev/null
+++ b/hypothesis-python/RELEASE.rst
@@ -0,0 +1,5 @@
+RELEASE_TYPE: patch
+
+This patch fixes :issue:`2406`, where use of :obj:`pandas:pandas.Timestamp`
+objects as bounds for the :func:`~hypothesis.strategies.datetimes` strategy
+caused an internal error.  This bug was introduced in :ref:`version 5.8.1 <v5.8.2>`.
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/datetime.py b/hypothesis-python/src/hypothesis/strategies/_internal/datetime.py
index 5310641cb9..6826813d91 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/datetime.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/datetime.py
@@ -78,18 +78,15 @@ def datetime_does_not_exist(value):
     return value != roundtrip
 
 
-def draw_capped_multipart(data, min_value, max_value):
+def draw_capped_multipart(
+    data, min_value, max_value, duration_names=DATENAMES + TIMENAMES
+):
     assert isinstance(min_value, (dt.date, dt.time, dt.datetime))
     assert type(min_value) == type(max_value)
     assert min_value <= max_value
     result = {}
     cap_low, cap_high = True, True
-    duration_names_by_type = {
-        dt.date: DATENAMES,
-        dt.time: TIMENAMES,
-        dt.datetime: DATENAMES + TIMENAMES,
-    }
-    for name in duration_names_by_type[type(min_value)]:
+    for name in duration_names:
         low = getattr(min_value if cap_low else dt.datetime.min, name)
         high = getattr(max_value if cap_high else dt.datetime.max, name)
         if name == "day" and not cap_high:
@@ -216,7 +213,7 @@ def __init__(self, min_value, max_value, timezones_strat):
         self.tz_strat = timezones_strat
 
     def do_draw(self, data):
-        result = draw_capped_multipart(data, self.min_value, self.max_value)
+        result = draw_capped_multipart(data, self.min_value, self.max_value, TIMENAMES)
         tz = data.draw(self.tz_strat)
         return dt.time(**result, tzinfo=tz)
 
@@ -257,7 +254,9 @@ def __init__(self, min_value, max_value):
         self.max_value = max_value
 
     def do_draw(self, data):
-        return dt.date(**draw_capped_multipart(data, self.min_value, self.max_value))
+        return dt.date(
+            **draw_capped_multipart(data, self.min_value, self.max_value, DATENAMES)
+        )
 
 
 @defines_strategy_with_reusable_values


Test patch:
diff --git a/hypothesis-python/tests/pandas/test_argument_validation.py b/hypothesis-python/tests/pandas/test_argument_validation.py
index 5b637f5f2a..65e7911c2b 100644
--- a/hypothesis-python/tests/pandas/test_argument_validation.py
+++ b/hypothesis-python/tests/pandas/test_argument_validation.py
@@ -13,8 +13,13 @@
 #
 # END HEADER
 
+from datetime import datetime
+
+import pandas as pd
+
 import hypothesis.extra.pandas as pdst
 import hypothesis.strategies as st
+from hypothesis import given
 from tests.common.arguments import argument_validation_test, e
 
 BAD_ARGS = [
@@ -69,3 +74,13 @@
 
 
 test_raise_invalid_argument = argument_validation_test(BAD_ARGS)
+
+lo, hi = pd.Timestamp(2017, 1, 1), pd.Timestamp(2084, 12, 21)
+
+
+@given(st.datetimes(min_value=lo, max_value=hi))
+def test_timestamp_as_datetime_bounds(dt):
+    # Would have caught https://github.com/HypothesisWorks/hypothesis/issues/2406
+    assert isinstance(dt, datetime)
+    assert lo <= dt <= hi
+    assert not isinstance(dt, pd.Timestamp)