Repository: HypothesisWorks/hypothesis

Issue:
Support `builds(Optional[MyDataclass])` or document alternative
```python
>>> import dataclasses
>>> @dataclasses.dataclass
... class A:
...     b: int
```

Since `builds(A)` works, I was thinking I can just use `builds(Optional[A])`, since if `A` is a type, `Optional[A]` is also a type.

But it doesn't work:

```python-traceback
>>> hypothesis.strategies.builds(Optional[A]).example()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/janus/.pyenv/versions/3.9.1/lib/python3.9/site-packages/hypothesis/strategies/_internal/strategies.py", line 314, in example
    example_generating_inner_function()
  File "/home/janus/.pyenv/versions/3.9.1/lib/python3.9/site-packages/hypothesis/strategies/_internal/strategies.py", line 302, in example_generating_inner_function
    @settings(
  File "/home/janus/.pyenv/versions/3.9.1/lib/python3.9/site-packages/hypothesis/core.py", line 1164, in wrapped_test
    raise the_error_hypothesis_found
  File "/home/janus/.pyenv/versions/3.9.1/lib/python3.9/typing.py", line 664, in __call__
    result = self.__origin__(*args, **kwargs)
  File "/home/janus/.pyenv/versions/3.9.1/lib/python3.9/typing.py", line 329, in __call__
    raise TypeError(f"Cannot instantiate {self!r}")
TypeError: Cannot instantiate typing.Union
```

It would be nice if it worked, or this use case was documented. I searched in vain for a strategy combinator `optional(s: Strategy[A]) -> Strategy[Optional[A]]` or something like that, so that I could do `optional(builds(A))` and sometimes get a `None`, sometimes get an `A`.


Discussion:
aaah, you can use `from_type`!

```python
>>> st.from_type(A)
builds(A)
>>> st.from_type(Optional[A])
one_of(none(), builds(A))
```

It would be nice if the `builds` documentation noted that `from_type` is strictly more capable for dataclasses. What do you think?
> It would be nice if the `builds` documentation noted that `from_type` is strictly more capable for dataclasses. What do you think?

It's not though - the real distinction is that `st.builds()` *calls the thing*, while `st.from_type()` *gives you instances* (possibly by using `builds()`).  On the other hand, you can `st.builds(A, b=st.integers(min_value=0)` and this is a level of flexibility that `st.from_type()` doesn't offer (unless you use `st.builds()` and `st.register_type_strategy()` first).

I think we could improve the error message when you attempt to build a typing type though, to explain the difference and suggest `from_type()` instead.

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
@@ -776,6 +776,15 @@ def do_draw(self, data):
                     f"Calling {name} with no arguments raised an error - "
                     f"try using sampled_from({name}) instead of builds({name})"
                 ) from err
+            if not (self.args or self.kwargs):
+                from .types import is_a_new_type, is_generic_type
+
+                if is_a_new_type(self.target) or is_generic_type(self.target):
+                    raise InvalidArgument(
+                        f"Calling {self.target!r} with no arguments raised an "
+                        f"error - try using from_type({self.target!r}) instead "
+                        f"of builds({self.target!r})"
+                    ) from err
             raise
 
     def validate(self):


Test patch:
diff --git a/hypothesis-python/tests/cover/test_lookup.py b/hypothesis-python/tests/cover/test_lookup.py
--- a/hypothesis-python/tests/cover/test_lookup.py
+++ b/hypothesis-python/tests/cover/test_lookup.py
@@ -846,3 +846,16 @@ def test_resolves_type_of_builtin_types(t):
 @given(st.from_type(typing.Type[typing.Union["str", "int"]]))
 def test_resolves_type_of_union_of_forwardrefs_to_builtins(x):
     assert x in (str, int)
+
+
+@pytest.mark.parametrize("type_", [typing.List[int], typing.Optional[int]])
+def test_builds_suggests_from_type(type_):
+    with pytest.raises(
+        InvalidArgument, match=re.escape(f"try using from_type({type_!r})")
+    ):
+        st.builds(type_).example()
+    try:
+        st.builds(type_, st.just("has an argument")).example()
+        raise AssertionError("Expected strategy to raise an error")
+    except TypeError as err:
+        assert not isinstance(err, InvalidArgument)