Repository: HypothesisWorks/hypothesis

Issue:
Generation doesn't work with `Annotated` types
## Versions
```
python==3.7.7
hypothesis==6.8.3
typing-extensions==3.7.4.3
```

Also tested with `python==3.9.0` and using `typing.Annotated` instead of `typing_extensions.Annotated`.

## Problem description

`hypothesis` is unable to generate data for a type if `Annotated` is added. Example:

```python
import hypothesis.strategies as st
from hypothesis import given
from typing_extensions import Annotated

# @given(st.from_type(int))                  # <-- this works
@given(st.from_type(Annotated[int, "foo"]))  # <-- this doesn't
def test(s):
    print(s)

test()
```

The error that I receive is as follows:

```
hypothesis.errors.ResolutionFailed: Could not resolve typing.Annotated[int, 'foo'] to a strategy; consider using register_type_strategy
```

I tried registering a strategy as the error message suggests like so:

```python
def my_strategy(t):
    raise Exception("Should enter here")

st.register_type_strategy(Annotated, my_strategy)

# ... rest of the example from above
```

... but the `my_strategy` function is never called, and the example fails with the same error.

## Potential solution

I followed the traceback to the function that failed and discovered this piece of code:

https://github.com/HypothesisWorks/hypothesis/blob/ef0c17481ba049329471fb6c503ad82579de4900/hypothesis-python/src/hypothesis/strategies/_internal/types.py#L207-L213

As far as I've tested, `Annotated[int, "foo"].__origin__` returns `int`, but `len(Annotated[int, "foo"].__args__)` is `1`. I guess adding an extra condition here to check for the case of `Annotated` would fix the issue, but I'm not familiar with the codebase, so correct me if I'm wrong.

I found no workarounds for the issue, so any suggestions are highly appreciated!



Discussion:
I think adding a clause for `Annotated` here would indeed fix your problem.

However, we might want to have a separate clause like `if is_annotated_type(thing):` just after `is_typing_literal()`, because we have both `typing` and `typing_extensions`, plus complications like `type(typing_extensions.Annotated[int, "label"]) == typing_extensions._AnnotatedAlias` which can probably vary with Python version.

