Repository: HypothesisWorks/hypothesis

Issue:
Add Python 3.12 beta to our CI pipeline
See https://discuss.python.org/t/python-3-12-beta-1-and-feature-freeze-is-here/

From reading the changelog I expect we'll hit some deprecations, but it doesn't look like there are any bigger changes that we'll need to work around.  The only new feature to test would be [PEP-695 syntax for parametrizable types](https://docs.python.org/dev/whatsnew/3.12.html#whatsnew312-pep695), but even that hopefully looks the same to runtime introspection...


Discussion:

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
@@ -1123,6 +1123,11 @@ def from_type_guarded(thing):
         if types.is_a_union(thing):
             args = sorted(thing.__args__, key=types.type_sorting_key)
             return one_of([_from_type(t, recurse_guard) for t in args])
+    # We also have a special case for TypeVars.
+    # They are represented as instances like `~T` when they come here.
+    # We need to work with their type instead.
+    if isinstance(thing, TypeVar) and type(thing) in types._global_type_lookup:
+        return as_strategy(types._global_type_lookup[type(thing)], thing)
     if not types.is_a_type(thing):
         if isinstance(thing, str):
             # See https://github.com/HypothesisWorks/hypothesis/issues/3016
@@ -1190,11 +1195,6 @@ def from_type_guarded(thing):
             mapping={k: v for k, v in anns.items() if k not in optional},
             optional={k: v for k, v in anns.items() if k in optional},
         )
-    # We also have a special case for TypeVars.
-    # They are represented as instances like `~T` when they come here.
-    # We need to work with their type instead.
-    if isinstance(thing, TypeVar) and type(thing) in types._global_type_lookup:
-        return as_strategy(types._global_type_lookup[type(thing)], thing)
 
     # If there's no explicitly registered strategy, maybe a subtype of thing
     # is registered - if so, we can resolve it to the subclass strategy.
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
@@ -429,7 +429,7 @@ def from_typing_type(thing):
         # Except for sequences of integers, or unions which include integer!
         # See https://github.com/HypothesisWorks/hypothesis/issues/2257
         #
-        # This block drops ByteString from the types that can be generated
+        # This block drops bytes from the types that can be generated
         # 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]
@@ -450,7 +450,7 @@ def from_typing_type(thing):
         and thing.__forward_arg__ in vars(builtins)
     ):
         return st.from_type(getattr(builtins, thing.__forward_arg__))
-    # Before Python 3.9, we sometimes have e.g. ByteString from both the typing
+    # Before Python 3.9, we sometimes have e.g. Sequence from both the typing
     # module, and collections.abc module.  Discard any type which is not it's own
     # origin, where the origin is also in the mapping.
     for t in sorted(mapping, key=type_sorting_key):


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
@@ -46,7 +46,9 @@
         t
         for t in types._global_type_lookup
         # We ignore TypeVar, because it is not a Generic type:
-        if isinstance(t, types.typing_root_type) and t != typing.TypeVar
+        if isinstance(t, types.typing_root_type)
+        and t != typing.TypeVar
+        and (sys.version_info[:2] <= (3, 11) or t != typing.ByteString)
     ),
     key=str,
 )
@@ -110,7 +112,11 @@ def test_typing_Type_Union(ex):
 @given(data=st.data())
 def test_rare_types(data, typ):
     ex = data.draw(from_type(typ))
-    assert isinstance(ex, typ)
+    with warnings.catch_warnings():
+        if sys.version_info[:2] >= (3, 12):
+            warnings.simplefilter("ignore", DeprecationWarning)
+        # ByteString is deprecated in Python 3.12
+        assert isinstance(ex, typ)
 
 
 class Elem:
@@ -752,7 +758,7 @@ def test_inference_on_generic_collections_abc_aliases(typ, data):
 def test_bytestring_not_treated_as_generic_sequence(val):
     # Check that we don't fall into the specific problem from
     # https://github.com/HypothesisWorks/hypothesis/issues/2257
-    assert not isinstance(val, typing.ByteString)
+    assert not isinstance(val, bytes)
     # Check it hasn't happened again from some other non-generic sequence type.
     for x in val:
         assert isinstance(x, set)
@@ -764,7 +770,7 @@ def test_bytestring_not_treated_as_generic_sequence(val):
 def test_bytestring_is_valid_sequence_of_int_and_parent_classes(type_):
     find_any(
         st.from_type(typing.Sequence[type_]),
-        lambda val: isinstance(val, typing.ByteString),
+        lambda val: isinstance(val, bytes),
     )
 
 
diff --git a/hypothesis-python/tests/cover/test_type_lookup.py b/hypothesis-python/tests/cover/test_type_lookup.py
--- a/hypothesis-python/tests/cover/test_type_lookup.py
+++ b/hypothesis-python/tests/cover/test_type_lookup.py
@@ -211,7 +211,11 @@ def test_uninspectable_from_type():
 
 def _check_instances(t):
     # See https://github.com/samuelcolvin/pydantic/discussions/2508
