Repository: HypothesisWorks/hypothesis

Issue:
Error generating dataframes with a column of sets
First off, love love love hypothesis! I know storing non-scalars in a dataframe isn't exactly the intended use case, it's not something we're not well-placed to fix right this moment. I also understand that probably means this isn't high priority.

Environment:
```
attrs            21.2.0 Classes Without Boilerplate
hypothesis       6.24.1 A library for property-based testing
more-itertools   8.10.0 More routines for operating on iterables, beyond it...
numpy            1.21.3 NumPy is the fundamental package for array computin...
packaging        21.2   Core utilities for Python packages
pandas           1.3.4  Powerful data structures for data analysis, time se...
pluggy           0.13.1 plugin and hook calling mechanisms for python
py               1.10.0 library with cross-python path, ini-parsing, io, co...
pyparsing        2.4.7  Python parsing module
pytest           5.4.3  pytest: simple powerful testing with Python
python-dateutil  2.8.2  Extensions to the standard Python datetime module
pytz             2021.3 World timezone definitions, modern and historical
six              1.16.0 Python 2 and 3 compatibility utilities
sortedcontainers 2.4.0  Sorted Containers -- Sorted List, Sorted Dict, Sort...
wcwidth          0.2.5  Measures the displayed width of unicode strings in ...
```

Code:
```python
from hypothesis import given, strategies as st
from hypothesis.extra.pandas import data_frames, column


@given(data_frames(columns=[column(elements=st.sets(st.text(), min_size=1))]))
def test_dataframe_with_set(df):
    pass
```

