Repository: HypothesisWorks/hypothesis

Issue:
Add helpful error message for `st.sampled_from(EmptyEnumWithAnnotations)`
Following the discussion at #2693, the documented method of sampling from an Enum does not appear to work (or the documentation could use clarification and/or an example). The approach I tried is:

```python
from enum import Enum
from hypothesis import given, strategies as st

class MyEnum(Enum):
    a: 'a'
    b: 'b'

@given(st.sampled_from(MyEnum))
def test_example(instance):
    assert True
```

This raises `hypothesis.errors.InvalidArgument: Cannot sample from a length-zero sequence.` Additionally, `sampled_from(list(MyEnum))` raises the same issue. Hypothesis version is 6.8.5, python is 3.9.2 




Discussion:
For context, the end goal I'm looking to achieve is generating Pydantic model, with some attributes being enums: https://pydantic-docs.helpmanual.io/hypothesis_plugin/
Your problem here is that `MyEnum` *doesn't have any elements*, because you've used annotations instead of assigning them - Enums are different to dataclasses or Pydantic like that.  Replace `:` with `=`, and it'll work 🙂

This is a weird enough edge case that I didn't spot it immediately though, so I think we should upgrade `st.sampled_from()` to check `vars(MyEnum).get('__annotations__')` if `list(MyEnum) == []` and raise an error that can explain what's happening.

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
@@ -158,6 +158,17 @@ def sampled_from(elements):
     """
     values = check_sample(elements, "sampled_from")
     if not values:
+        if (
+            isinstance(elements, type)
+            and issubclass(elements, enum.Enum)
+            and vars(elements).get("__annotations__")
+        ):
+            # See https://github.com/HypothesisWorks/hypothesis/issues/2923
+            raise InvalidArgument(
+                f"Cannot sample from {elements.__module__}.{elements.__name__} "
+                "because it contains no elements.  It does however have annotations, "
+                "so maybe you tried to write an enum as if it was a dataclass?"
+            )
         raise InvalidArgument("Cannot sample from a length-zero sequence.")
     if len(values) == 1:
         return just(values[0])
@@ -385,13 +396,12 @@ def fixed_dictionaries(
     """Generates a dictionary of the same type as mapping with a fixed set of
     keys mapping to strategies. ``mapping`` must be a dict subclass.
 
-    Generated values have all keys present in mapping, with the
-    corresponding values drawn from mapping[key]. If mapping is an
-    instance of OrderedDict the keys will also be in the same order,
-    otherwise the order is arbitrary.
+    Generated values have all keys present in mapping, in iteration order,
+    with the corresponding values drawn from mapping[key].
 
     If ``optional`` is passed, the generated value *may or may not* contain each
     key from ``optional`` and a value drawn from the corresponding strategy.
+    Generated values may contain optional keys in an arbitrary order.
 
     Examples from this strategy shrink by shrinking each individual value in
     the generated dictionary, and omitting optional key-value pairs.


Test patch:
diff --git a/hypothesis-python/tests/cover/test_sampled_from.py b/hypothesis-python/tests/cover/test_sampled_from.py
--- a/hypothesis-python/tests/cover/test_sampled_from.py
+++ b/hypothesis-python/tests/cover/test_sampled_from.py
@@ -186,3 +186,12 @@ def test_mutability_2(data):
     assert data.draw(s) != 2
     x.append(2)
     assert data.draw(s) != 2
+
+
+class AnnotationsInsteadOfElements(enum.Enum):
+    a: "a"
+
+
+def test_suggests_elements_instead_of_annotations():
+    with pytest.raises(InvalidArgument, match="Cannot sample.*annotations.*dataclass"):
+        st.sampled_from(AnnotationsInsteadOfElements).example()