Repository: HypothesisWorks/hypothesis

Issue:
PEP 604 not supported - exception when writing union types X | Y
hypothesis is not recognizing the new bar-annotation for union types as a valid type on python 3.10+
resulting in an exception, see minimal reproduction example below -

```python
from typing import Optional, Union
from dataclasses import dataclass

@dataclass
class A:
    x: str | None

@dataclass
class B:
    x: Union[str, None]

@dataclass
class C:
    x: Optional[str]

>>> st.from_type(A).example()
hypothesis.errors.InvalidArgument: thing=str | None must be a type

>>> st.from_type(B).example()
B(x='\U00076342v¯\\\x8cî£\x00\x7f±b')

>>> st.from_type(C).example()
C(x='鏀\U00109e07h\x81ø\x9a\x9c\U0010a5e3\x8e=û')
```


Discussion:
Thanks for reporting this - #2979 has been open for nearly a year now, we definitely want it to work but nobody has yet volunteered to work on it.  

(If you'd be interested in contributing, I'm happy to help out!)

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
@@ -740,6 +740,10 @@ def __repr__(self):
 
 class RandomModule(SearchStrategy):
     def do_draw(self, data):
+        # It would be unsafe to do run this method more than once per test case,
+        # because cleanup() runs tasks in FIFO order (at time of writing!).
+        # Fortunately, the random_module() strategy wraps us in shared(), so
+        # it's cached for all but the first of any number of calls.
         seed = data.draw(integers(0, 2**32 - 1))
         seed_all, restore_all = get_seeder_and_restorer(seed)
         seed_all()
@@ -1021,7 +1025,7 @@ def as_strategy(strat_or_callable, thing, final=True):
                 return as_strategy(types._global_type_lookup[thing], thing)
             return from_type(thing.__supertype__)
         # Unions are not instances of `type` - but we still want to resolve them!
-        if getattr(thing, "__origin__", None) is typing.Union:
+        if types.is_a_union(thing):
             args = sorted(thing.__args__, key=types.type_sorting_key)
             return one_of([from_type(t) for t in args])
     if not types.is_a_type(thing):
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
@@ -39,6 +39,12 @@
 from hypothesis.strategies._internal.lazy import unwrap_strategies
 from hypothesis.strategies._internal.strategies import OneOfStrategy
 
+try:
+    # The type of PEP-604 unions (`int | str`), added in Python 3.10
+    from types import UnionType  # type: ignore
+except ImportError:
+    UnionType = ()
+
 try:
     import typing_extensions
 except ImportError:
@@ -170,6 +176,14 @@ def is_a_new_type(thing):
     return isinstance(thing, typing.NewType)  # pragma: no cover  # on 3.8, anyway
 
 
+def is_a_union(thing):
+    """Return True if thing is a typing.Union or types.UnionType (in py310)."""
+    return (
+        isinstance(thing, UnionType)
+        or getattr(thing, "__origin__", None) is typing.Union
+    )
+
+
 def is_a_type(thing):
     """Return True if thing is a type or a generic type like thing."""
     return isinstance(thing, type) or is_generic_type(thing) or is_a_new_type(thing)
@@ -247,9 +261,8 @@ def _try_import_forward_ref(thing, bound):  # pragma: no cover
 
 
 def from_typing_type(thing):
-    # We start with special-case support for Union and Tuple - the latter
-    # isn't actually a generic type. Then we handle Literal since it doesn't
-    # support `isinstance`.
+    # We start with special-case support for Tuple, which isn't actually a generic
+    # type; then Final, Literal, and Annotated since they don't support `isinstance`.
     #
     # We then explicitly error on non-Generic types, which don't carry enough
     # information to sensibly resolve to strategies at runtime.
@@ -332,7 +345,7 @@ def from_typing_type(thing):
         # if there is more than one allowed type, and the element type is
         # not either `int` or a Union with `int` as one of its elements.
         elem_type = (getattr(thing, "__args__", None) or ["not int"])[0]
-        if getattr(elem_type, "__origin__", None) is typing.Union:
+        if is_a_union(elem_type):
             union_elems = elem_type.__args__
         else:
             union_elems = ()
@@ -613,7 +626,7 @@ def resolve_Type(thing):
         # This branch is for Python < 3.8, when __args__ was not always tracked
         return st.just(type)  # pragma: no cover
     args = (thing.__args__[0],)
-    if getattr(args[0], "__origin__", None) is typing.Union:
+    if is_a_union(args[0]):
         args = args[0].__args__
     # Duplicate check from from_type here - only paying when needed.
     args = list(args)


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
@@ -31,6 +31,8 @@
     collect_ignore_glob.append("cover/*py38*")
 if sys.version_info < (3, 9):
     collect_ignore_glob.append("cover/*py39*")
+if sys.version_info < (3, 10):
+    collect_ignore_glob.append("cover/*py310*")
 
 if sys.version_info >= (3, 11):
     collect_ignore_glob.append("cover/test_asyncio.py")  # @asyncio.coroutine removed
diff --git a/hypothesis-python/tests/cover/test_lookup_py310.py b/hypothesis-python/tests/cover/test_lookup_py310.py
new file mode 100644
--- /dev/null
+++ b/hypothesis-python/tests/cover/test_lookup_py310.py
@@ -0,0 +1,19 @@
+# This file is part of Hypothesis, which may be found at
+# https://github.com/HypothesisWorks/hypothesis/
+#
+# Copyright the Hypothesis Authors.
+# Individual contributors are listed in AUTHORS.rst and the git log.
+#
+# 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/.
+
+from hypothesis import strategies as st
+
+from tests.common.debug import find_any
+
+
+def test_native_unions():
+    s = st.from_type(int | list[str])
+    find_any(s, lambda x: isinstance(x, int))
+    find_any(s, lambda x: isinstance(x, list))