[output.txt](https://github.com/HypothesisWorks/hypothesis/files/7465029/output.txt):
```
============================= test session starts ==============================
platform linux -- Python 3.8.11, pytest-5.4.3, py-1.10.0, pluggy-0.13.1
rootdir: /home/andrea/omnistream/bugs/hypothesis-dataframes-sets
plugins: hypothesis-6.24.1
collected 1 item

repro.py F                                                               [100%]

=================================== FAILURES ===================================
___________________________ test_dataframe_with_set ____________________________

self = 0    0.0
dtype: float64, key = 0, value = {'', '0'}

    def __setitem__(self, key, value) -> None:
        key = com.apply_if_callable(key, self)
        cacher_needs_updating = self._check_is_chained_assignment_possible()
    
        if key is Ellipsis:
            key = slice(None)
    
        try:
>           self._set_with_engine(key, value)

.venv/lib/python3.8/site-packages/pandas/core/series.py:1062: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = 0    0.0
dtype: float64, key = 0, value = {'', '0'}

    def _set_with_engine(self, key, value) -> None:
        # fails with AttributeError for IntervalIndex
        loc = self.index._engine.get_loc(key)
        # error: Argument 1 to "validate_numeric_casting" has incompatible type
        # "Union[dtype, ExtensionDtype]"; expected "dtype"
        validate_numeric_casting(self.dtype, value)  # type: ignore[arg-type]
>       self._values[loc] = value
E       TypeError: float() argument must be a string or a number, not 'set'

.venv/lib/python3.8/site-packages/pandas/core/series.py:1099: TypeError

During handling of the above exception, another exception occurred:

    @given(data_frames(columns=[column(elements=st.sets(st.text(), min_size=1))]))
>   def test_dataframe_with_set(df):

repro.py:6: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
.venv/lib/python3.8/site-packages/pandas/core/series.py:1088: in __setitem__
    self._set_with(key, value)
.venv/lib/python3.8/site-packages/pandas/core/series.py:1123: in _set_with
    self._set_labels(key, value)
.venv/lib/python3.8/site-packages/pandas/core/series.py:1135: in _set_labels
    self._set_values(indexer, value)
.venv/lib/python3.8/site-packages/pandas/core/series.py:1141: in _set_values
    self._mgr = self._mgr.setitem(indexer=key, value=value)
.venv/lib/python3.8/site-packages/pandas/core/internals/managers.py:355: in setitem
    return self.apply("setitem", indexer=indexer, value=value)
.venv/lib/python3.8/site-packages/pandas/core/internals/managers.py:327: in apply
    applied = getattr(b, f)(**kwargs)
.venv/lib/python3.8/site-packages/pandas/core/internals/blocks.py:927: in setitem
    return self.coerce_to_target_dtype(value).setitem(indexer, value)
.venv/lib/python3.8/site-packages/pandas/core/internals/blocks.py:943: in setitem
    check_setitem_lengths(indexer, value, values)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

indexer = array([0]), value = {'', '0'}, values = array([0.0], dtype=object)

    def check_setitem_lengths(indexer, value, values) -> bool:
        """
        Validate that value and indexer are the same length.
    
        An special-case is allowed for when the indexer is a boolean array
        and the number of true values equals the length of ``value``. In
        this case, no exception is raised.
    
        Parameters
        ----------
        indexer : sequence
            Key for the setitem.
        value : array-like
            Value for the setitem.
        values : array-like
            Values being set into.
    
        Returns
        -------
        bool
            Whether this is an empty listlike setting which is a no-op.
    
        Raises
        ------
        ValueError
            When the indexer is an ndarray or list and the lengths don't match.
        """
        no_op = False
    
        if isinstance(indexer, (np.ndarray, list)):
            # We can ignore other listlikes because they are either
            #  a) not necessarily 1-D indexers, e.g. tuple
            #  b) boolean indexers e.g. BoolArray
            if is_list_like(value):
                if len(indexer) != len(value) and values.ndim == 1:
                    # boolean with truth values == len of the value is ok too
                    if isinstance(indexer, list):
                        indexer = np.array(indexer)
                    if not (
                        isinstance(indexer, np.ndarray)
                        and indexer.dtype == np.bool_
                        and len(indexer[indexer]) == len(value)
                    ):
>                       raise ValueError(
                            "cannot set using a list-like indexer "
                            "with a different length than the value"
                        )
E                       ValueError: cannot set using a list-like indexer with a different length than the value

.venv/lib/python3.8/site-packages/pandas/core/indexers.py:176: ValueError
=========================== short test summary info ============================
FAILED repro.py::test_dataframe_with_set - ValueError: cannot set using a lis...
============================== 1 failed in 0.52s ===============================
```



hypothesis.extra.pandas.column, errors out when elements = lists and unique = True
When defining a pandas.columns with lists 

```python
from hypothesis.extra.pandas import column
from hypothesis.strategies import lists

column(name='group_array', elements=lists(integers(min_value=-2147483648, max_value=2147483647), max_size=5, min_size=5, unique=True), dtype=list, fill=None, unique=True)
```

We got this error 

```python-traceback
...
/usr/local/lib/python3.8/dist-packages/hypothesis/internal/conjecture/data.py:884: in draw
    return strategy.do_draw(self)
/usr/local/lib/python3.8/dist-packages/hypothesis/strategies/_internal/lazy.py:168: in do_draw
    return data.draw(self.wrapped_strategy)
/usr/local/lib/python3.8/dist-packages/hypothesis/internal/conjecture/data.py:884: in draw
    return strategy.do_draw(self)
/usr/local/lib/python3.8/dist-packages/hypothesis/strategies/_internal/lazy.py:168: in do_draw
    return data.draw(self.wrapped_strategy)
/usr/local/lib/python3.8/dist-packages/hypothesis/internal/conjecture/data.py:884: in draw
    return strategy.do_draw(self)
/usr/local/lib/python3.8/dist-packages/hypothesis/strategies/_internal/core.py:1393: in do_draw
    return self.definition(data.draw, *self.args, **self.kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

draw = <bound method ConjectureData.draw of ConjectureData(VALID, 1961 bytes, frozen)>

    @st.composite
    def just_draw_columns(draw):
        index = draw(index_strategy)
        local_index_strategy = st.just(index)
    
        data = OrderedDict((c.name, None) for c in rewritten_columns)
    
        # Depending on how the columns are going to be generated we group
        # them differently to get better shrinking. For columns with fill
        # enabled, the elements can be shrunk independently of the size,
        # so we can just shrink by shrinking the index then shrinking the
        # length and are generally much more free to move data around.
    
        # For columns with no filling the problem is harder, and drawing
        # them like that would result in rows being very far apart from
        # each other in the underlying data stream, which gets in the way
        # of shrinking. So what we do is reorder and draw those columns
        # row wise, so that the values of each row are next to each other.
        # This makes life easier for the shrinker when deleting blocks of
        # data.
        columns_without_fill = [c for c in rewritten_columns if c.fill.is_empty]
    
        if columns_without_fill:
            for c in columns_without_fill:
                data[c.name] = pandas.Series(
                    np.zeros(shape=len(index), dtype=c.dtype), index=index
                )
            seen = {c.name: set() for c in columns_without_fill if c.unique}
    
            for i in range(len(index)):
                for c in columns_without_fill:
                    if c.unique:
                        for _ in range(5):
                            value = draw(c.elements)
>                           if value not in seen[c.name]:
E                           TypeError: unhashable type: 'numpy.ndarray'

/usr/local/lib/python3.8/dist-packages/hypothesis/extra/pandas/impl.py:578: TypeError
```

But it works fine, when we set column `unique=False`: 
`column(name='group_array', elements=lists(integers(min_value=-2147483648, max_value=2147483647), max_size=5, min_size=5, unique=True), dtype=list, fill=None, unique=False)`




Discussion:
Addendum: `st.lists(st.text(), min_size=1)` works, `st.lists(st.text(), min_size=1).map(set)` doesn't.
I'm glad you like Hypothesis, and thanks for an excellent reproducible bug report 🥰 

This looks like a real bug to me, and a use-case that I'd like to support if that's practical - even if dataframes-of-sets are pretty unusual, Pandas is happy to represent them and so Hypothesis ought to generate them.

As you note though, without any resources to do so this is going to be a low-priority issue - I'm happy to help external contributors, but over the next few months I'm more likely to focus my OSS time on filter rewrites, the 3.6 EOL, and better support for external randomness.
Have no idea myself sorry, here's just a reproducer for folks.
```python
>>> from hypothesis import strategies as st
>>> from hypothesis.extra import pandas as pdst
>>> col1 = pdst.column(elements=st.lists(st.integers()), dtype=list, unique=False)
>>> pdst.data_frames([col1]).example()
    0
0  []
>>> col2 = pdst.column(elements=st.lists(st.integers()), dtype=list, unique=True)
>>> pdst.data_frames([col2]).example()
TypeError: unhashable type: 'numpy.ndarray'
```
Well, the problem is just that we track a `dict[str, set]` of seen items for each column name, but if the generated element is not hashable (and indeed `ndarray` is not) then we get a type error.

You get the same error from `st.lists(st.lists(st.none()), unique=True).example()`, so I think this is a docs issue rather than a bug.
I had labeled this a bug because I think that `pdst.column(elements=st.lists(st.integers()), dtype=list, unique=True)` should immediately raise since it is not supported.

Even if the user has insight into the fact that we require "hashability" in order to draw unique elements, and used `pdst.column(elements=st.lists(st.integers()).map(tuple), dtype=tuple, unique=True)`  instead, they would still hit the same error because `pdst.data_frames` will convert the sequence to an `ndarray` under the hood, which of course is not hashable.

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
@@ -10,6 +10,7 @@
 
 from collections import OrderedDict, abc
 from copy import copy
+from datetime import datetime, timedelta
 from typing import Any, List, Optional, Sequence, Set, Union
 
 import attr
@@ -17,6 +18,7 @@
 import pandas
 
 from hypothesis import strategies as st
+from hypothesis._settings import note_deprecation
 from hypothesis.control import reject
 from hypothesis.errors import InvalidArgument
 from hypothesis.extra import numpy as npst
@@ -78,6 +80,14 @@ def elements_and_dtype(elements, dtype, source=None):
                 f"{prefix}dtype is categorical, which is currently unsupported"
             )
 
