Repository: HypothesisWorks/hypothesis

Issue:
'Unsatisfiable' error when generating a dataframe from `rows` and `columns` without a `fill` value
According to the documentation, absent values should be filled based on the column dtype, but that does not happen. I created the following minimal example of the problem. Any ideas?

```python
from hypothesis.extra.pandas import data_frames, indexes, column
from hypothesis.strategies import sampled_from, text

print(
    data_frames(
        columns=[column(c, text(), dtype=str) for c in ['a', 'b']],
        rows=sampled_from([{'a': 'x'}]),
        index=indexes(dtype=int, min_size=1)
    ).example()
)
```
The error is:

```python-traceback
  File ".../python3.8/site-packages/hypothesis/core.py", line 763, in run_engine
    raise Unsatisfiable(
hypothesis.errors.Unsatisfiable: Unable to satisfy assumptions of hypothesis example_generating_inner_function.
```


Discussion:
Hey @snthibaud - that's a great bug report :smile: 

It looks like the problem is in providing both rows and columns - [if you provide both, they are validated against each other](https://hypothesis.readthedocs.io/en/latest/numpy.html#hypothesis.extra.pandas.data_frames).  If you *just* use columns, the following example works:

```python
from hypothesis.extra.pandas import data_frames, indexes, column
from hypothesis import given, strategies as st


@given(
    data_frames(
        columns=[
            column("a", st.just("x"), dtype=str),  # note: more specific elements
            column("b", st.text(), dtype=str),
        ],
        index=indexes(dtype=int, min_size=1),
    )
)
def test(df):
    pass
```

We should try to provide a more specific error message for this case though!
Hello @Zac-HD ,

Thank you! What exactly is meant by 'validated' here?

I read this phrase in the same section:
> Any values missing from the generated rows will be provided using the column’s fill.

So I assumed that 'a' would be the specified string 'x' and column 'b' would be filled by some other text (provided by text() ).

I actually need a Dataframe with columns depending on each other (and I cannot specify all columns for all rows).
Does my bug report still stand? :innocent:
When quoting that line I noticed the word 'fill' and it turns out to be a keyword to the 'column' function!
With this subtle change, it does work as I intended:

```
from hypothesis.extra.pandas import data_frames, indexes, column
from hypothesis.strategies import sampled_from, text

print(
    data_frames(
        columns=[column(c, fill=text(), dtype=str) for c in ['a', 'b']],
        rows=sampled_from([{'a': 'x'}]),
        index=indexes(dtype=int, min_size=1)).example())
```
Thanks for your help! :smile:
Aha, that would do it!  We can definitely provide a better error message though, so I'm keeping the issue open for that :grin:

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
@@ -648,6 +648,11 @@ def assign_rows(draw):
                                 try:
                                     as_list[i] = fills[i]
                                 except KeyError:
+                                    if c.fill.is_empty:
+                                        raise InvalidArgument(
+                                            f"Empty fill strategy in {c!r} cannot "
+                                            f"complete row {original_row!r}"
+                                        ) from None
                                     fills[i] = draw(c.fill)
                                     as_list[i] = fills[i]
                         for k in row:
@@ -679,7 +684,13 @@ def assign_rows(draw):
                             % (original_row, len(row), len(rewritten_columns))
                         )
                     while len(row) < len(rewritten_columns):
-                        row.append(draw(rewritten_columns[len(row)].fill))
+                        c = rewritten_columns[len(row)]
+                        if c.fill.is_empty:
+                            raise InvalidArgument(
+                                f"Empty fill strategy in {c!r} cannot "
+                                f"complete row {original_row!r}"
+                            )
+                        row.append(draw(c.fill))
                     result.iloc[row_index] = row
                     break
                 else:


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
@@ -50,6 +50,18 @@
     e(pdst.data_frames, rows=st.integers(), index=pdst.range_indexes(0, 0)),
     e(pdst.data_frames, rows=st.integers(), index=pdst.range_indexes(1, 1)),
     e(pdst.data_frames, pdst.columns(1, dtype=int), rows=st.integers()),
+    e(
+        pdst.data_frames,
+        columns=pdst.columns(["a", "b"], dtype=str, elements=st.text()),
+        rows=st.just({"a": "x"}),
+        index=pdst.indexes(dtype=int, min_size=1),
+    ),
+    e(
+        pdst.data_frames,
+        columns=pdst.columns(["a", "b"], dtype=str, elements=st.text()),
+        rows=st.just(["x"]),
+        index=pdst.indexes(dtype=int, min_size=1),
+    ),
     e(pdst.indexes),
     e(pdst.indexes, dtype="category"),
     e(pdst.indexes, dtype="not a dtype"),