Repository: HypothesisWorks/hypothesis

Issue:
Document how to make a Strategy for enums
If one tries `builds(Enum)`, it fails horribly:

    >>> from hypothesis.strategies import builds
    >>> from enum import Enum
    >>> builds(Enum("One",[chr(x) for x in range(ord('A'),ord('C'))])).example()
    Traceback:
      File "/home/janus/.pyenv/versions/3.9.0rc2/lib/python3.9/site-packages/hypothesis/strategies/_internal/core.py", line 1318, in <lambda>
        lambda value: target(*value[0], **value[1])  # type: ignore
    TypeError: __call__() missing 1 required positional argument: 'value'

It would be nice if the right way was documented, or if `builds` detected that it was being called on an enum. It is a standard library class.

`sampled_from(list(Enum))` does work, but it may not be obvious to everybody.

This was tested with version 5.41.5 on Python 3.9.0rc2.


Discussion:
[As the docs note](https://hypothesis.readthedocs.io/en/latest/data.html?highlight=Enum#hypothesis.strategies.sampled_from), `sampled_from(Enum)` works directly, and even has special handling for `Flag` enums to generate combinations of options.

I actually don't think we should change the semantics of `builds()` away from "calls the target with arguments", even when there's a clear alternative interpretation (though it's not *that* clear given that enums are actually callable!).  It's not a bad API style for interactive tools, but as a testing framework we prioritize clear and simple mental models to minimize the risk of surprising test behaviour.

However, I'd be happy to detect this particular problem and raise an error with a helpful message explaining what to do instead.
Ok! The confusion mainly stems from the fact that I can build dataclasses using `builds`, even if they contain Enums. It seemed odd to me that I would call a different function based on whether the outermost type is a sum type or a product type. I would very much appreciate a more helpful error message! Thanks.
> It seemed odd to me that I would call a different function based on whether the outermost type is a sum type or a product type.

Ah - the distinction is actually "`builds(target)` calls the target, `sampled_from(target)` returns elements from the target sequence".  Sum-type vs product-type is a nice mental model to have, but not quite applicable here.  

Knowing that's the distinction you had in mind will help me write the new error message though - thanks!

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
@@ -1312,11 +1312,28 @@ def builds(
             )
         for kw in set(hints) & (required | to_infer):
             kwargs[kw] = from_type(hints[kw])
+
+    def build_target(value):
+        args, kwargs = value
+        try:
+            return target(*args, **kwargs)
+        except TypeError as err:
+            if (
+                isinstance(target, type)
+                and issubclass(target, enum.Enum)
+                and not (args or kwargs)
+            ):
+                name = target.__module__ + "." + target.__qualname__
+                raise InvalidArgument(
+                    f"Calling {name} with no arguments raised an error - "
+                    f"try using sampled_from({name}) instead of builds({name})"
+                ) from err
+            raise
+
     # Mypy doesn't realise that `infer` is gone from kwargs now
     # and thinks that target and args have the same (union) type.
-    return tuples(tuples(*args), fixed_dictionaries(kwargs)).map(  # type: ignore
-        lambda value: target(*value[0], **value[1])  # type: ignore
-    )
+    args_kwargs = tuples(tuples(*args), fixed_dictionaries(kwargs))  # type: ignore
+    return args_kwargs.map(build_target)
 
 
 if sys.version_info[:2] >= (3, 8):  # pragma: no cover


Test patch:
diff --git a/hypothesis-python/tests/cover/test_direct_strategies.py b/hypothesis-python/tests/cover/test_direct_strategies.py
--- a/hypothesis-python/tests/cover/test_direct_strategies.py
+++ b/hypothesis-python/tests/cover/test_direct_strategies.py
@@ -15,6 +15,7 @@
 
 import collections
 import decimal
+import enum
 import fractions
 import math
 from datetime import date, datetime, time, timedelta
@@ -494,3 +495,31 @@ def test_ipaddress_from_network_is_always_in_network(data, network):
     ip = data.draw(ds.ip_addresses(network=network), label="address")
     assert ip in network
     assert ip.version == network.version
+
+
+class AnEnum(enum.Enum):
+    a = 1
+
+
+def requires_arg(value):
+    """Similar to the enum.Enum.__call__ method."""
+
+
+@given(ds.data())
+def test_builds_error_messages(data):
+    # If we call them directly, we get a simple TypeError in both cases
+    with pytest.raises(TypeError):
+        requires_arg()
+    with pytest.raises(TypeError):
+        AnEnum()
+    # But we have an improved error message if you try to build an Enum
+    assert issubclass(InvalidArgument, TypeError)  # it's a valid substitution
+    with pytest.raises(TypeError):  # which only applies to enums
+        data.draw(ds.builds(requires_arg))
+    with pytest.raises(
+        InvalidArgument,
+        match=r".* try using sampled_from\(.+\) instead of builds\(.+\)",
+    ):
+        data.draw(ds.builds(AnEnum))
+    # and sampled_from() does in fact work
+    data.draw(ds.sampled_from(AnEnum))