+    if isinstance(dtype, type) and np.dtype(dtype).kind == "O" and dtype is not object:
+        note_deprecation(
+            f"Passed dtype={dtype!r} 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.",
+            since="RELEASEDAY",
+            has_codemod=False,
+        )
+
     dtype = try_convert(np.dtype, dtype, "dtype")
 
     if elements is None:
@@ -577,7 +587,19 @@ def just_draw_columns(draw):
                                 reject()
                         else:
                             value = draw(c.elements)
-                        data[c.name][i] = value
+                        try:
+                            data[c.name][i] = value
+                        except ValueError as err:
+                            if c.dtype is None and not isinstance(
+                                value, (float, int, str, bool, datetime, timedelta)
+                            ):
+                                raise ValueError(
+                                    f"Failed to add value={value!r} to column "
+                                    f"{c.name} with dtype=None.  Maybe passing "
+                                    "dtype=object would help?"
+                                ) from err
+                            # Unclear how this could happen, but users find a way...
+                            raise  # pragma: no cover
 
             for c in rewritten_columns:
                 if not c.fill.is_empty:


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
@@ -16,6 +16,7 @@
 from hypothesis.extra import pandas as pdst
 
 from tests.common.arguments import argument_validation_test, e
+from tests.common.utils import checks_deprecated_behaviour
 
 BAD_ARGS = [
     e(pdst.data_frames),
@@ -91,3 +92,8 @@ def test_timestamp_as_datetime_bounds(dt):
     assert isinstance(dt, datetime)
     assert lo <= dt <= hi
     assert not isinstance(dt, pd.Timestamp)
+
+
+@checks_deprecated_behaviour
+def test_confusing_object_dtype_aliases():
+    pdst.series(elements=st.tuples(st.integers()), dtype=tuple).example()
diff --git a/hypothesis-python/tests/pandas/test_data_frame.py b/hypothesis-python/tests/pandas/test_data_frame.py
--- a/hypothesis-python/tests/pandas/test_data_frame.py
+++ b/hypothesis-python/tests/pandas/test_data_frame.py
@@ -9,6 +9,7 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 import numpy as np
+import pytest
 
 from hypothesis import HealthCheck, given, reject, settings, strategies as st
 from hypothesis.extra import numpy as npst, pandas as pdst
@@ -238,3 +239,20 @@ def test_will_fill_missing_columns_in_tuple_row(df):
 )
 def test_can_generate_unique_columns(df):
     assert set(df[0]) == set(range(10))
+
+
+@pytest.mark.parametrize("dtype", [None, object])
+def test_expected_failure_from_omitted_object_dtype(dtype):
+    # See https://github.com/HypothesisWorks/hypothesis/issues/3133
+    col = pdst.column(elements=st.sets(st.text(), min_size=1), dtype=dtype)
+
+    @given(pdst.data_frames(columns=[col]))
+    def works_with_object_dtype(df):
+        pass
+
+    if dtype is object:
+        works_with_object_dtype()
+    else:
+        assert dtype is None
+        with pytest.raises(ValueError, match="Maybe passing dtype=object would help"):
+            works_with_object_dtype()