(this would also make it easier for a future PR to support e.g. `Annotated[float, lambda x: 0 <= x < 1]`, and resolve this to `st.floats().filter(lambda x: 0 <= x < 1)` with a plan (#2701) for `st.floats(0, 1, exclude_max=True)`... at least if we can convince people that this is the natural interpretation of callable predicates for annotated types)

----------

Are you interested in writing a PR for this one?
Sure, I can take a stab at it. Could you just clarify this part?

> we might want to have a separate clause like if `is_typing_literal(thing):` just after `is_typing_literal()`

I mean did you intentionally leave the name `is_typing_literal` in both places here? 😁
Ahem, that's `is_annotated_type()` 😅

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/types.py b/hypothesis-python/src/hypothesis/strategies/_internal/types.py
--- a/hypothesis-python/src/hypothesis/strategies/_internal/types.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/types.py
@@ -59,6 +59,21 @@
 except ImportError:
     _GenericAlias = ()
 
+try:
+    from typing import _AnnotatedAlias  # type: ignore
+except ImportError:
+    try:
+        from typing_extensions import _AnnotatedAlias
+    except ImportError:
+        try:
+            from typing_extensions import (  # type: ignore
+                AnnotatedMeta as _AnnotatedAlias,
+            )
+
+            assert sys.version_info[:2] == (3, 6)
+        except ImportError:
+            _AnnotatedAlias = ()
+
 
 def type_sorting_key(t):
     """Minimise to None, then non-container types, then container types."""
@@ -110,6 +125,13 @@ def is_typing_literal(thing):
     )
 
 
+def is_annotated_type(thing):
+    return (
+        isinstance(thing, _AnnotatedAlias)
+        and getattr(thing, "__args__", None) is not None
+    )
+
+
 def has_type_arguments(type_):
     """Decides whethere or not this type has applied type arguments."""
     args = getattr(type_, "__args__", None)
@@ -195,6 +217,12 @@ def from_typing_type(thing):
             else:
                 literals.append(arg)
         return st.sampled_from(literals)
+    if is_annotated_type(thing):  # pragma: no cover
+        # This requires Python 3.9+ or the typing_extensions package
+        args = thing.__args__
+        assert args, "it's impossible to make an annotated type with no args"
+        annotated_type = args[0]
+        return st.from_type(annotated_type)
     # Now, confirm that we're dealing with a generic type as we expected
     if sys.version_info[:2] < (3, 9) and not isinstance(
         thing, typing_root_type


Test patch:
diff --git a/hypothesis-python/tests/conftest.py b/hypothesis-python/tests/conftest.py
--- a/hypothesis-python/tests/conftest.py
+++ b/hypothesis-python/tests/conftest.py
@@ -34,6 +34,8 @@
     collect_ignore_glob.append("cover/*py37*")
 if sys.version_info < (3, 8):
     collect_ignore_glob.append("cover/*py38*")
+if sys.version_info < (3, 9):
+    collect_ignore_glob.append("cover/*py39*")
 
 
 def pytest_configure(config):
diff --git a/hypothesis-python/tests/cover/test_lookup_py39.py b/hypothesis-python/tests/cover/test_lookup_py39.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/tests/cover/test_lookup_py39.py
@@ -0,0 +1,38 @@
+# This file is part of Hypothesis, which may be found at
+# https://github.com/HypothesisWorks/hypothesis/
+#
+# Most of this work is copyright (C) 2013-2021 David R. MacIver
+# (david@drmaciver.com), but it contains contributions by others. See
+# CONTRIBUTING.rst for a full list of people who may hold copyright, and
+# consult the git log if you need to determine who owns an individual
+# contribution.
+#
+# This Source Code Form is subject to the terms of the Mozilla Public License,
+# v. 2.0. If a copy of the MPL was not distributed with this file, You can
+# obtain one at https://mozilla.org/MPL/2.0/.
+#
+# END HEADER
+
+import typing
+
+import pytest
+
+from hypothesis import strategies as st
+
+
+@pytest.mark.parametrize(
+    "annotated_type,expected_strategy_repr",
+    [
+        (typing.Annotated[int, "foo"], "integers()"),
+        (typing.Annotated[typing.List[float], "foo"], "lists(floats())"),
+        (typing.Annotated[typing.Annotated[str, "foo"], "bar"], "text()"),
+        (
+            typing.Annotated[
+                typing.Annotated[typing.List[typing.Dict[str, bool]], "foo"], "bar"
+            ],
+            "lists(dictionaries(keys=text(), values=booleans()))",
+        ),
+    ],
+)
+def test_typing_Annotated(annotated_type, expected_strategy_repr):
+    assert repr(st.from_type(annotated_type)) == expected_strategy_repr
diff --git a/hypothesis-python/tests/typing_extensions/test_backported_types.py b/hypothesis-python/tests/typing_extensions/test_backported_types.py
--- a/hypothesis-python/tests/typing_extensions/test_backported_types.py
+++ b/hypothesis-python/tests/typing_extensions/test_backported_types.py
@@ -14,10 +14,10 @@
 # END HEADER
 
 import collections
-from typing import Union
+from typing import Dict, List, Union
 
 import pytest
-from typing_extensions import DefaultDict, Literal, NewType, Type, TypedDict
+from typing_extensions import Annotated, DefaultDict, Literal, NewType, Type, TypedDict
 
 from hypothesis import assume, given, strategies as st
 from hypothesis.strategies import from_type
@@ -81,3 +81,19 @@ def test_defaultdict(ex):
     assume(ex)
     assert all(isinstance(elem, int) for elem in ex)
     assert all(isinstance(elem, int) for elem in ex.values())
+
+
+@pytest.mark.parametrize(
+    "annotated_type,expected_strategy_repr",
+    [
+        (Annotated[int, "foo"], "integers()"),
+        (Annotated[List[float], "foo"], "lists(floats())"),
+        (Annotated[Annotated[str, "foo"], "bar"], "text()"),
+        (
+            Annotated[Annotated[List[Dict[str, bool]], "foo"], "bar"],
+            "lists(dictionaries(keys=text(), values=booleans()))",
+        ),
+    ],
+)
+def test_typing_extensions_Annotated(annotated_type, expected_strategy_repr):
+    assert repr(st.from_type(annotated_type)) == expected_strategy_repr