-    return t.__module__ != "typing" and not t.__module__.startswith("pydantic")
+    return (
+        t.__module__ != "typing"
+        and t.__name__ != "ByteString"
+        and not t.__module__.startswith("pydantic")
+    )
 
 
 @pytest.mark.parametrize(
diff --git a/hypothesis-python/tests/cover/test_type_lookup_forward_ref.py b/hypothesis-python/tests/cover/test_type_lookup_forward_ref.py
--- a/hypothesis-python/tests/cover/test_type_lookup_forward_ref.py
+++ b/hypothesis-python/tests/cover/test_type_lookup_forward_ref.py
@@ -134,7 +134,7 @@ def test_bound_type_cheking_only_forward_ref():
         st.builds(typechecking_only_fun).example()
 
 
-def test_bound_type_cheking_only_forward_ref_wrong_type():
+def test_bound_type_checking_only_forward_ref_wrong_type():
     """We should check ``ForwardRef`` parameter name correctly."""
     with utils.temp_registered(ForwardRef("WrongType"), st.just(1)):
         with pytest.raises(ResolutionFailed):
diff --git a/hypothesis-python/tests/datetime/test_dateutil_timezones.py b/hypothesis-python/tests/datetime/test_dateutil_timezones.py
--- a/hypothesis-python/tests/datetime/test_dateutil_timezones.py
+++ b/hypothesis-python/tests/datetime/test_dateutil_timezones.py
@@ -9,19 +9,27 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 import datetime as dt
+import sys
+import warnings
 
 import pytest
-from dateutil import tz, zoneinfo
 
 from hypothesis import assume, given
 from hypothesis.errors import FailedHealthCheck, InvalidArgument
-from hypothesis.extra.dateutil import timezones
 from hypothesis.strategies import data, datetimes, just, sampled_from, times
 from hypothesis.strategies._internal.datetime import datetime_does_not_exist
 
 from tests.common.debug import assert_all_examples, find_any, minimal
 from tests.common.utils import fails_with
 
+with warnings.catch_warnings():
+    if sys.version_info[:2] >= (3, 12):
+        # Prior to https://github.com/dateutil/dateutil/pull/1285/
+        warnings.simplefilter("ignore", DeprecationWarning)
+    from dateutil import tz, zoneinfo
+
+from hypothesis.extra.dateutil import timezones
+
 
 def test_utc_is_minimal():
     assert tz.UTC is minimal(timezones())
diff --git a/hypothesis-python/tests/datetime/test_pytz_timezones.py b/hypothesis-python/tests/datetime/test_pytz_timezones.py
--- a/hypothesis-python/tests/datetime/test_pytz_timezones.py
+++ b/hypothesis-python/tests/datetime/test_pytz_timezones.py
@@ -9,14 +9,13 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 import datetime as dt
+import sys
+import warnings
 
 import pytest
-import pytz
-from dateutil.tz import datetime_exists
 
 from hypothesis import assume, given
 from hypothesis.errors import InvalidArgument
-from hypothesis.extra.pytz import timezones
 from hypothesis.strategies import data, datetimes, just, sampled_from, times
 from hypothesis.strategies._internal.datetime import datetime_does_not_exist
 
@@ -27,6 +26,16 @@
     minimal,
 )
 
+with warnings.catch_warnings():
+    if sys.version_info[:2] >= (3, 12):
+        # See https://github.com/stub42/pytz/issues/105 and
+        # https://github.com/dateutil/dateutil/pull/1285/
+        warnings.simplefilter("ignore", DeprecationWarning)
+    import pytz
+    from dateutil.tz import datetime_exists
+
+from hypothesis.extra.pytz import timezones
+
 
 def test_utc_is_minimal():
     assert pytz.UTC is minimal(timezones())
diff --git a/hypothesis-python/tests/nocover/test_from_type_recipe.py b/hypothesis-python/tests/nocover/test_from_type_recipe.py
--- a/hypothesis-python/tests/nocover/test_from_type_recipe.py
+++ b/hypothesis-python/tests/nocover/test_from_type_recipe.py
@@ -11,7 +11,14 @@
 from hypothesis import given, strategies as st
 from hypothesis.strategies._internal.types import _global_type_lookup
 
-TYPES = sorted((x for x in _global_type_lookup if x.__module__ != "typing"), key=str)
+TYPES = sorted(
+    (
+        x
+        for x in _global_type_lookup
+        if x.__module__ != "typing" and x.__name__ != "ByteString"
+    ),
+    key=str,
+)
 
 
 def everything_except(excluded_types):