Repository: HypothesisWorks/hypothesis (2022-12-02 ~ 2023-03-15)

problem_statement: Hypothesis fails with ForwardRefs and recursive pydantic models
I would like to use hypothesis to generate instances of pydantic models, but it looks like it cannot handle recursive models with ForwardRefs.

(Quite) minimal example triggering the problem:

```python
from pydantic import BaseModel
from typing import Optional, Union
from hypothesis import given, strategies as st

class Dummy(BaseModel):
    x: Optional[Union[int, Dummy]]

Dummy.update_forward_refs()


@given(st.builds(Dummy))
def test_dummy(obj):
    print(obj.json(indent=2))
```

When running this with pytest, it fails with:
```
E           hypothesis.errors.ResolutionFailed: Could not resolve ForwardRef('Optional[Union[int, Dummy]]') to a strategy; consider using register_type_strategy

.../site-packages/hypothesis/strategies/_internal/types.py:442: ResolutionFailed
```

**Extra info:**
* Python 3.8
* pydantic 1.10.2
* hypothesis 6.58
* pytest 7.0.1
date: 2022-12-02
patch: diff --git a/hypothesis-python/setup.py b/hypothesis-python/setup.py
index cdca6f41d..21c44d01c 100644
--- a/hypothesis-python/setup.py
+++ b/hypothesis-python/setup.py
@@ -66,7 +66,7 @@ extras = {
     # zoneinfo is an odd one: every dependency is conditional, because they're
     # only necessary on old versions of Python or Windows systems.
     "zoneinfo": [
-        "tzdata>=2022.6 ; sys_platform == 'win32'",
+        "tzdata>=2022.7 ; sys_platform == 'win32'",
         "backports.zoneinfo>=0.2.1 ; python_version<'3.9'",
     ],
     # We only support Django versions with upstream support - see
diff --git a/hypothesis-python/src/hypothesis/internal/compat.py b/hypothesis-python/src/hypothesis/internal/compat.py
index 2d6fa1d7a..6456744de 100644
--- a/hypothesis-python/src/hypothesis/internal/compat.py
+++ b/hypothesis-python/src/hypothesis/internal/compat.py
@@ -13,6 +13,37 @@ import inspect
 import platform
 import sys
 import typing
+from typing import Any, ForwardRef, Tuple
+
+try:
+    from typing import get_args
+except ImportError:
+    # remove at Python 3.7 end-of-life
+    from collections.abc import Callable as _Callable
+
+    def get_args(
+        tp: Any,
+    ) -> Tuple[Any, ...]:  # pragma: no cover
+        """
+        Examples
+        --------
+        >>> assert get_args(int) == ()
+        >>> assert get_args(Dict[str, int]) == (str, int)
+        >>> assert get_args(Union[int, Union[T, int], str][int]) == (int, str)
+        >>> assert get_args(Union[int, Tuple[T, int]][str]) == (int, Tuple[str, int])
+        >>> assert get_args(Callable[[], T][int]) == ([], int)
+        """
+        if hasattr(tp, "__origin__") and hasattr(tp, "__args__"):
+            args = tp.__args__
+            if (
+                getattr(tp, "__origin__", None) is _Callable
+                and args
+                and args[0] is not Ellipsis
+            ):
+                args = (list(args[:-1]), args[-1])
+            return args
+        return ()
+
 
 try:
     BaseExceptionGroup = BaseExceptionGroup
@@ -71,6 +102,10 @@ def is_typed_named_tuple(cls):
     )
 
 
+def _hint_and_args(x):
+    return (x,) + get_args(x)
+
+
 def get_type_hints(thing):
     """Like the typing version, but tries harder and never errors.
 
@@ -111,10 +146,24 @@ def get_type_hints(thing):
                     and is_a_type(p.annotation)
                     and p.annotation is not p.empty
                 ):
+
+                    p_hint = p.annotation
+
+                    # Defer to `get_type_hints` if signature annotation is, or
+                    # contains, a forward reference that is otherwise resolved.
+                    if any(
+                        isinstance(sig_hint, ForwardRef)
+                        and not isinstance(hint, ForwardRef)
+                        for sig_hint, hint in zip(
+                            _hint_and_args(p.annotation),
+                            _hint_and_args(hints.get(p.name, Any)),
+                        )
+                    ):
+                        p_hint = hints[p.name]
                     if p.default is None:
-                        hints[p.name] = typing.Optional[p.annotation]
+                        hints[p.name] = typing.Optional[p_hint]
                     else:
-                        hints[p.name] = p.annotation
+                        hints[p.name] = p_hint
     except (AttributeError, TypeError, NameError):  # pragma: no cover
         pass
 
diff --git a/requirements/coverage.txt b/requirements/coverage.txt
index 92c66543f..97bd5958a 100644
--- a/requirements/coverage.txt
+++ b/requirements/coverage.txt
@@ -26,7 +26,7 @@ exceptiongroup==1.0.4 ; python_version < "3.11"
     #   pytest
 execnet==1.9.0
     # via pytest-xdist
-fakeredis==2.0.0
+fakeredis==2.1.0
     # via -r requirements/coverage.in
 iniconfig==1.1.1
     # via pytest
@@ -64,7 +64,7 @@ pytest==7.2.0
     # via
     #   -r requirements/test.in
     #   pytest-xdist
-pytest-xdist==3.0.2
+pytest-xdist==3.1.0
     # via -r requirements/test.in
 python-dateutil==2.8.2
     # via
diff --git a/requirements/test.txt b/requirements/test.txt
index ae76d31ab..38e3e5f60 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -30,7 +30,7 @@ pytest==7.2.0
     # via
     #   -r requirements/test.in
     #   pytest-xdist
-pytest-xdist==3.0.2
+pytest-xdist==3.1.0
     # via -r requirements/test.in
 sortedcontainers==2.4.0
     # via hypothesis (hypothesis-python/setup.py)
diff --git a/requirements/tools.txt b/requirements/tools.txt
index 220302e4e..f7d8554e9 100644
--- a/requirements/tools.txt
+++ b/requirements/tools.txt
@@ -10,7 +10,7 @@ asgiref==3.5.2
     # via django
 astor==0.8.1
     # via flake8-simplify
-asttokens==2.1.0
+asttokens==2.2.0
     # via stack-data
 attrs==22.1.0
     # via
@@ -134,7 +134,7 @@ importlib-metadata==5.1.0
     #   twine
 iniconfig==1.1.1
     # via pytest
-ipython==8.6.0
+ipython==8.7.0
     # via -r requirements/tools.in
 isort==5.10.1
     # via shed
@@ -191,9 +191,9 @@ pexpect==4.8.0
     # via ipython
 pickleshare==0.7.5
     # via ipython
-pip-tools==6.10.0
+pip-tools==6.11.0
     # via -r requirements/tools.in
-pkginfo==1.8.3
+pkginfo==1.9.2
     # via twine
 platformdirs==2.5.4
     # via
@@ -229,7 +229,7 @@ pygments==2.13.0
     #   sphinx
 pyparsing==3.0.9
     # via packaging
-pyright==1.1.281
+pyright==1.1.282
     # via -r requirements/tools.in
 pytest==7.2.0
     # via -r requirements/tools.in
@@ -237,7 +237,7 @@ python-dateutil==2.8.2
     # via -r requirements/tools.in
 pytz==2022.6
     # via babel
-pyupgrade==3.2.2
+pyupgrade==3.3.0
     # via shed
 pyyaml==6.0
     # via
@@ -326,11 +326,11 @@ tomli==2.0.1
     #   tox
 tox==3.27.1
     # via -r requirements/tools.in
-traitlets==5.5.0
+traitlets==5.6.0
     # via
     #   ipython
     #   matplotlib-inline
-twine==4.0.1
+twine==4.0.2
     # via -r requirements/tools.in
 types-click==7.1.8
     # via -r requirements/tools.in
@@ -338,7 +338,7 @@ types-pkg-resources==0.1.3
     # via -r requirements/tools.in
 types-pytz==2022.6.0.1
     # via -r requirements/tools.in
-types-redis==4.3.21.5
+types-redis==4.3.21.6
     # via -r requirements/tools.in
 typing-extensions==4.4.0
     # via
@@ -354,7 +354,7 @@ urllib3==1.26.13
     # via
     #   requests
     #   twine
-virtualenv==20.16.7
+virtualenv==20.17.0
     # via tox
 wcwidth==0.2.5
     # via prompt-toolkit

test_patch: diff --git a/hypothesis-python/tests/cover/test_compat.py b/hypothesis-python/tests/cover/test_compat.py
index 673b19e07..90f4d1a2e 100644
--- a/hypothesis-python/tests/cover/test_compat.py
+++ b/hypothesis-python/tests/cover/test_compat.py
@@ -9,7 +9,9 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 import math
-from inspect import Parameter, Signature
+from dataclasses import dataclass
+from inspect import Parameter, Signature, signature
+from typing import ForwardRef, Optional, Union
 
 import pytest
 
@@ -45,3 +47,44 @@ class WeirdSig:
 
 def test_no_type_hints():
     assert get_type_hints(WeirdSig) == {}
+
+
+@dataclass
+class Foo:
+    x: "Foo" = None  # type: ignore
+
+
+Foo.__signature__ = signature(Foo).replace(  # type: ignore
+    parameters=[
+        Parameter(
+            "x",
+            Parameter.POSITIONAL_OR_KEYWORD,
+            annotation=ForwardRef("Foo"),
+            default=None,
+        )
+    ]
+)
+
+
+@dataclass
+class Bar:
+    x: Optional[Union[int, "Bar"]]
+
+
+Bar.__signature__ = signature(Bar).replace(  # type: ignore
+    parameters=[
+        Parameter(
+            "x",
+            Parameter.POSITIONAL_OR_KEYWORD,
+            annotation=Optional[Union[int, ForwardRef("Bar")]],  # type: ignore
+        )
+    ]
+)
+
+
+@pytest.mark.parametrize(
+    "obj,expected", [(Foo, Optional[Foo]), (Bar, Optional[Union[int, Bar]])]
+)
+def test_resolve_fwd_refs(obj, expected):
+    # See: https://github.com/HypothesisWorks/hypothesis/issues/3519
+    assert get_type_hints(obj)["x"] == expected


----

problem_statement: `get_type_hints()` should support `functools.partial()`
Our `get_type_hints()` helper doesn't currently support `functools.partial()`, but I think it should - it's easy enough to detect, recurse on the wrapped callable, and then drop any bound-positional args (bound kwargs just assign a default value).

We should also consider upstreaming this fix into `typing.get_type_hints()`, since `inspect.signature()` does this already.
date: 2023-01-06
patch: diff --git a/build.sh b/build.sh
index 29173336d..2d2920bc3 100755
--- a/build.sh
+++ b/build.sh
@@ -23,8 +23,8 @@ if [ -n "${GITHUB_ACTIONS-}" ] || [ -n "${CODESPACES-}" ] ; then
 else
     # Otherwise, we install it from scratch
     # NOTE: tooling keeps this version in sync with ci_version in tooling
-    "$SCRIPTS/ensure-python.sh" 3.10.8
-    PYTHON=$(pythonloc 3.10.8)/bin/python
+    "$SCRIPTS/ensure-python.sh" 3.10.9
+    PYTHON=$(pythonloc 3.10.9)/bin/python
 fi
 
 TOOL_REQUIREMENTS="$ROOT/requirements/tools.txt"
diff --git a/hypothesis-python/.coveragerc b/hypothesis-python/.coveragerc
index 1deab7668..4cd8a529e 100644
--- a/hypothesis-python/.coveragerc
+++ b/hypothesis-python/.coveragerc
@@ -23,5 +23,5 @@ exclude_lines =
     except ModuleNotFoundError:
     if PYPY:
     if TYPE_CHECKING:
+    if sys\.version_info
     if "\w+" in sys\.modules:
-    assert all\(.+\)
diff --git a/hypothesis-python/src/hypothesis/_error_if_old.py b/hypothesis-python/src/hypothesis/_error_if_old.py
index 176c74e27..d82c32eb3 100644
--- a/hypothesis-python/src/hypothesis/_error_if_old.py
+++ b/hypothesis-python/src/hypothesis/_error_if_old.py
@@ -19,5 +19,5 @@ This can only happen if your packaging toolchain is older than python_requires.
 See https://packaging.python.org/guides/distributing-packages-using-setuptools/
 """
 
-if sys.version_info[:2] < (3, 7):  # pragma: no cover
+if sys.version_info[:2] < (3, 7):
     raise Exception(message.format(__version__))
diff --git a/hypothesis-python/src/hypothesis/control.py b/hypothesis-python/src/hypothesis/control.py
index ebf2e5303..1ae300e11 100644
--- a/hypothesis-python/src/hypothesis/control.py
+++ b/hypothesis-python/src/hypothesis/control.py
@@ -9,7 +9,7 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 import math
-from typing import NoReturn, Union
+from typing import NoReturn, Optional, Union
 
 from hypothesis import Verbosity, settings
 from hypothesis.errors import InvalidArgument, UnsatisfiedAssumption
@@ -51,7 +51,7 @@ def currently_in_test_context() -> bool:
     return _current_build_context.value is not None
 
 
-def current_build_context():
+def current_build_context() -> "Optional[BuildContext]":
     context = _current_build_context.value
     if context is None:
         raise InvalidArgument("No build context registered")
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
index befea5dcf..1b0a7959d 100644
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -196,7 +196,7 @@ class example:
         # This is deliberately a no-op at runtime; the tools operate on source code.
         return self
 
-    if sys.version_info[:2] >= (3, 8):  # pragma: no branch
+    if sys.version_info[:2] >= (3, 8):
         # We want a positional-only argument, and on Python 3.8+ we'll get it.
         __sig = get_signature(via)
         via = define_function_signature(
@@ -348,7 +348,7 @@ def is_invalid_test(test, original_sig, given_arguments, given_kwargs):
 
     if ... in given_arguments:
         return invalid(
-            "... was passed as a positional argument to @given,  but may only be "
+            "... was passed as a positional argument to @given, but may only be "
             "passed as a keyword argument or as the sole argument of @given"
         )
 
@@ -717,28 +717,13 @@ class StateForActualGivenExecution:
 
                             if self.print_given_args:
                                 printer.text(" ")
-                                printer.text(test.__name__)
-                                with printer.group(indent=4, open="(", close=""):
-                                    printer.break_()
-                                    for v in args:
-                                        printer.pretty(v)
-                                        # We add a comma unconditionally because
-                                        # generated arguments will always be kwargs,
-                                        # so there will always be more to come.
-                                        printer.text(",")
-                                        printer.breakable()
-
-                                    for i, (k, v) in enumerate(kwargs.items()):
-                                        printer.text(k)
-                                        printer.text("=")
-                                        printer.pretty(v)
-                                        printer.text(",")
-                                        if i + 1 < len(kwargs):
-                                            printer.breakable()
-                                printer.break_()
-                                printer.text(")")
-                            printer.flush()
-                            report(output.getvalue())
+                                printer.repr_call(
+                                    test.__name__,
+                                    args,
+                                    kwargs,
+                                    force_split=True,
+                                )
+                            report(printer.getvalue())
                         return test(*args, **kwargs)
 
         # Run the test function once, via the executor hook.
@@ -1119,7 +1104,7 @@ def given(
                 for p in original_sig.parameters.values()
                 if p.kind is p.POSITIONAL_OR_KEYWORD
             ]
-            given_kwargs = dict(zip(posargs[::-1], given_arguments[::-1]))
+            given_kwargs = dict(list(zip(posargs[::-1], given_arguments[::-1]))[::-1])
         # These have been converted, so delete them to prevent accidental use.
         del given_arguments
 
diff --git a/hypothesis-python/src/hypothesis/extra/django/_impl.py b/hypothesis-python/src/hypothesis/extra/django/_impl.py
index 397898cdd..8b461a622 100644
--- a/hypothesis-python/src/hypothesis/extra/django/_impl.py
+++ b/hypothesis-python/src/hypothesis/extra/django/_impl.py
@@ -25,7 +25,7 @@ from hypothesis.extra.django._fields import from_field
 from hypothesis.internal.reflection import define_function_signature
 from hypothesis.strategies._internal.utils import defines_strategy
 
-if sys.version_info >= (3, 10):  # pragma: no cover
+if sys.version_info >= (3, 10):
     from types import EllipsisType as EllipsisType
 elif TYPE_CHECKING:
     from builtins import ellipsis as EllipsisType
@@ -130,7 +130,7 @@ def from_model(
     return _models_impl(st.builds(m_type.objects.get_or_create, **field_strategies))
 
 
-if sys.version_info[:2] >= (3, 8):  # pragma: no branch
+if sys.version_info[:2] >= (3, 8):
     # See notes above definition of st.builds() - this signature is compatible
     # and better matches the semantics of the function.  Great for documentation!
     sig = signature(from_model)
diff --git a/hypothesis-python/src/hypothesis/extra/ghostwriter.py b/hypothesis-python/src/hypothesis/extra/ghostwriter.py
index 6b24549a4..d13fc1100 100644
--- a/hypothesis-python/src/hypothesis/extra/ghostwriter.py
+++ b/hypothesis-python/src/hypothesis/extra/ghostwriter.py
@@ -52,7 +52,7 @@ generally do their best to write you a useful test.  You can also use
 .. tip::
 
     Using a light theme?  Hypothesis respects `NO_COLOR <https://no-color.org/>`__
-    and :envvar:`DJANGO_COLORS=light <django:DJANGO_COLORS>`.
+    and ``DJANGO_COLORS=light``.
 
 .. note::
 
@@ -117,7 +117,7 @@ from hypothesis.strategies._internal.strategies import (
 )
 from hypothesis.strategies._internal.types import _global_type_lookup, is_generic_type
 
-if sys.version_info >= (3, 10):  # pragma: no cover
+if sys.version_info >= (3, 10):
     from types import EllipsisType as EllipsisType
 elif TYPE_CHECKING:
     from builtins import ellipsis as EllipsisType
diff --git a/hypothesis-python/src/hypothesis/extra/numpy.py b/hypothesis-python/src/hypothesis/extra/numpy.py
index 06376ae42..6e2c7ec9b 100644
--- a/hypothesis-python/src/hypothesis/extra/numpy.py
+++ b/hypothesis-python/src/hypothesis/extra/numpy.py
@@ -208,15 +208,18 @@ class ArrayStrategy(st.SearchStrategy):
             ) from err
         if elem_changed:
             strategy = self.fill if fill else self.element_strategy
-            if self.dtype.kind == "f":
+            if self.dtype.kind == "f":  # pragma: no cover
+                # This logic doesn't trigger in our coverage tests under Numpy 1.24+,
+                # with built-in checks for overflow, but we keep it for good error
+                # messages and compatibility with older versions of Numpy.
                 try:
                     is_subnormal = 0 < abs(val) < np.finfo(self.dtype).tiny
-                except Exception:  # pragma: no cover
+                except Exception:
                     # val may be a non-float that does not support the
                     # operations __lt__ and __abs__
-                    is_subnormal = False  # pragma: no cover
+                    is_subnormal = False
                 if is_subnormal:
-                    raise InvalidArgument(  # pragma: no cover
+                    raise InvalidArgument(
                         f"Generated subnormal float {val} from strategy "
                         f"{strategy} resulted in {result[idx]!r}, probably "
                         "as a result of NumPy being built with flush-to-zero "
diff --git a/hypothesis-python/src/hypothesis/internal/compat.py b/hypothesis-python/src/hypothesis/internal/compat.py
index 6456744de..34b712e68 100644
--- a/hypothesis-python/src/hypothesis/internal/compat.py
+++ b/hypothesis-python/src/hypothesis/internal/compat.py
@@ -13,6 +13,7 @@ import inspect
 import platform
 import sys
 import typing
+from functools import partial
 from typing import Any, ForwardRef, Tuple
 
 try:
@@ -119,6 +120,14 @@ def get_type_hints(thing):
     Never errors: instead of raising TypeError for uninspectable objects, or
     NameError for unresolvable forward references, just return an empty dict.
     """
+    if isinstance(thing, partial):
+        from hypothesis.internal.reflection import get_signature
+
+        bound = set(get_signature(thing.func).parameters).difference(
+            get_signature(thing).parameters
+        )
+        return {k: v for k, v in get_type_hints(thing.func).items() if k not in bound}
+
     kwargs = {} if sys.version_info[:2] < (3, 9) else {"include_extras": True}
 
     try:
diff --git a/hypothesis-python/src/hypothesis/internal/entropy.py b/hypothesis-python/src/hypothesis/internal/entropy.py
index d47747792..bf7f3a9f0 100644
--- a/hypothesis-python/src/hypothesis/internal/entropy.py
+++ b/hypothesis-python/src/hypothesis/internal/entropy.py
@@ -22,7 +22,7 @@ from hypothesis.errors import HypothesisWarning, InvalidArgument
 from hypothesis.internal.compat import PYPY
 
 if TYPE_CHECKING:
-    if sys.version_info >= (3, 8):  # pragma: no cover
+    if sys.version_info >= (3, 8):
         from typing import Protocol
     else:
         from typing_extensions import Protocol
diff --git a/hypothesis-python/src/hypothesis/internal/reflection.py b/hypothesis-python/src/hypothesis/internal/reflection.py
index 1c1419c60..5e66b4471 100644
--- a/hypothesis-python/src/hypothesis/internal/reflection.py
+++ b/hypothesis-python/src/hypothesis/internal/reflection.py
@@ -20,13 +20,14 @@ import sys
 import textwrap
 import types
 from functools import wraps
+from io import StringIO
 from keyword import iskeyword
-from tokenize import detect_encoding
+from tokenize import COMMENT, detect_encoding, generate_tokens, untokenize
 from types import ModuleType
-from typing import TYPE_CHECKING, Callable
+from typing import TYPE_CHECKING, Any, Callable
 from unittest.mock import _patch as PatchType
 
-from hypothesis.internal.compat import is_typed_named_tuple, update_code_location
+from hypothesis.internal.compat import PYPY, is_typed_named_tuple, update_code_location
 from hypothesis.utils.conventions import not_set
 from hypothesis.vendor.pretty import pretty
 
@@ -48,6 +49,40 @@ def is_mock(obj):
     return hasattr(obj, "hypothesis_internal_is_this_a_mock_check")
 
 
+def _clean_source(src: str) -> bytes:
+    """Return the source code as bytes, without decorators or comments.
+
+    Because this is part of our database key, we reduce the cache invalidation
+    rate by ignoring decorators, comments, trailing whitespace, and empty lines.
+    We can't just use the (dumped) AST directly because it changes between Python
+    versions (e.g. ast.Constant)
+    """
+    # Get the (one-indexed) line number of the function definition, and drop preceding
+    # lines - i.e. any decorators, so that adding `@example()`s keeps the same key.
+    try:
+        funcdef = ast.parse(src).body[0]
+        if sys.version_info[:2] == (3, 7) or (sys.version_info[:2] == (3, 8) and PYPY):
+            # We can't get a line number of the (async) def here, so as a best-effort
+            # approximation we'll use str.split instead and hope for the best.
+            tag = "async def " if isinstance(funcdef, ast.AsyncFunctionDef) else "def "
+            if tag in src:
+                src = tag + src.split(tag, maxsplit=1)[1]
+        else:
+            src = "".join(src.splitlines(keepends=True)[funcdef.lineno - 1 :])
+    except Exception:
+        pass
+    # Remove blank lines and use the tokenize module to strip out comments,
+    # so that those can be changed without changing the database key.
+    try:
+        src = untokenize(
+            t for t in generate_tokens(StringIO(src).readline) if t.type != COMMENT
+        )
+    except Exception:
+        pass
+    # Finally, remove any trailing whitespace and empty lines as a last cleanup.
+    return "\n".join(x.rstrip() for x in src.splitlines() if x.rstrip()).encode()
+
+
 def function_digest(function):
     """Returns a string that is stable across multiple invocations across
     multiple processes and is prone to changing significantly in response to
@@ -57,24 +92,23 @@ def function_digest(function):
     """
     hasher = hashlib.sha384()
     try:
-        hasher.update(inspect.getsource(function).encode())
+        src = inspect.getsource(function)
     except (OSError, TypeError):
-        pass
-    try:
-        hasher.update(function.__name__.encode())
-    except AttributeError:
-        pass
+        # If we can't actually get the source code, try for the name as a fallback.
+        try:
+            hasher.update(function.__name__.encode())
+        except AttributeError:
+            pass
+    else:
+        hasher.update(_clean_source(src))
     try:
-        # We prefer to use the modern signature API, but left this for compatibility.
-        # While we don't promise stability of the database, there's no advantage to
-        # using signature here, so we might as well keep the existing keys for now.
-        spec = inspect.getfullargspec(function)
-        if inspect.ismethod(function):
-            del spec.args[0]
-        hasher.update(repr(spec).encode())
-    except TypeError:
+        # This is additional to the source code because it can include the effects
+        # of decorators, or of post-hoc assignment to the .__signature__ attribute.
+        hasher.update(repr(get_signature(function)).encode())
+    except Exception:
         pass
     try:
+        # We set this in order to distinguish e.g. @pytest.mark.parametrize cases.
         hasher.update(function._hypothesis_internal_add_digest)
     except AttributeError:
         pass
@@ -93,7 +127,7 @@ def check_signature(sig: inspect.Signature) -> None:
             )
 
 
-def get_signature(target, *, follow_wrapped=True):
+def get_signature(target: Any, *, follow_wrapped: bool = True) -> inspect.Signature:
     # Special case for use of `@unittest.mock.patch` decorator, mimicking the
     # behaviour of getfullargspec instead of reporting unusable arguments.
     patches = getattr(target, "patchings", None)
@@ -120,7 +154,7 @@ def get_signature(target, *, follow_wrapped=True):
                     parameters=[v for k, v in sig.parameters.items() if k != "self"]
                 )
         return sig
-    if sys.version_info[:2] <= (3, 8) and inspect.isclass(target):  # pragma: no cover
+    if sys.version_info[:2] <= (3, 8) and inspect.isclass(target):
         # Workaround for subclasses of typing.Generic on Python <= 3.8
         from hypothesis.strategies._internal.types import is_generic_type
 
@@ -409,6 +443,7 @@ def nicerepr(v):
 
 
 def repr_call(f, args, kwargs, reorder=True):
+    # Note: for multi-line pretty-printing, see RepresentationPrinter.repr_call()
     if reorder:
         args, kwargs = convert_positional_arguments(f, args, kwargs)
 
diff --git a/hypothesis-python/src/hypothesis/provisional.py b/hypothesis-python/src/hypothesis/provisional.py
index d3a699530..62b81cf8e 100644
--- a/hypothesis-python/src/hypothesis/provisional.py
+++ b/hypothesis-python/src/hypothesis/provisional.py
@@ -107,6 +107,11 @@ class DomainNameStrategy(st.SearchStrategy):
                 ).map("".join)
             )
         )
+        # RFC-5890 s2.3.1 says such labels are reserved, and since we don't
+        # want to bother with xn-- punycode labels we'll exclude them all.
+        elem_st = st.from_regex(self.label_regex, fullmatch=True).filter(
+            lambda label: len(label) < 4 or label[2:4] != "--"
+        )
         # The maximum possible number of subdomains is 126,
         # 1 character subdomain + 1 '.' character, * 126 = 252,
         # with a max of 255, that leaves 3 characters for a TLD.
@@ -115,7 +120,7 @@ class DomainNameStrategy(st.SearchStrategy):
         elements = cu.many(data, min_size=1, average_size=3, max_size=126)
         while elements.more():
             # Generate a new valid subdomain using the regex strategy.
-            sub_domain = data.draw(st.from_regex(self.label_regex, fullmatch=True))
+            sub_domain = data.draw(elem_st)
             if len(domain) + len(sub_domain) >= self.max_length:
                 data.stop_example(discard=True)
                 break
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
index 575c1c70a..f54321f54 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
@@ -116,6 +116,7 @@ from hypothesis.strategies._internal.strings import (
 )
 from hypothesis.strategies._internal.utils import cacheable, defines_strategy
 from hypothesis.utils.conventions import not_set
+from hypothesis.vendor.pretty import RepresentationPrinter
 
 if sys.version_info >= (3, 10):
     from types import EllipsisType as EllipsisType
@@ -125,7 +126,7 @@ else:
     EllipsisType = type(Ellipsis)  # pragma: no cover
 
 
-if sys.version_info >= (3, 8):  # pragma: no cover
+if sys.version_info >= (3, 8):
     from typing import Protocol
 elif TYPE_CHECKING:
     from typing_extensions import Protocol
@@ -902,7 +903,7 @@ def builds(
 
     If the callable is a class defined with :pypi:`attrs`, missing required
     arguments will be inferred from the attribute on a best-effort basis,
-    e.g. by checking :ref:`attrs standard validators <attrs:api_validators>`.
+    e.g. by checking :ref:`attrs standard validators <attrs:api-validators>`.
     Dataclasses are handled natively by the inference from type hints.
 
     Examples from this strategy shrink by shrinking the argument values to
@@ -960,7 +961,7 @@ def builds(
     return BuildsStrategy(target, args, kwargs)
 
 
-if sys.version_info[:2] >= (3, 8):  # pragma: no branch
+if sys.version_info[:2] >= (3, 8):
     # See notes above definition - this signature is compatible and better
     # matches the semantics of the function.  Great for documentation!
     sig = signature(builds)
@@ -1834,10 +1835,11 @@ class DataObject:
         check_strategy(strategy, "strategy")
         result = self.conjecture_data.draw(strategy)
         self.count += 1
-        if label is not None:
-            note(f"Draw {self.count} ({label}): {result!r}")
-        else:
-            note(f"Draw {self.count}: {result!r}")
+        printer = RepresentationPrinter()
+        printer.text(f"Draw {self.count}")
+        printer.text(": " if label is None else f" ({label}): ")
+        printer.pretty(result)
+        note(printer.getvalue())
         return result
 
 
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/random.py b/hypothesis-python/src/hypothesis/strategies/_internal/random.py
index 2c8997e52..c9bf4550a 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/random.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/random.py
@@ -70,6 +70,7 @@ RANDOM_METHODS = [
     for name in [
         "_randbelow",
         "betavariate",
+        "binomialvariate",
         "choice",
         "choices",
         "expovariate",
@@ -271,6 +272,9 @@ class ArtificialRandom(HypothesisRandom):
                 result = cu.integer_range(self.__data, start, stop - 1)
         elif method == "randint":
             result = cu.integer_range(self.__data, kwargs["a"], kwargs["b"])
+        # New in Python 3.12, so not taken by our coverage job
+        elif method == "binomialvariate":  # pragma: no cover
+            result = cu.integer_range(self.__data, 0, kwargs["n"])
         elif method == "choice":
             seq = kwargs["seq"]
             result = cu.integer_range(self.__data, 0, len(seq) - 1)
@@ -317,8 +321,7 @@ class ArtificialRandom(HypothesisRandom):
             result = self.__data.draw(floats(min_value=0.0))
         elif method == "shuffle":
             result = self.__data.draw(permutations(range(len(kwargs["x"]))))
-        # This is tested for but only appears in 3.9 so doesn't appear in coverage.
-        elif method == "randbytes":  # pragma: no cover
+        elif method == "randbytes":
             n = kwargs["n"]
             result = self.__data.draw(binary(min_size=n, max_size=n))
         else:
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/types.py b/hypothesis-python/src/hypothesis/strategies/_internal/types.py
index ddd73dd31..71e663532 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/types.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/types.py
@@ -582,7 +582,7 @@ if PYPY:
 _global_type_lookup[type] = st.sampled_from(
     [type(None)] + sorted(_global_type_lookup, key=str)
 )
-if sys.version_info[:2] >= (3, 9):  # pragma: no cover
+if sys.version_info[:2] >= (3, 9):
     # subclass of MutableMapping, and in Python 3.9 we resolve to a union
     # which includes this... but we don't actually ever want to build one.
     _global_type_lookup[os._Environ] = st.just(os.environ)
diff --git a/hypothesis-python/src/hypothesis/utils/terminal.py b/hypothesis-python/src/hypothesis/utils/terminal.py
index 6a6f410d6..afe6779a0 100644
--- a/hypothesis-python/src/hypothesis/utils/terminal.py
+++ b/hypothesis-python/src/hypothesis/utils/terminal.py
@@ -18,9 +18,10 @@ def guess_background_color():
     See also https://stackoverflow.com/questions/2507337/ and
     https://unix.stackexchange.com/questions/245378/
     """
-    django_colors = os.getenv("DJANGO_COLORS")
-    if django_colors in ("light", "dark"):
-        return django_colors
+    django_colors = os.getenv("DJANGO_COLORS", "")
+    for theme in ("light", "dark"):
+        if theme in django_colors.split(";"):
+            return theme
     # Guessing based on the $COLORFGBG environment variable
     try:
         fg, *_, bg = os.getenv("COLORFGBG").split(";")
diff --git a/hypothesis-python/src/hypothesis/vendor/pretty.py b/hypothesis-python/src/hypothesis/vendor/pretty.py
index 2f52ecf64..13b31f639 100644
--- a/hypothesis-python/src/hypothesis/vendor/pretty.py
+++ b/hypothesis-python/src/hypothesis/vendor/pretty.py
@@ -63,7 +63,6 @@ Inheritance diagram:
 """
 
 import datetime
-import platform
 import re
 import struct
 import types
@@ -74,17 +73,12 @@ from math import copysign, isnan
 
 __all__ = [
     "pretty",
-    "PrettyPrinter",
     "RepresentationPrinter",
-    "for_type_by_name",
 ]
 
 
-MAX_SEQ_LENGTH = 1000
 _re_pattern_type = type(re.compile(""))
 
-PYPY = platform.python_implementation() == "PyPy"
-
 
 def _safe_getattr(obj, attr, default=None):
     """Safe version of getattr.
@@ -99,56 +93,28 @@ def _safe_getattr(obj, attr, default=None):
         return default
 
 
-def pretty(
-    obj, verbose=False, max_width=79, newline="\n", max_seq_length=MAX_SEQ_LENGTH
-):
+def pretty(obj):
     """Pretty print the object's representation."""
-    stream = StringIO()
-    printer = RepresentationPrinter(
-        stream, verbose, max_width, newline, max_seq_length=max_seq_length
-    )
+    printer = RepresentationPrinter()
     printer.pretty(obj)
-    printer.flush()
-    return stream.getvalue()
-
-
-class _PrettyPrinterBase:
-    @contextmanager
-    def indent(self, indent):
-        """`with`-statement support for indenting/dedenting."""
-        self.indentation += indent
-        try:
-            yield
-        finally:
-            self.indentation -= indent
-
-    @contextmanager
-    def group(self, indent=0, open="", close=""):
-        """Like begin_group / end_group but for the with statement."""
-        self.begin_group(indent, open)
-        try:
-            yield
-        finally:
-            self.end_group(indent, close)
+    return printer.getvalue()
 
 
-class PrettyPrinter(_PrettyPrinterBase):
-    """Baseclass for the `RepresentationPrinter` prettyprinter that is used to
-    generate pretty reprs of objects.
+class RepresentationPrinter:
+    """Special pretty printer that has a `pretty` method that calls the pretty
+    printer for a python object.
 
-    Contrary to the `RepresentationPrinter` this printer knows nothing
-    about the default pprinters or the `_repr_pretty_` callback method.
+    This class stores processing data on `self` so you must *never* use
+    this class in a threaded environment.  Always lock it or
+    reinstantiate it.
 
     """
 
-    def __init__(
-        self, output, max_width=79, newline="\n", max_seq_length=MAX_SEQ_LENGTH
-    ):
+    def __init__(self, output=None):
         self.broken = False
-        self.output = output
-        self.max_width = max_width
-        self.newline = newline
-        self.max_seq_length = max_seq_length
+        self.output = StringIO() if output is None else output
+        self.max_width = 79
+        self.max_seq_length = 1000
         self.output_width = 0
         self.buffer_width = 0
         self.buffer = deque()
@@ -160,6 +126,59 @@ class PrettyPrinter(_PrettyPrinterBase):
 
         self.snans = 0
 
+        self.stack = []
+        self.singleton_pprinters = _singleton_pprinters.copy()
+        self.type_pprinters = _type_pprinters.copy()
+        self.deferred_pprinters = _deferred_type_pprinters.copy()
+
+    def pretty(self, obj):
+        """Pretty print the given object."""
+        obj_id = id(obj)
+        cycle = obj_id in self.stack
+        self.stack.append(obj_id)
+        try:
+            with self.group():
+                obj_class = _safe_getattr(obj, "__class__", None) or type(obj)
+                # First try to find registered singleton printers for the type.
+                try:
+                    printer = self.singleton_pprinters[obj_id]
+                except (TypeError, KeyError):
+                    pass
+                else:
+                    return printer(obj, self, cycle)
+                # Next walk the mro and check for either:
+                #   1) a registered printer
+                #   2) a _repr_pretty_ method
+                for cls in obj_class.__mro__:
+                    if cls in self.type_pprinters:
+                        # printer registered in self.type_pprinters
+                        return self.type_pprinters[cls](obj, self, cycle)
+                    else:
+                        # Check if the given class is specified in the deferred type
+                        # registry; move it to the regular type registry if so.
+                        key = (
+                            _safe_getattr(cls, "__module__", None),
+                            _safe_getattr(cls, "__name__", None),
+                        )
+                        if key in self.deferred_pprinters:
+                            # Move the printer over to the regular registry.
+                            printer = self.deferred_pprinters.pop(key)
+                            self.type_pprinters[cls] = printer
+                            return printer(obj, self, cycle)
+                        else:
+                            # Finally look for special method names.
+                            # Some objects automatically create any requested
+                            # attribute. Try to ignore most of them by checking for
+                            # callability.
+                            if "_repr_pretty_" in cls.__dict__:
+                                meth = cls._repr_pretty_
+                                if callable(meth):
+                                    return meth(obj, self, cycle)
+                # A user-provided repr. Find newlines and replace them with p.break_()
+                return _repr_pprint(obj, self, cycle)
+        finally:
+            self.stack.pop()
+
     def _break_outer_groups(self):
         while self.max_width < self.output_width + self.buffer_width:
             group = self.group_queue.deq()
@@ -201,8 +220,7 @@ class PrettyPrinter(_PrettyPrinterBase):
         group = self.group_stack[-1]
         if group.want_break:
             self.flush()
-            self.output.write(self.newline)
-            self.output.write(" " * self.indentation)
+            self.output.write("\n" + " " * self.indentation)
             self.output_width = self.indentation
             self.buffer_width = 0
         else:
@@ -214,24 +232,28 @@ class PrettyPrinter(_PrettyPrinterBase):
         """Explicitly insert a newline into the output, maintaining correct
         indentation."""
         self.flush()
-        self.output.write(self.newline)
-        self.output.write(" " * self.indentation)
+        self.output.write("\n" + " " * self.indentation)
         self.output_width = self.indentation
         self.buffer_width = 0
 
-    def begin_group(self, indent=0, open=""):
-        """
-        Begin a group.  If you want support for python < 2.5 which doesn't has
-        the with statement this is the preferred way:
-            p.begin_group(1, '{')
-            ...
-            p.end_group(1, '}')
-        The python 2.5 expression would be this:
+    @contextmanager
+    def indent(self, indent):
+        """`with`-statement support for indenting/dedenting."""
+        self.indentation += indent
+        try:
+            yield
+        finally:
+            self.indentation -= indent
+
+    @contextmanager
+    def group(self, indent=0, open="", close=""):
+        """Context manager for an indented group.
+
             with p.group(1, '{', '}'):
-                ...
-        The first parameter specifies the indentation for the next line (
-        usually the width of the opening text), the second the opening text.
-        All parameters are optional.
+
+        The first parameter specifies the indentation for the next line
+        (usually the width of the opening text), the second and third the
+        opening and closing delimiters.
         """
         if open:
             self.text(open)
@@ -239,6 +261,15 @@ class PrettyPrinter(_PrettyPrinterBase):
         self.group_stack.append(group)
         self.group_queue.enq(group)
         self.indentation += indent
+        try:
+            yield
+        finally:
+            self.indentation -= indent
+            group = self.group_stack.pop()
+            if not group.breakables:
+                self.group_queue.remove(group)
+            if close:
+                self.text(close)
 
     def _enumerate(self, seq):
         """Like enumerate, but with an upper limit on the number of items."""
@@ -250,19 +281,6 @@ class PrettyPrinter(_PrettyPrinterBase):
                 return
             yield idx, x
 
-    def end_group(self, dedent=0, close=""):
-        """End a group.
-
-        See `begin_group` for more details.
-
-        """
-        self.indentation -= dedent
-        group = self.group_stack.pop()
-        if not group.breakables:
-            self.group_queue.remove(group)
-        if close:
-            self.text(close)
-
     def flush(self):
         """Flush data that is left in the buffer."""
         if self.snans:
@@ -276,121 +294,49 @@ class PrettyPrinter(_PrettyPrinterBase):
         self.buffer.clear()
         self.buffer_width = 0
 
+    def getvalue(self):
+        assert isinstance(self.output, StringIO)
+        self.flush()
+        return self.output.getvalue()
 
-def _get_mro(obj_class):
-    """Get a reasonable method resolution order of a class and its superclasses
-    for both old-style and new-style classes."""
-    if not hasattr(obj_class, "__mro__"):  # pragma: no cover
-        # Old-style class. Mix in object to make a fake new-style class.
-        try:
-            obj_class = type(obj_class.__name__, (obj_class, object), {})
-        except TypeError:
-            # Old-style extension type that does not descend from object.
-            # FIXME: try to construct a more thorough MRO.
-            mro = [obj_class]
-        else:
-            mro = obj_class.__mro__[1:-1]
-    else:
-        mro = obj_class.__mro__
-    return mro
-
-
-class RepresentationPrinter(PrettyPrinter):
-    """Special pretty printer that has a `pretty` method that calls the pretty
-    printer for a python object.
-
-    This class stores processing data on `self` so you must *never* use
-    this class in a threaded environment.  Always lock it or
-    reinstanciate it. Instances also have a verbose flag callbacks can
-    access to control their output.  For example the default instance
-    repr prints all attributes and methods that are not prefixed by an
-    underscore if the printer is in verbose mode.
-
-    """
-
-    def __init__(
-        self,
-        output,
-        verbose=False,
-        max_width=79,
-        newline="\n",
-        singleton_pprinters=None,
-        type_pprinters=None,
-        deferred_pprinters=None,
-        max_seq_length=MAX_SEQ_LENGTH,
-    ):
-
-        super().__init__(output, max_width, newline, max_seq_length=max_seq_length)
-        self.verbose = verbose
-        self.stack = []
-        if singleton_pprinters is None:
-            singleton_pprinters = _singleton_pprinters.copy()
-        self.singleton_pprinters = singleton_pprinters
-        if type_pprinters is None:
-            type_pprinters = _type_pprinters.copy()
-        self.type_pprinters = type_pprinters
-        if deferred_pprinters is None:
-            deferred_pprinters = _deferred_type_pprinters.copy()
-        self.deferred_pprinters = deferred_pprinters
-
-    def pretty(self, obj):
-        """Pretty print the given object."""
-        obj_id = id(obj)
-        cycle = obj_id in self.stack
-        self.stack.append(obj_id)
-        self.begin_group()
-        try:
-            obj_class = _safe_getattr(obj, "__class__", None) or type(obj)
-            # First try to find registered singleton printers for the type.
-            try:
-                printer = self.singleton_pprinters[obj_id]
-            except (TypeError, KeyError):
-                pass
-            else:
-                return printer(obj, self, cycle)
-            # Next walk the mro and check for either:
-            #   1) a registered printer
-            #   2) a _repr_pretty_ method
-            for cls in _get_mro(obj_class):
-                if cls in self.type_pprinters:
-                    # printer registered in self.type_pprinters
-                    return self.type_pprinters[cls](obj, self, cycle)
-                else:
-                    # deferred printer
-                    printer = self._in_deferred_types(cls)
-                    if printer is not None:
-                        return printer(obj, self, cycle)
-                    else:
-                        # Finally look for special method names.
-                        # Some objects automatically create any requested
-                        # attribute. Try to ignore most of them by checking for
-                        # callability.
-                        if "_repr_pretty_" in cls.__dict__:
-                            meth = cls._repr_pretty_
-                            if callable(meth):
-                                return meth(obj, self, cycle)
-            return _default_pprint(obj, self, cycle)
-        finally:
-            self.end_group()
-            self.stack.pop()
-
-    def _in_deferred_types(self, cls):
-        """Check if the given class is specified in the deferred type registry.
-
-        Returns the printer from the registry if it exists, and None if
-        the class is not in the registry. Successful matches will be
-        moved to the regular type registry for future use.
+    def repr_call(self, func_name, args, kwargs, *, force_split=False):
+        """Helper function to represent a function call.
 
+        - func_name, args, and kwargs should all be pretty obvious.
+        - If split_lines, we'll force one-argument-per-line; otherwise we'll place
+          calls that fit on a single line (and split otherwise).
         """
-        mod = _safe_getattr(cls, "__module__", None)
-        name = _safe_getattr(cls, "__name__", None)
-        key = (mod, name)
-        printer = None
-        if key in self.deferred_pprinters:
-            # Move the printer over to the regular registry.
-            printer = self.deferred_pprinters.pop(key)
-            self.type_pprinters[cls] = printer
-        return printer
+        assert isinstance(func_name, str)
+        if func_name.startswith(("lambda:", "lambda ")):
+            func_name = f"({func_name})"
+        self.text(func_name)
+        all_args = [(None, v) for v in args] + list(kwargs.items())
+        if not force_split:
+            # We're OK with printing this call on a single line, but will it fit?
+            # If not, we'd rather fall back to one-argument-per-line instead.
+            p = RepresentationPrinter()
+            for k, v in all_args:
+                if k:
+                    p.text(f"{k}=")
+                p.pretty(v)
+                p.text(", ")
+            force_split = self.max_width <= self.output_width + len(p.getvalue())
+
+        with self.group(indent=4, open="(", close=""):
+            all_args = [(None, v) for v in args] + list(kwargs.items())
+            for i, (k, v) in enumerate(all_args):
+                if force_split:
+                    self.break_()
+                elif i:
+                    self.breakable()
+                if k:
+                    self.text(f"{k}=")
+                self.pretty(v)
+                if force_split or i + 1 < len(all_args):
+                    self.text(",")
+        if all_args and force_split:
+            self.break_()
+        self.text(")")  # after dedent
 
 
 class Printable:
@@ -425,8 +371,7 @@ class Breakable(Printable):
     def output(self, stream, output_width):
         self.group.breakables.popleft()
         if self.group.want_break:
-            stream.write(self.pretty.newline)
-            stream.write(" " * self.indentation)
+            stream.write("\n" + " " * self.indentation)
             return self.indentation
         if not self.group.breakables:
             self.pretty.group_queue.remove(self.group)
@@ -471,46 +416,6 @@ class GroupQueue:
             pass
 
 
-def _default_pprint(obj, p, cycle):
-    """The default print function.
-
-    Used if an object does not provide one and it's none of the builtin
-    objects.
-
-    """
-    klass = _safe_getattr(obj, "__class__", None) or type(obj)
-    if _safe_getattr(klass, "__repr__", None) is not object.__repr__:
-        # A user-provided repr. Find newlines and replace them with p.break_()
-        _repr_pprint(obj, p, cycle)
-        return
-    p.begin_group(1, "<")
-    p.pretty(klass)
-    p.text(f" at 0x{id(obj):x}")
-    if cycle:
-        p.text(" ...")
-    elif p.verbose:
-        first = True
-        for key in dir(obj):
-            if not key.startswith("_"):
-                try:
-                    value = getattr(obj, key)
-                except AttributeError:
-                    continue
-                if isinstance(value, types.MethodType):
-                    continue
-                if not first:
-                    p.text(",")
-                p.breakable()
-                p.text(key)
-                p.text("=")
-                step = len(key) + 1
-                p.indentation += step
-                p.pretty(value)
-                p.indentation -= step
-                first = False
-    p.end_group(1, ">")
-
-
 def _seq_pprinter_factory(start, end, basetype):
     """Factory that returns a pprint function useful for sequences.
 
@@ -531,16 +436,15 @@ def _seq_pprinter_factory(start, end, basetype):
         if cycle:
             return p.text(start + "..." + end)
         step = len(start)
-        p.begin_group(step, start)
-        for idx, x in p._enumerate(obj):
-            if idx:
+        with p.group(step, start, end):
+            for idx, x in p._enumerate(obj):
+                if idx:
+                    p.text(",")
+                    p.breakable()
+                p.pretty(x)
+            if len(obj) == 1 and type(obj) is tuple:
+                # Special case for 1-item tuples.
                 p.text(",")
-                p.breakable()
-            p.pretty(x)
-        if len(obj) == 1 and type(obj) is tuple:
-            # Special case for 1-item tuples.
-            p.text(",")
-        p.end_group(step, end)
 
     return inner
 
@@ -566,22 +470,20 @@ def _set_pprinter_factory(start, end, basetype):
             p.text(basetype.__name__ + "()")
         else:
             step = len(start)
-            p.begin_group(step, start)
-            # Like dictionary keys, we will try to sort the items if there
-            # aren't too many
-            items = obj
-            if not (p.max_seq_length and len(obj) >= p.max_seq_length):
-                try:
-                    items = sorted(obj)
-                except Exception:
-                    # Sometimes the items don't sort.
-                    pass
-            for idx, x in p._enumerate(items):
-                if idx:
-                    p.text(",")
-                    p.breakable()
-                p.pretty(x)
-            p.end_group(step, end)
+            with p.group(step, start, end):
+                # Like dictionary keys, try to sort the items if there aren't too many
+                items = obj
+                if not (p.max_seq_length and len(obj) >= p.max_seq_length):
+                    try:
+                        items = sorted(obj)
+                    except Exception:
+                        # Sometimes the items don't sort.
+                        pass
+                for idx, x in p._enumerate(items):
+                    if idx:
+                        p.text(",")
+                        p.breakable()
+                    p.pretty(x)
 
     return inner
 
@@ -602,15 +504,14 @@ def _dict_pprinter_factory(start, end, basetype=None):
 
         if cycle:
             return p.text("{...}")
-        p.begin_group(1, start)
-        for idx, key in p._enumerate(obj):
-            if idx:
-                p.text(",")
-                p.breakable()
-            p.pretty(key)
-            p.text(": ")
-            p.pretty(obj[key])
-        p.end_group(1, end)
+        with p.group(1, start, end):
+            for idx, key in p._enumerate(obj):
+                if idx:
+                    p.text(",")
+                    p.breakable()
+                p.pretty(key)
+                p.text(": ")
+                p.pretty(obj[key])
 
     inner.__name__ = f"_dict_pprinter_factory({start!r}, {end!r}, {basetype!r})"
     return inner
@@ -618,21 +519,11 @@ def _dict_pprinter_factory(start, end, basetype=None):
 
 def _super_pprint(obj, p, cycle):
     """The pprint for the super type."""
-    try:
-        # This section works around various pypy versions that don't do
-        # have the same attributes on super objects
-        obj.__thisclass__
-        obj.__self__
-    except AttributeError:  # pragma: no cover
-        assert PYPY
-        _repr_pprint(obj, p, cycle)
-        return
-    p.begin_group(8, "<super: ")
-    p.pretty(obj.__thisclass__)
-    p.text(",")
-    p.breakable()
-    p.pretty(obj.__self__)
-    p.end_group(8, ">")
+    with p.group(8, "<super: ", ">"):
+        p.pretty(obj.__thisclass__)
+        p.text(",")
+        p.breakable()
+        p.pretty(obj.__self__)
 
 
 def _re_pattern_pprint(obj, p, cycle):
@@ -720,13 +611,12 @@ def _exception_pprint(obj, p, cycle):
     if obj.__class__.__module__ not in ("exceptions", "builtins"):
         name = f"{obj.__class__.__module__}.{name}"
     step = len(name) + 1
-    p.begin_group(step, name + "(")
-    for idx, arg in enumerate(getattr(obj, "args", ())):
-        if idx:
-            p.text(",")
-            p.breakable()
-        p.pretty(arg)
-    p.end_group(step, ")")
+    with p.group(step, name + "(", ")"):
+        for idx, arg in enumerate(getattr(obj, "args", ())):
+            if idx:
+                p.text(",")
+                p.breakable()
+            p.pretty(arg)
 
 
 def _repr_float_counting_nans(obj, p, cycle):
diff --git a/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt b/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt
index cbff00706..30cf8b9fa 100644
--- a/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt
+++ b/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt
@@ -1,4 +1,4 @@
-# Version 2022112700, Last Updated Sun Nov 27 07:07:01 2022 UTC
+# Version 2022123100, Last Updated Sat Dec 31 07:07:01 2022 UTC
 AAA
 AARP
 ABARTH
@@ -720,7 +720,6 @@ LOAN
 LOANS
 LOCKER
 LOCUS
-LOFT
 LOL
 LONDON
 LOTTE
@@ -1057,7 +1056,6 @@ SEEK
 SELECT
 SENER
 SERVICES
-SES
 SEVEN
 SEW
 SEX
@@ -1384,7 +1382,6 @@ XN--J1AEF
 XN--J1AMH
 XN--J6W193G
 XN--JLQ480N2RG
-XN--JLQ61U9W7B
 XN--JVR189M
 XN--KCRX77D1X4A
 XN--KPRW13D
diff --git a/hypothesis-python/src/hypothesis/version.py b/hypothesis-python/src/hypothesis/version.py
index a48a54199..0e1e63b27 100644
--- a/hypothesis-python/src/hypothesis/version.py
+++ b/hypothesis-python/src/hypothesis/version.py
@@ -8,5 +8,5 @@
 # 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/.
 
-__version_info__ = (6, 59, 0)
+__version_info__ = (6, 61, 2)
 __version__ = ".".join(map(str, __version_info__))
diff --git a/hypothesis-python/tox.ini b/hypothesis-python/tox.ini
index fc86194b3..4b295cd8b 100644
--- a/hypothesis-python/tox.ini
+++ b/hypothesis-python/tox.ini
@@ -5,7 +5,7 @@ toxworkdir={env:TOX_WORK_DIR:.tox}
 [testenv]
 deps =
     -r../requirements/test.txt
-whitelist_externals=
+allowlist_externals =
     bash
 passenv=
     HOME
@@ -18,9 +18,9 @@ setenv=
     brief: HYPOTHESIS_PROFILE=speedy
 commands =
     full: bash scripts/basic-test.sh
-    brief: python -bb -X dev -m pytest tests/cover/test_testdecorators.py {posargs}
-    cover: python -bb -X dev -m pytest tests/cover/ tests/pytest/ {posargs}
-    nocover: python -bb -X dev -m pytest tests/nocover/ {posargs}
+    brief: python -bb -X dev -m pytest -n auto tests/cover/test_testdecorators.py {posargs}
+    cover: python -bb -X dev -m pytest -n auto tests/cover/ tests/pytest/ {posargs}
+    nocover: python -bb -X dev -m pytest -n auto tests/nocover/ {posargs}
     niche: bash scripts/other-tests.sh
     custom: python -bb -X dev -m pytest {posargs}
 
@@ -49,6 +49,7 @@ commands=
 deps =
     -r../requirements/test.txt
     pandas~=1.0.5
+    numpy~=1.23.5
 commands =
     python -bb -X dev -m pytest tests/pandas -n auto
 
@@ -141,7 +142,7 @@ commands=
 [testenv:coverage]
 deps =
     -r../requirements/coverage.txt
-whitelist_externals=
+allowlist_externals =
     rm
 setenv=
     PYTHONDEVMODE=1
diff --git a/requirements/coverage.txt b/requirements/coverage.txt
index 97bd5958a..bed6446c0 100644
--- a/requirements/coverage.txt
+++ b/requirements/coverage.txt
@@ -1,36 +1,36 @@
 #
-# This file is autogenerated by pip-compile with python 3.10
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
 #
 #    ./build.sh upgrade-requirements
 #
 async-timeout==4.0.2
     # via redis
-attrs==22.1.0
+attrs==22.2.0
     # via
     #   hypothesis (hypothesis-python/setup.py)
     #   pytest
-black==22.10.0
+black==22.12.0
     # via -r requirements/coverage.in
 click==8.1.3
     # via
     #   -r requirements/coverage.in
     #   black
-coverage==6.5.0
+coverage==7.0.1
     # via -r requirements/coverage.in
 dpcontracts==0.6.0
     # via -r requirements/coverage.in
-exceptiongroup==1.0.4 ; python_version < "3.11"
+exceptiongroup==1.1.0 ; python_version < "3.11"
     # via
     #   hypothesis (hypothesis-python/setup.py)
     #   pytest
 execnet==1.9.0
     # via pytest-xdist
-fakeredis==2.1.0
+fakeredis==2.4.0
     # via -r requirements/coverage.in
 iniconfig==1.1.1
     # via pytest
-lark==1.1.4
+lark==1.1.5
     # via -r requirements/coverage.in
 libcst==0.4.9
     # via -r requirements/coverage.in
@@ -38,28 +38,24 @@ mypy-extensions==0.4.3
     # via
     #   black
     #   typing-inspect
-numpy==1.23.5
+numpy==1.24.1
     # via
     #   -r requirements/coverage.in
     #   pandas
-packaging==21.3
-    # via
-    #   pytest
-    #   redis
+packaging==22.0
+    # via pytest
 pandas==1.5.2
     # via -r requirements/coverage.in
-pathspec==0.10.2
+pathspec==0.10.3
     # via black
 pexpect==4.8.0
     # via -r requirements/test.in
-platformdirs==2.5.4
+platformdirs==2.6.2
     # via black
 pluggy==1.0.0
     # via pytest
 ptyprocess==0.7.0
     # via pexpect
-pyparsing==3.0.9
-    # via packaging
 pytest==7.2.0
     # via
     #   -r requirements/test.in
@@ -70,13 +66,13 @@ python-dateutil==2.8.2
     # via
     #   -r requirements/coverage.in
     #   pandas
-pytz==2022.6
+pytz==2022.7
     # via
     #   -r requirements/coverage.in
     #   pandas
 pyyaml==6.0
     # via libcst
-redis==4.3.5
+redis==4.4.0
     # via fakeredis
 six==1.16.0
     # via python-dateutil
diff --git a/requirements/test.txt b/requirements/test.txt
index 38e3e5f60..10e5580cf 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -1,14 +1,14 @@
 #
-# This file is autogenerated by pip-compile with python 3.10
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
 #
 #    ./build.sh upgrade-requirements
 #
-attrs==22.1.0
+attrs==22.2.0
     # via
     #   hypothesis (hypothesis-python/setup.py)
     #   pytest
-exceptiongroup==1.0.4 ; python_version < "3.11"
+exceptiongroup==1.1.0 ; python_version < "3.11"
     # via
     #   hypothesis (hypothesis-python/setup.py)
     #   pytest
@@ -16,7 +16,7 @@ execnet==1.9.0
     # via pytest-xdist
 iniconfig==1.1.1
     # via pytest
-packaging==21.3
+packaging==22.0
     # via pytest
 pexpect==4.8.0
     # via -r requirements/test.in
@@ -24,8 +24,6 @@ pluggy==1.0.0
     # via pytest
 ptyprocess==0.7.0
     # via pexpect
-pyparsing==3.0.9
-    # via packaging
 pytest==7.2.0
     # via
     #   -r requirements/test.in
diff --git a/requirements/tools.txt b/requirements/tools.txt
index f7d8554e9..bea4434e8 100644
--- a/requirements/tools.txt
+++ b/requirements/tools.txt
@@ -1,18 +1,18 @@
 #
-# This file is autogenerated by pip-compile with python 3.10
-# To update, run:
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
 #
 #    ./build.sh upgrade-requirements
 #
 alabaster==0.7.12
     # via sphinx
-asgiref==3.5.2
+asgiref==3.6.0
     # via django
 astor==0.8.1
     # via flake8-simplify
-asttokens==2.2.0
+asttokens==2.2.1
     # via stack-data
-attrs==22.1.0
+attrs==22.2.0
     # via
     #   flake8-bugbear
     #   hypothesis (hypothesis-python/setup.py)
@@ -27,16 +27,20 @@ bandit==1.7.4
     # via flake8-bandit
 beautifulsoup4==4.11.1
     # via sphinx-codeautolink
-black==22.10.0
+black==22.12.0
     # via shed
 bleach==5.0.1
     # via readme-renderer
 build==0.9.0
     # via pip-tools
-certifi==2022.9.24
+cachetools==5.2.0
+    # via tox
+certifi==2022.12.7
     # via requests
 cffi==1.15.1
     # via cryptography
+chardet==5.1.0
+    # via tox
 charset-normalizer==2.1.1
     # via requests
 click==8.1.3
@@ -45,11 +49,13 @@ click==8.1.3
     #   pip-tools
 codespell==2.2.2
     # via -r requirements/tools.in
+colorama==0.4.6
+    # via tox
 com2ann==0.3.0
     # via shed
 commonmark==0.9.1
     # via rich
-coverage==6.5.0
+coverage==7.0.1
     # via -r requirements/tools.in
 cryptography==38.0.4
     # via secretstorage
@@ -57,23 +63,22 @@ decorator==5.1.1
     # via ipython
 distlib==0.3.6
     # via virtualenv
-django==4.1.3
+django==4.1.4
     # via -r requirements/tools.in
-docutils==0.17.1
+docutils==0.19
     # via
     #   readme-renderer
     #   restructuredtext-lint
     #   sphinx
-    #   sphinx-rtd-theme
 dpcontracts==0.6.0
     # via -r requirements/tools.in
-exceptiongroup==1.0.4 ; python_version < "3.11"
+exceptiongroup==1.1.0 ; python_version < "3.11"
     # via
     #   hypothesis (hypothesis-python/setup.py)
     #   pytest
 executing==1.2.0
     # via stack-data
-filelock==3.8.0
+filelock==3.9.0
     # via
     #   tox
     #   virtualenv
@@ -94,9 +99,9 @@ flake8-2020==1.7.0
     # via -r requirements/tools.in
 flake8-bandit==4.1.1
     # via -r requirements/tools.in
-flake8-bugbear==22.10.27
+flake8-bugbear==22.12.6
     # via -r requirements/tools.in
-flake8-builtins==2.0.1
+flake8-builtins==2.1.0
     # via -r requirements/tools.in
 flake8-comprehensions==3.10.1
     # via -r requirements/tools.in
@@ -122,13 +127,13 @@ flake8-simplify==0.19.3
     # via -r requirements/tools.in
 gitdb==4.0.10
     # via gitpython
-gitpython==3.1.29
+gitpython==3.1.30
     # via bandit
 idna==3.4
     # via requests
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==5.1.0
+importlib-metadata==5.2.0
     # via
     #   keyring
     #   twine
@@ -136,7 +141,7 @@ iniconfig==1.1.1
     # via pytest
 ipython==8.7.0
     # via -r requirements/tools.in
-isort==5.10.1
+isort==5.11.4
     # via shed
 jaraco-classes==3.2.3
     # via keyring
@@ -148,9 +153,9 @@ jeepney==0.8.0
     #   secretstorage
 jinja2==3.1.2
     # via sphinx
-keyring==23.11.0
+keyring==23.13.1
     # via twine
-lark==1.1.4
+lark==1.1.5
     # via -r requirements/tools.in
 libcst==0.4.9
     # via
@@ -173,15 +178,16 @@ mypy-extensions==0.4.3
     #   typing-inspect
 nodeenv==1.7.0
     # via pyright
-packaging==21.3
+packaging==22.0
     # via
     #   build
+    #   pyproject-api
     #   pytest
     #   sphinx
     #   tox
 parso==0.8.3
     # via jedi
-pathspec==0.10.2
+pathspec==0.10.3
     # via black
 pbr==5.11.0
     # via stevedore
@@ -191,26 +197,25 @@ pexpect==4.8.0
     # via ipython
 pickleshare==0.7.5
     # via ipython
-pip-tools==6.11.0
+pip-tools==6.12.1
     # via -r requirements/tools.in
 pkginfo==1.9.2
     # via twine
-platformdirs==2.5.4
+platformdirs==2.6.2
     # via
     #   black
+    #   tox
     #   virtualenv
 pluggy==1.0.0
     # via
     #   pytest
     #   tox
-prompt-toolkit==3.0.33
+prompt-toolkit==3.0.36
     # via ipython
 ptyprocess==0.7.0
     # via pexpect
 pure-eval==0.2.2
     # via stack-data
-py==1.11.0
-    # via tox
 pycodestyle==2.10.0
     # via flake8
 pycparser==2.21
@@ -227,17 +232,17 @@ pygments==2.13.0
     #   readme-renderer
     #   rich
     #   sphinx
-pyparsing==3.0.9
-    # via packaging
-pyright==1.1.282
+pyproject-api==1.2.1
+    # via tox
+pyright==1.1.286
     # via -r requirements/tools.in
 pytest==7.2.0
     # via -r requirements/tools.in
 python-dateutil==2.8.2
     # via -r requirements/tools.in
-pytz==2022.6
+pytz==2022.7
     # via babel
-pyupgrade==3.3.0
+pyupgrade==3.3.1
     # via shed
 pyyaml==6.0
     # via
@@ -257,18 +262,17 @@ restructuredtext-lint==1.4.0
     # via -r requirements/tools.in
 rfc3986==2.0.0
     # via twine
-rich==12.6.0
+rich==13.0.0
     # via twine
 secretstorage==3.3.3
     # via keyring
-shed==0.10.7
+shed==0.10.8
     # via -r requirements/tools.in
 six==1.16.0
     # via
     #   asttokens
     #   bleach
     #   python-dateutil
-    #   tox
 smmap==5.0.0
     # via gitdb
 snowballstemmer==2.2.0
@@ -279,7 +283,7 @@ sortedcontainers==2.4.0
     # via hypothesis (hypothesis-python/setup.py)
 soupsieve==2.3.2.post1
     # via beautifulsoup4
-sphinx==5.3.0
+sphinx==6.0.0
     # via
     #   -r requirements/tools.in
     #   sphinx-codeautolink
@@ -289,7 +293,7 @@ sphinx-codeautolink==0.12.1
     # via -r requirements/tools.in
 sphinx-hoverxref==1.3.0
     # via -r requirements/tools.in
-sphinx-rtd-theme==1.1.1
+sphinx-rtd-theme==0.5.1
     # via -r requirements/tools.in
 sphinx-selective-exclude==1.0.3
     # via -r requirements/tools.in
@@ -322,11 +326,12 @@ tomli==2.0.1
     #   build
     #   mypy
     #   pep517
+    #   pyproject-api
     #   pytest
     #   tox
-tox==3.27.1
+tox==4.1.2
     # via -r requirements/tools.in
-traitlets==5.6.0
+traitlets==5.8.0
     # via
     #   ipython
     #   matplotlib-inline
@@ -336,9 +341,9 @@ types-click==7.1.8
     # via -r requirements/tools.in
 types-pkg-resources==0.1.3
     # via -r requirements/tools.in
-types-pytz==2022.6.0.1
+types-pytz==2022.7.0.0
     # via -r requirements/tools.in
-types-redis==4.3.21.6
+types-redis==4.3.21.7
     # via -r requirements/tools.in
 typing-extensions==4.4.0
     # via
@@ -354,7 +359,7 @@ urllib3==1.26.13
     # via
     #   requests
     #   twine
-virtualenv==20.17.0
+virtualenv==20.17.1
     # via tox
 wcwidth==0.2.5
     # via prompt-toolkit
diff --git a/tooling/src/hypothesistooling/__main__.py b/tooling/src/hypothesistooling/__main__.py
index 9909260f0..b4999d27f 100644
--- a/tooling/src/hypothesistooling/__main__.py
+++ b/tooling/src/hypothesistooling/__main__.py
@@ -330,16 +330,9 @@ def upgrade_requirements():
     compile_requirements(upgrade=True)
     subprocess.call(["./build.sh", "format"], cwd=tools.ROOT)  # exits 1 if changed
     if has_diff(hp.PYTHON_SRC) and not os.path.isfile(hp.RELEASE_FILE):
-        if has_diff(f"{hp.PYTHON_SRC}/hypothesis/vendor/tlds-alpha-by-domain.txt"):
-            msg = (
-                "our vendored `list of top-level domains "
-                "<https://www.iana.org/domains/root/db>`__,\nwhich is used by the "
-                "provisional :func:`~hypothesis.provisional.domains` strategy."
-            )
-        else:
-            msg = "our autoformatting tools, improving our code style without any API changes."
+        msg = hp.get_autoupdate_message(domainlist_changed=has_diff(hp.DOMAINS_LIST))
         with open(hp.RELEASE_FILE, mode="w") as f:
-            f.write(f"RELEASE_TYPE: patch\n\nThis patch updates {msg}\n")
+            f.write(f"RELEASE_TYPE: patch\n\n" + msg)
     update_python_versions()
     subprocess.call(["git", "add", "."], cwd=tools.ROOT)
 
@@ -386,15 +379,15 @@ def run_tox(task, version, *args):
 # When a version is added or removed, manually update the env lists in tox.ini and
 # workflows/main.yml, and the `Programming Language ::` specifiers in setup.py
 PYTHONS = {
-    "3.7": "3.7.15",
-    "3.8": "3.8.15",
-    "3.9": "3.9.15",
-    "3.10": "3.10.8",
-    "3.11": "3.11.0",
+    "3.7": "3.7.16",
+    "3.8": "3.8.16",
+    "3.9": "3.9.16",
+    "3.10": "3.10.9",
+    "3.11": "3.11.1",
     "3.12": "3.12-dev",
     "pypy3.7": "pypy3.7-7.3.9",
-    "pypy3.8": "pypy3.8-7.3.9",
-    "pypy3.9": "pypy3.9-7.3.9",
+    "pypy3.8": "pypy3.8-7.3.11",
+    "pypy3.9": "pypy3.9-7.3.11",
 }
 ci_version = "3.10"  # Keep this in sync with GH Actions main.yml and .readthedocs.yml
 
diff --git a/tooling/src/hypothesistooling/projects/hypothesispython.py b/tooling/src/hypothesistooling/projects/hypothesispython.py
index 8a1452021..5a8194d00 100644
--- a/tooling/src/hypothesistooling/projects/hypothesispython.py
+++ b/tooling/src/hypothesistooling/projects/hypothesispython.py
@@ -29,6 +29,9 @@ BASE_DIR = HYPOTHESIS_PYTHON
 
 PYTHON_SRC = os.path.join(HYPOTHESIS_PYTHON, "src")
 PYTHON_TESTS = os.path.join(HYPOTHESIS_PYTHON, "tests")
+DOMAINS_LIST = os.path.join(
+    PYTHON_SRC, "hypothesis", "vendor", "tlds-alpha-by-domain.txt"
+)
 
 RELEASE_FILE = os.path.join(HYPOTHESIS_PYTHON, "RELEASE.rst")
 
@@ -234,3 +237,16 @@ def latest_version():
 
 def tag_name():
     return PYTHON_TAG_PREFIX + __version__
+
+
+def get_autoupdate_message(domainlist_changed: bool) -> str:
+    if domainlist_changed:
+        return (
+            "This patch updates our vendored `list of top-level domains "
+            "<https://www.iana.org/domains/root/db>`__,\nwhich is used by the "
+            "provisional :func:`~hypothesis.provisional.domains` strategy.\n"
+        )
+    return (
+        "This patch updates our autoformatting tools, "
+        "improving our code style without any API changes."
+    )
diff --git a/whole-repo-tests/test_release_files.py b/whole-repo-tests/test_release_files.py
index 95bbb3e83..ccd812b9c 100644
--- a/whole-repo-tests/test_release_files.py
+++ b/whole-repo-tests/test_release_files.py
@@ -29,6 +29,8 @@ def test_release_file_exists_and_is_valid(project):
 def test_release_file_has_no_merge_conflicts():
     _, message = rm.parse_release_file(hp.RELEASE_FILE)
     assert "<<<" not in message, "Merge conflict in RELEASE.rst"
+    if message in {hp.get_autoupdate_message(x).strip() for x in (True, False)}:
+        return
     _, *recent_changes, _ = hp.CHANGELOG_ANCHOR.split(hp.changelog(), maxsplit=12)
     for entry in recent_changes:
         _, version, old_msg = (x.strip() for x in hp.CHANGELOG_BORDER.split(entry))

test_patch: diff --git a/hypothesis-python/tests/cover/test_compat.py b/hypothesis-python/tests/cover/test_compat.py
index 90f4d1a2e..832cc1082 100644
--- a/hypothesis-python/tests/cover/test_compat.py
+++ b/hypothesis-python/tests/cover/test_compat.py
@@ -10,6 +10,7 @@
 
 import math
 from dataclasses import dataclass
+from functools import partial
 from inspect import Parameter, Signature, signature
 from typing import ForwardRef, Optional, Union
 
@@ -88,3 +89,20 @@ Bar.__signature__ = signature(Bar).replace(  # type: ignore
 def test_resolve_fwd_refs(obj, expected):
     # See: https://github.com/HypothesisWorks/hypothesis/issues/3519
     assert get_type_hints(obj)["x"] == expected
+
+
+def func(a, b: int, *c: str, d: int = None):
+    pass
+
+
+@pytest.mark.parametrize(
+    "pf, names",
+    [
+        (partial(func, 1), "b c d"),
+        (partial(func, 1, 2), "c d"),
+        (partial(func, 1, 2, 3, 4, 5), "c d"),  # varargs don't fill
+        (partial(func, 1, 2, 3, d=4), "c d"),  # kwonly args just get new defaults
+    ],
+)
+def test_get_hints_through_partial(pf, names):
+    assert set(get_type_hints(pf)) == set(names.split())
diff --git a/hypothesis-python/tests/cover/test_falsifying_example_output.py b/hypothesis-python/tests/cover/test_falsifying_example_output.py
index 6d942aa1b..95929a08a 100644
--- a/hypothesis-python/tests/cover/test_falsifying_example_output.py
+++ b/hypothesis-python/tests/cover/test_falsifying_example_output.py
@@ -12,12 +12,6 @@ import pytest
 
 from hypothesis import Phase, example, given, settings, strategies as st
 
-OUTPUT_NO_BREAK = """
-Falsifying explicit example: test(
-    x={0!r}, y={0!r},
-)
-"""
-
 OUTPUT_WITH_BREAK = """
 Falsifying explicit example: test(
     x={0!r},
@@ -26,9 +20,9 @@ Falsifying explicit example: test(
 """
 
 
-@pytest.mark.parametrize("line_break,input", [(False, "0" * 10), (True, "0" * 100)])
-def test_inserts_line_breaks_only_at_appropriate_lengths(line_break, input):
-    @example(input, input)
+@pytest.mark.parametrize("n", [10, 100])
+def test_inserts_line_breaks_only_at_appropriate_lengths(n):
+    @example("0" * n, "0" * n)
     @given(st.text(), st.text())
     def test(x, y):
         assert x < y
@@ -36,8 +30,7 @@ def test_inserts_line_breaks_only_at_appropriate_lengths(line_break, input):
     with pytest.raises(AssertionError) as err:
         test()
 
-    expected = (OUTPUT_WITH_BREAK if line_break else OUTPUT_NO_BREAK).format(input)
-    assert expected.strip() == "\n".join(err.value.__notes__)
+    assert OUTPUT_WITH_BREAK.format("0" * n).strip() == "\n".join(err.value.__notes__)
 
 
 @given(kw=st.none())
@@ -61,4 +54,4 @@ def test_vararg_output(fn):
     with pytest.raises(AssertionError) as err:
         fn(1, 2, 3)
 
-    assert "1, 2, 3" in "\n".join(err.value.__notes__)
+    assert "1,\n    2,\n    3,\n" in "\n".join(err.value.__notes__)
diff --git a/hypothesis-python/tests/cover/test_pretty.py b/hypothesis-python/tests/cover/test_pretty.py
index 86359362f..568f025e3 100644
--- a/hypothesis-python/tests/cover/test_pretty.py
+++ b/hypothesis-python/tests/cover/test_pretty.py
@@ -49,7 +49,6 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 import re
 from collections import Counter, OrderedDict, defaultdict, deque
-from io import StringIO
 
 import pytest
 
@@ -380,12 +379,10 @@ def test_basic_class():
 
     type_pprint_wrapper.called = False
 
-    stream = StringIO()
-    printer = pretty.RepresentationPrinter(stream)
+    printer = pretty.RepresentationPrinter()
     printer.type_pprinters[type] = type_pprint_wrapper
     printer.pretty(MyObj)
-    printer.flush()
-    output = stream.getvalue()
+    output = printer.getvalue()
 
     assert output == f"{__name__}.MyObj"
     assert type_pprint_wrapper.called
@@ -576,26 +573,6 @@ def test_re_evals():
         assert r.pattern == r2.pattern and r.flags == r2.flags
 
 
-class CustomStuff:
-    def __init__(self):
-        self.hi = 1
-        self.bye = "fish"
-        self.spoon = self
-
-    @property
-    def oops(self):
-        raise AttributeError("Nope")
-
-    def squirrels(self):
-        pass
-
-
-def test_custom():
-    assert "bye" not in pretty.pretty(CustomStuff())
-    assert "bye=" in pretty.pretty(CustomStuff(), verbose=True)
-    assert "squirrels" not in pretty.pretty(CustomStuff(), verbose=True)
-
-
 def test_print_builtin_function():
     assert pretty.pretty(abs) == "abs"
 
@@ -604,19 +581,8 @@ def test_pretty_function():
     assert pretty.pretty(test_pretty_function) == "test_pretty_function"
 
 
-def test_empty_printer():
-    printer = pretty.RepresentationPrinter(
-        StringIO(),
-        singleton_pprinters={},
-        type_pprinters={int: pretty._repr_pprint, list: pretty._repr_pprint},
-        deferred_pprinters={},
-    )
-    printer.pretty([1, 2, 3])
-    assert printer.output.getvalue() == "[1, 2, 3]"
-
-
 def test_breakable_at_group_boundary():
-    assert "\n" in pretty.pretty([[], "000000"], max_width=5)
+    assert "\n" in pretty.pretty([[], "0" * 80])
 
 
 @pytest.mark.parametrize(
@@ -631,3 +597,19 @@ def test_breakable_at_group_boundary():
 )
 def test_nan_reprs(obj, rep):
     assert pretty.pretty(obj) == rep
+
+
+def _repr_call(*args, **kwargs):
+    p = pretty.RepresentationPrinter()
+    p.repr_call(*args, **kwargs)
+    return p.getvalue()
+
+
+@pytest.mark.parametrize("func_name", ["f", "lambda: ...", "lambda *args: ..."])
+def test_repr_call(func_name):
+    fn = f"({func_name})" if func_name.startswith(("lambda:", "lambda ")) else func_name
+    aas = "a" * 100
+    assert _repr_call(func_name, (1, 2), {}) == f"{fn}(1, 2)"
+    assert _repr_call(func_name, (aas,), {}) == f"{fn}(\n    {aas!r},\n)"
+    assert _repr_call(func_name, (), {"a": 1, "b": 2}) == f"{fn}(a=1, b=2)"
+    assert _repr_call(func_name, (), {"x": aas}) == f"{fn}(\n    x={aas!r},\n)"
diff --git a/hypothesis-python/tests/cover/test_randoms.py b/hypothesis-python/tests/cover/test_randoms.py
index 4b4db455e..ed7aec51f 100644
--- a/hypothesis-python/tests/cover/test_randoms.py
+++ b/hypothesis-python/tests/cover/test_randoms.py
@@ -50,6 +50,7 @@ def define_method_strategy(name, **kwargs):
 
 
 define_method_strategy("betavariate", alpha=beta_param, beta=beta_param)
+define_method_strategy("binomialvariate", n=st.integers(min_value=1), p=st.floats(0, 1))
 define_method_strategy("gammavariate", alpha=beta_param, beta=beta_param)
 define_method_strategy("weibullvariate", alpha=beta_param, beta=beta_param)
 define_method_strategy("choice", seq=seq_param)
diff --git a/hypothesis-python/tests/cover/test_reflection.py b/hypothesis-python/tests/cover/test_reflection.py
index b4639494d..9d94d19bb 100644
--- a/hypothesis-python/tests/cover/test_reflection.py
+++ b/hypothesis-python/tests/cover/test_reflection.py
@@ -13,6 +13,7 @@ from copy import deepcopy
 from datetime import time
 from functools import partial, wraps
 from inspect import Parameter, Signature, signature
+from textwrap import dedent
 from unittest.mock import MagicMock, Mock, NonCallableMagicMock, NonCallableMock
 
 import pytest
@@ -651,3 +652,61 @@ def test_param_called_within_defaults_on_error():
     # Create a function object for which we cannot retrieve the source.
     f = compile("lambda: ...", "_.py", "eval")
     assert is_first_param_referenced_in_function(f)
+
+
+def _prep_source(*pairs):
+    return [
+        pytest.param(dedent(x).strip(), dedent(y).strip().encode(), id=f"case-{i}")
+        for i, (x, y) in enumerate(pairs)
+    ]
+
+
+@pytest.mark.parametrize(
+    "src, clean",
+    _prep_source(
+        ("", ""),
+        ("def test(): pass", "def test(): pass"),
+        ("def invalid syntax", "def invalid syntax"),
+        ("def also invalid(", "def also invalid("),
+        (
+            """
+            @example(1)
+            @given(st.integers())
+            def test(x):
+                # line comment
+                assert x  # end-of-line comment
+
+
+                "Had some blank lines above"
+            """,
+            """
+            def test(x):
+                assert x
+                "Had some blank lines above"
+            """,
+        ),
+        (
+            """
+            def      \\
+                f(): pass
+            """,
+            """
+            def\\
+                f(): pass
+            """,
+        ),
+        (
+            """
+            @dec
+            async def f():
+                pass
+            """,
+            """
+            async def f():
+                pass
+            """,
+        ),
+    ),
+)
+def test_clean_source(src, clean):
+    assert reflection._clean_source(src).splitlines() == clean.splitlines()
diff --git a/hypothesis-python/tests/numpy/test_gen_data.py b/hypothesis-python/tests/numpy/test_gen_data.py
index 554efc1df..795ffb93a 100644
--- a/hypothesis-python/tests/numpy/test_gen_data.py
+++ b/hypothesis-python/tests/numpy/test_gen_data.py
@@ -340,8 +340,12 @@ def test_may_not_fill_with_non_nan_when_unique_is_set_and_type_is_not_number(arr
     pass
 
 
+np_version = tuple(int(x) for x in np.__version__.split(".")[:2])
+
+
 @pytest.mark.parametrize("fill", [False, True])
-@fails_with(InvalidArgument)
+# Overflowing elements deprecated upstream in Numpy 1.24 :-)
+@fails_with(InvalidArgument if np_version < (1, 24) else DeprecationWarning)
 @given(st.data())
 def test_overflowing_integers_are_deprecated(fill, data):
     kw = {"elements": st.just(300)}
@@ -372,7 +376,12 @@ def test_unrepresentable_elements_are_deprecated(fill, dtype, strat, data):
         kw = {"elements": st.nothing(), "fill": strat}
     else:
         kw = {"elements": strat}
-    arr = data.draw(nps.arrays(dtype=dtype, shape=(1,), **kw))
+    try:
+        arr = data.draw(nps.arrays(dtype=dtype, shape=(1,), **kw))
+    except RuntimeWarning:
+        assert np_version >= (1, 24), "New overflow-on-cast detection"
+        raise InvalidArgument("so the test passes") from None
+
     try:
         # This is a float or complex number, and has overflowed to infinity,
         # triggering our deprecation for overflow.
diff --git a/hypothesis-python/tests/pytest/test_capture.py b/hypothesis-python/tests/pytest/test_capture.py
index 33688aab8..af1121aec 100644
--- a/hypothesis-python/tests/pytest/test_capture.py
+++ b/hypothesis-python/tests/pytest/test_capture.py
@@ -12,7 +12,7 @@ import sys
 
 import pytest
 
-from hypothesis.internal.compat import PYPY, WINDOWS, escape_unicode_characters
+from hypothesis.internal.compat import WINDOWS, escape_unicode_characters
 
 pytest_plugins = "pytester"
 
@@ -102,8 +102,7 @@ def test_healthcheck_traceback_is_hidden(testdir):
     timeout_token = ": FailedHealthCheck"
     def_line = get_line_num(def_token, result)
     timeout_line = get_line_num(timeout_token, result)
-    seven_min = (3, 9) if PYPY else (3, 8)
-    expected = 6 if sys.version_info[:2] < seven_min else 7
+    expected = 6 if sys.version_info[:2] < (3, 8) else 7
     assert timeout_line - def_line == expected
 
 
diff --git a/hypothesis-python/tests/quality/test_float_shrinking.py b/hypothesis-python/tests/quality/test_float_shrinking.py
index 3e545dc42..082942ed6 100644
--- a/hypothesis-python/tests/quality/test_float_shrinking.py
+++ b/hypothesis-python/tests/quality/test_float_shrinking.py
@@ -13,7 +13,6 @@ import pytest
 from hypothesis import (
     HealthCheck,
     Verbosity,
-    assume,
     example,
     given,
     settings,
@@ -42,7 +41,10 @@ def test_can_shrink_in_variable_sized_context(n):
 @given(st.floats(min_value=0, allow_infinity=False, allow_nan=False))
 @settings(deadline=None, suppress_health_check=HealthCheck.all())
 def test_shrinks_downwards_to_integers(f):
-    g = minimal(st.floats(), lambda x: x >= f, settings(verbosity=Verbosity.quiet))
+    g = minimal(
+        st.floats().filter(lambda x: x >= f),
+        settings=settings(verbosity=Verbosity.quiet, max_examples=10**6),
+    )
     assert g == ceil(f)
 
 
@@ -51,8 +53,7 @@ def test_shrinks_downwards_to_integers(f):
 @settings(deadline=None, suppress_health_check=HealthCheck.all(), max_examples=10)
 def test_shrinks_downwards_to_integers_when_fractional(b):
     g = minimal(
-        st.floats(),
-        lambda x: assume((0 < x < (2**53)) and int(x) != x) and x >= b,
+        st.floats().filter(lambda x: b < x < 2**53 and int(x) != x),
         settings=settings(verbosity=Verbosity.quiet, max_examples=10**6),
     )
     assert g == b + 0.5


----

problem_statement: Explicit support for special-use domains.
Hypothesisâ€™ [`emails()`](https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.strategies.emails) strategy generated an email address `0@a.arpa` which was rejected by the [email-validator](https://github.com/JoshData/python-email-validator) package:
```python
>>> import email_validator
>>> email_validator.validate_email('0@a.arpa')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/.../.venv/lib/python3.9/site-packages/email_validator/__init__.py", line 319, in validate_email
    domain_part_info = validate_email_domain_part(parts[1], test_environment=test_environment, globally_deliverable=globally_deliverable)
  File "/.../.venv/lib/python3.9/site-packages/email_validator/__init__.py", line 580, in validate_email_domain_part
    raise EmailSyntaxError("The domain name %s is a special-use or reserved name that cannot be used with email." % domain_i18n)
email_validator.EmailSyntaxError: The domain name a.arpa is a special-use or reserved name that cannot be used with email.
```

The email-validator package defines â€œspecial-use or reserved namesâ€ [here](https://github.com/JoshData/python-email-validator/blob/364d2d189e28b334437dcdbf554a484d07977a9b/email_validator/__init__.py#L19-L82).

Now, Hypothesis builds email addresses using the internal [`domains()`](https://hypothesis.readthedocs.io/en/latest/data.html#hypothesis.provisional.domains) strategy:

https://github.com/HypothesisWorks/hypothesis/blob/48165d90f6afc1c0cd618ae0659f02a88b579f8b/hypothesis-python/src/hypothesis/strategies/_internal/core.py#L2029-L2031

which doesnâ€™t have explicit support to filter out or use these special-use domain names.

I think adding an optional kwarg `allow_special_use=True` to the `emails()` strategy which then propagates to the [`DomainNameStrategy` class](https://github.com/HypothesisWorks/hypothesis/blob/48165d90f6afc1c0cd618ae0659f02a88b579f8b/hypothesis-python/src/hypothesis/provisional.py#L47-L128) would be useful. Happy to supply a PR?
date: 2023-03-15
patch: diff --git a/build.sh b/build.sh
index 2d2920bc3..1114e9c51 100755
--- a/build.sh
+++ b/build.sh
@@ -23,8 +23,8 @@ if [ -n "${GITHUB_ACTIONS-}" ] || [ -n "${CODESPACES-}" ] ; then
 else
     # Otherwise, we install it from scratch
     # NOTE: tooling keeps this version in sync with ci_version in tooling
-    "$SCRIPTS/ensure-python.sh" 3.10.9
-    PYTHON=$(pythonloc 3.10.9)/bin/python
+    "$SCRIPTS/ensure-python.sh" 3.10.10
+    PYTHON=$(pythonloc 3.10.10)/bin/python
 fi
 
 TOOL_REQUIREMENTS="$ROOT/requirements/tools.txt"
diff --git a/hypothesis-python/examples/test_basic.py b/hypothesis-python/examples/test_basic.py
new file mode 100644
index 000000000..3e3bc762d
--- /dev/null
+++ b/hypothesis-python/examples/test_basic.py
@@ -0,0 +1,34 @@
+# 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 given, strategies as st
+
+
+# Our tested class
+class Product:
+    def __init__(self, price: float) -> None:
+        self.price: float = price
+
+    def get_discount_price(self, discount_percentage: float):
+        return self.price * (discount_percentage / 100)
+
+
+# The @given decorater generates examples for us!
+@given(
+    price=st.floats(min_value=0, allow_nan=False, allow_infinity=False),
+    discount_percentage=st.floats(
+        min_value=0, max_value=100, allow_nan=False, allow_infinity=False
+    ),
+)
+def test_a_discounted_price_is_not_higher_than_the_original_price(
+    price, discount_percentage
+):
+    product = Product(price)
+    assert product.get_discount_price(discount_percentage) <= product.price
diff --git a/hypothesis-python/scripts/basic-test.sh b/hypothesis-python/scripts/basic-test.sh
index a9851d9b0..0d837aca9 100755
--- a/hypothesis-python/scripts/basic-test.sh
+++ b/hypothesis-python/scripts/basic-test.sh
@@ -45,7 +45,7 @@ pip install "$(grep 'lark==' ../requirements/coverage.txt)"
 $PYTEST tests/lark/
 pip uninstall -y lark
 
-if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel == \'final\' and platform.python_implementation() != "PyPy")')" = "True" ] ; then
+if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel == \'final\' and platform.python_implementation() not in ("PyPy", "GraalVM"))')" = "True" ] ; then
   pip install ".[codemods,cli]"
   $PYTEST tests/codemods/
   pip uninstall -y libcst click
@@ -75,16 +75,19 @@ PYTHONOPTIMIZE=2 $PYTEST \
     -W'ignore:Module already imported so cannot be rewritten:pytest.PytestAssertRewriteWarning' \
     tests/cover/test_testdecorators.py
 
-if [ "$(python -c 'import platform; print(platform.python_implementation())')" != "PyPy" ]; then
-  pip install .[django]
-  HYPOTHESIS_DJANGO_USETZ=TRUE python -m tests.django.manage test tests.django
-  HYPOTHESIS_DJANGO_USETZ=FALSE python -m tests.django.manage test tests.django
-  pip uninstall -y django pytz
+case "$(python -c 'import platform; print(platform.python_implementation())')" in
+  PyPy|GraalVM)
+    ;;
+  *)
+    pip install .[django]
+    HYPOTHESIS_DJANGO_USETZ=TRUE python -m tests.django.manage test tests.django
+    HYPOTHESIS_DJANGO_USETZ=FALSE python -m tests.django.manage test tests.django
+    pip uninstall -y django pytz
 
-  pip install "$(grep 'numpy==' ../requirements/coverage.txt)"
-  $PYTEST tests/array_api
-  $PYTEST tests/numpy
+    pip install "$(grep 'numpy==' ../requirements/coverage.txt)"
+    $PYTEST tests/array_api
+    $PYTEST tests/numpy
 
-  pip install "$(grep 'pandas==' ../requirements/coverage.txt)"
-  $PYTEST tests/pandas
-fi
+    pip install "$(grep 'pandas==' ../requirements/coverage.txt)"
+    $PYTEST tests/pandas
+esac
diff --git a/hypothesis-python/scripts/other-tests.sh b/hypothesis-python/scripts/other-tests.sh
index 3cc98c82e..13c2dde82 100644
--- a/hypothesis-python/scripts/other-tests.sh
+++ b/hypothesis-python/scripts/other-tests.sh
@@ -43,7 +43,7 @@ pip install "$(grep 'lark==' ../requirements/coverage.txt)"
 $PYTEST tests/lark/
 pip uninstall -y lark
 
-if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel == \'final\' and platform.python_implementation() != "PyPy")')" = "True" ] ; then
+if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel == \'final\' and platform.python_implementation() not in ("PyPy", "GraalVM"))')" = "True" ] ; then
   pip install ".[codemods,cli]"
   $PYTEST tests/codemods/
   pip uninstall -y libcst click
@@ -55,10 +55,13 @@ if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel ==
     pip install "$(grep 'numpy==' ../requirements/coverage.txt)"
   fi
 
-  if [ "$(python -c 'import platform; print(platform.python_implementation())')" != "PyPy" ]; then\
-    $PYTEST tests/array_api
-    $PYTEST tests/numpy
-  fi
+  case "$(python -c 'import platform; print(platform.python_implementation())')" in
+    PyPy|GraalVM)
+      ;;
+    *)
+      $PYTEST tests/array_api
+      $PYTEST tests/numpy
+  esac
 
   pip install "$(grep 'black==' ../requirements/coverage.txt)"
   $PYTEST tests/ghostwriter/
diff --git a/hypothesis-python/src/hypothesis/_settings.py b/hypothesis-python/src/hypothesis/_settings.py
index 4743a666f..54ec5f11e 100644
--- a/hypothesis-python/src/hypothesis/_settings.py
+++ b/hypothesis-python/src/hypothesis/_settings.py
@@ -19,7 +19,7 @@ import datetime
 import inspect
 import os
 import warnings
-from enum import Enum, IntEnum, unique
+from enum import Enum, EnumMeta, IntEnum, unique
 from typing import TYPE_CHECKING, Any, Collection, Dict, List, Optional, TypeVar, Union
 
 import attr
@@ -445,8 +445,14 @@ class Phase(IntEnum):
         return f"Phase.{self.name}"
 
 
+class HealthCheckMeta(EnumMeta):
+    def __iter__(self):
+        deprecated = (HealthCheck.return_value, HealthCheck.not_a_test_method)
+        return iter(x for x in super().__iter__() if x not in deprecated)
+
+
 @unique
-class HealthCheck(Enum):
+class HealthCheck(Enum, metaclass=HealthCheckMeta):
     """Arguments for :attr:`~hypothesis.settings.suppress_health_check`.
 
     Each member of this enum is a type of health check to suppress.
@@ -457,6 +463,8 @@ class HealthCheck(Enum):
 
     @classmethod
     def all(cls) -> List["HealthCheck"]:
+        # Skipping of deprecated attributes is handled in HealthCheckMeta.__iter__
+        # TODO: note_deprecation() and write a codemod for HC.all() -> list(HC)
         return list(HealthCheck)
 
     data_too_large = 1
@@ -479,15 +487,14 @@ class HealthCheck(Enum):
     testing."""
 
     return_value = 5
-    """Checks if your tests return a non-None value (which will be ignored and
-    is unlikely to do what you want)."""
+    """Deprecated; we always error if a test returns a non-None value."""
 
     large_base_example = 7
     """Checks if the natural example to shrink towards is very large."""
 
     not_a_test_method = 8
-    """Checks if :func:`@given <hypothesis.given>` has been applied to a
-    method defined by :class:`python:unittest.TestCase` (i.e. not a test)."""
+    """Deprecated; we always error if :func:`@given <hypothesis.given>` is applied
+    to a method defined by :class:`python:unittest.TestCase` (i.e. not a test)."""
 
     function_scoped_fixture = 9
     """Checks if :func:`@given <hypothesis.given>` has been applied to a test
@@ -585,6 +592,12 @@ def validate_health_check_suppressions(suppressions):
                 f"Non-HealthCheck value {s!r} of type {type(s).__name__} "
                 "is invalid in suppress_health_check."
             )
+        if s in (HealthCheck.return_value, HealthCheck.not_a_test_method):
+            note_deprecation(
+                f"The {s.name} health check is deprecated, because this is always an error.",
+                since="2023-03-15",
+                has_codemod=False,
+            )
     return suppressions
 
 
@@ -673,7 +686,7 @@ settings.lock_further_definitions()
 def note_deprecation(message: str, *, since: str, has_codemod: bool) -> None:
     if since != "RELEASEDAY":
         date = datetime.datetime.strptime(since, "%Y-%m-%d").date()
-        assert datetime.date(2016, 1, 1) <= date
+        assert datetime.date(2021, 1, 1) <= date
     if has_codemod:
         message += (
             "\n    The `hypothesis codemod` command-line tool can automatically "
diff --git a/hypothesis-python/src/hypothesis/control.py b/hypothesis-python/src/hypothesis/control.py
index 1ae300e11..5ac078298 100644
--- a/hypothesis-python/src/hypothesis/control.py
+++ b/hypothesis-python/src/hypothesis/control.py
@@ -9,15 +9,18 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 import math
-from typing import NoReturn, Optional, Union
+from collections import defaultdict
+from typing import NoReturn, Union
 
 from hypothesis import Verbosity, settings
 from hypothesis.errors import InvalidArgument, UnsatisfiedAssumption
 from hypothesis.internal.compat import BaseExceptionGroup
 from hypothesis.internal.conjecture.data import ConjectureData
+from hypothesis.internal.reflection import get_pretty_function_description
 from hypothesis.internal.validation import check_type
 from hypothesis.reporting import report, verbose_report
 from hypothesis.utils.dynamicvariables import DynamicVariable
+from hypothesis.vendor.pretty import IDKey
 
 
 def reject() -> NoReturn:
@@ -51,7 +54,7 @@ def currently_in_test_context() -> bool:
     return _current_build_context.value is not None
 
 
-def current_build_context() -> "Optional[BuildContext]":
+def current_build_context() -> "BuildContext":
     context = _current_build_context.value
     if context is None:
         raise InvalidArgument("No build context registered")
@@ -66,6 +69,16 @@ class BuildContext:
         self.is_final = is_final
         self.close_on_capture = close_on_capture
         self.close_on_del = False
+        # Use defaultdict(list) here to handle the possibility of having multiple
+        # functions registered for the same object (due to caching, small ints, etc).
+        # The printer will discard duplicates which return different representations.
+        self.known_object_printers = defaultdict(list)
+
+    def record_call(self, obj, func, a, kw):
+        name = get_pretty_function_description(func)
+        self.known_object_printers[IDKey(obj)].append(
+            lambda obj, p, cycle: p.text("<...>") if cycle else p.repr_call(name, a, kw)
+        )
 
     def __enter__(self):
         self.assign_variable = _current_build_context.with_value(self)
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
index 1b0a7959d..049e16d3c 100644
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -34,6 +34,8 @@ from typing import (
     Hashable,
     List,
     Optional,
+    Tuple,
+    Type,
     TypeVar,
     Union,
     overload,
@@ -92,10 +94,12 @@ from hypothesis.internal.reflection import (
     get_signature,
     impersonate,
     is_mock,
+    nicerepr,
     proxies,
     repr_call,
 )
 from hypothesis.internal.scrutineer import Tracer, explanatory_lines
+from hypothesis.internal.validation import check_type
 from hypothesis.reporting import (
     current_verbosity,
     report,
@@ -134,6 +138,9 @@ _hypothesis_global_random = None
 class Example:
     args = attr.ib()
     kwargs = attr.ib()
+    # Plus two optional arguments for .xfail()
+    raises = attr.ib(default=None)
+    reason = attr.ib(default=None)
 
 
 class example:
@@ -156,6 +163,51 @@ class example:
         test.hypothesis_explicit_examples.append(self._this_example)  # type: ignore
         return test
 
+    def xfail(
+        self,
+        condition: bool = True,
+        *,
+        reason: str = "",
+        raises: Union[
+            Type[BaseException], Tuple[Type[BaseException], ...]
+        ] = BaseException,
+    ) -> "example":
+        """Mark this example as an expected failure, like pytest.mark.xfail().
+
+        Expected-failing examples allow you to check that your test does fail on
+        some examples, and therefore build confidence that *passing* tests are
+        because your code is working, not because the test is missing something.
+
+        .. code-block:: python
+
+            @example(...).xfail()
+            @example(...).xfail(reason="Prices must be non-negative")
+            @example(...).xfail(raises=(KeyError, ValueError))
+            @example(...).xfail(sys.version_info[:2] >= (3, 9), reason="needs py39+")
+            @example(...).xfail(condition=sys.platform != "linux", raises=OSError)
+            def test(x):
+                pass
+        """
+        check_type(bool, condition, "condition")
+        check_type(str, reason, "reason")
+        if not (
+            isinstance(raises, type) and issubclass(raises, BaseException)
+        ) and not (
+            isinstance(raises, tuple)
+            and raises  # () -> expected to fail with no error, which is impossible
+            and all(
+                isinstance(r, type) and issubclass(r, BaseException) for r in raises
+            )
+        ):
+            raise InvalidArgument(
+                f"raises={raises!r} must be an exception type or tuple of exception types"
+            )
+        if condition:
+            self._this_example = attr.evolve(
+                self._this_example, raises=raises, reason=reason
+            )
+        return self
+
     def via(self, *whence: str) -> "example":
         """Attach a machine-readable label noting whence this example came.
 
@@ -400,9 +452,7 @@ class ArtificialDataForExample(ConjectureData):
         assert self.__draws == 0
         self.__draws += 1
         # The main strategy for given is always a tuples strategy that returns
-        # first positional arguments then keyword arguments. When building this
-        # object already converted all positional arguments to keyword arguments,
-        # so this is the correct format to return.
+        # first positional arguments then keyword arguments.
         return self.__args, self.__kwargs
 
 
@@ -414,6 +464,7 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
     ]
 
     for example in reversed(getattr(wrapped_test, "hypothesis_explicit_examples", ())):
+        assert isinstance(example, Example)
         # All of this validation is to check that @example() got "the same" arguments
         # as @given, i.e. corresponding to the same parameters, even though they might
         # be any mixture of positional and keyword arguments.
@@ -455,12 +506,47 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
         with local_settings(state.settings):
             fragments_reported = []
             try:
+                adata = ArtificialDataForExample(arguments, example_kwargs)
+                bits = ", ".join(nicerepr(x) for x in arguments) + ", ".join(
+                    f"{k}={nicerepr(v)}" for k, v in example_kwargs.items()
+                )
                 with with_reporter(fragments_reported.append):
-                    state.execute_once(
-                        ArtificialDataForExample(arguments, example_kwargs),
-                        is_final=True,
-                        print_example=True,
-                    )
+                    if example.raises is None:
+                        state.execute_once(adata, is_final=True, print_example=True)
+                    else:
+                        # @example(...).xfail(...)
+                        try:
+                            state.execute_once(adata, is_final=True, print_example=True)
+                        except failure_exceptions_to_catch() as err:
+                            if not isinstance(err, example.raises):
+                                raise
+                        except example.raises as err:
+                            # We'd usually check this as early as possible, but it's
+                            # possible for failure_exceptions_to_catch() to grow when
+                            # e.g. pytest is imported between import- and test-time.
+                            raise InvalidArgument(
+                                f"@example({bits}) raised an expected {err!r}, "
+                                "but Hypothesis does not treat this as a test failure"
+                            ) from err
+                        else:
+                            # Unexpectedly passing; always raise an error in this case.
+                            reason = f" because {example.reason}" * bool(example.reason)
+                            if example.raises is BaseException:
+                                name = "exception"  # special-case no raises= arg
+                            elif not isinstance(example.raises, tuple):
+                                name = example.raises.__name__
+                            elif len(example.raises) == 1:
+                                name = example.raises[0].__name__
+                            else:
+                                name = (
+                                    ", ".join(ex.__name__ for ex in example.raises[:-1])
+                                    + f", or {example.raises[-1].__name__}"
+                                )
+                            vowel = name.upper()[0] in "AEIOU"
+                            raise AssertionError(
+                                f"Expected a{'n' * vowel} {name} from @example({bits})"
+                                f"{reason}, but no exception was raised."
+                            )
             except UnsatisfiedAssumption:
                 # Odd though it seems, we deliberately support explicit examples that
                 # are then rejected by a call to `assume()`.  As well as iterative
@@ -478,7 +564,7 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
                 # One user error - whether misunderstanding or typo - we've seen a few
                 # times is to pass strategies to @example() where values are expected.
                 # Checking is easy, and false-positives not much of a problem, so:
-                if any(
+                if isinstance(err, failure_exceptions_to_catch()) and any(
                     isinstance(arg, SearchStrategy)
                     for arg in example.args + tuple(example.kwargs.values())
                 ):
@@ -494,6 +580,7 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
                 if (
                     state.settings.report_multiple_bugs
                     and pytest_shows_exceptiongroups
+                    and isinstance(err, failure_exceptions_to_catch())
                     and not isinstance(err, skip_exceptions_to_reraise())
                 ):
                     continue
@@ -698,8 +785,7 @@ class StateForActualGivenExecution:
             # Set up dynamic context needed by a single test run.
             with local_settings(self.settings):
                 with deterministic_PRNG():
-                    with BuildContext(data, is_final=is_final):
-
+                    with BuildContext(data, is_final=is_final) as context:
                         # Generate all arguments to the test function.
                         args, kwargs = data.draw(self.search_strategy)
                         if expected_failure is not None:
@@ -709,7 +795,7 @@ class StateForActualGivenExecution:
                         if print_example or current_verbosity() >= Verbosity.verbose:
                             output = StringIO()
 
-                            printer = RepresentationPrinter(output)
+                            printer = RepresentationPrinter(output, context=context)
                             if print_example:
                                 printer.text("Falsifying example:")
                             else:
@@ -1312,9 +1398,9 @@ def given(
             if not (ran_explicit_examples or state.ever_executed):
                 raise SKIP_BECAUSE_NO_EXAMPLES
 
-        def _get_fuzz_target() -> Callable[
-            [Union[bytes, bytearray, memoryview, BinaryIO]], Optional[bytes]
-        ]:
+        def _get_fuzz_target() -> (
+            Callable[[Union[bytes, bytearray, memoryview, BinaryIO]], Optional[bytes]]
+        ):
             # Because fuzzing interfaces are very performance-sensitive, we use a
             # somewhat more complicated structure here.  `_get_fuzz_target()` is
             # called by the `HypothesisHandle.fuzz_one_input` property, allowing
@@ -1409,7 +1495,7 @@ def find(
     if settings is None:
         settings = Settings(max_examples=2000)
     settings = Settings(
-        settings, suppress_health_check=HealthCheck.all(), report_multiple_bugs=False
+        settings, suppress_health_check=list(HealthCheck), report_multiple_bugs=False
     )
 
     if database_key is None and settings.database is not None:
diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py
index 130959345..82f394011 100644
--- a/hypothesis-python/src/hypothesis/extra/array_api.py
+++ b/hypothesis-python/src/hypothesis/extra/array_api.py
@@ -69,10 +69,10 @@ __all__ = [
 ]
 
 
-RELEASED_VERSIONS = ("2021.12",)
+RELEASED_VERSIONS = ("2021.12", "2022.12")
 NOMINAL_VERSIONS = RELEASED_VERSIONS + ("draft",)
 assert sorted(NOMINAL_VERSIONS) == list(NOMINAL_VERSIONS)  # sanity check
-NominalVersion = Literal["2021.12", "draft"]
+NominalVersion = Literal["2021.12", "2022.12", "draft"]
 assert get_args(NominalVersion) == NOMINAL_VERSIONS  # sanity check
 
 
@@ -194,7 +194,7 @@ def _from_dtype(
     allow_subnormal: Optional[bool] = None,
     exclude_min: Optional[bool] = None,
     exclude_max: Optional[bool] = None,
-) -> st.SearchStrategy[Union[bool, int, float]]:
+) -> st.SearchStrategy[Union[bool, int, float, complex]]:
     """Return a strategy for any value of the given dtype.
 
     Values generated are of the Python scalar which is
@@ -297,31 +297,22 @@ def _from_dtype(
 
         return st.floats(width=finfo.bits, **kw)
     else:
-        # A less-inelegant solution to support complex dtypes exists, but as
-        # this is currently a draft feature, we might as well wait for
-        # discussion of complex inspection to resolve first - a better method
-        # might become available soon enough.
-        # See https://github.com/data-apis/array-api/issues/433
-        for attr in ["float32", "float64", "complex64"]:
-            if not hasattr(xp, attr):
-                raise NotImplementedError(
-                    f"Array module {xp.__name__} has no dtype {attr}, which is "
-                    "currently required for xps.from_dtype() to work with "
-                    "any complex dtype."
-                )
-        component_dtype = xp.float32 if dtype == xp.complex64 else xp.float64
-
-        floats = _from_dtype(
-            xp,
-            api_version,
-            component_dtype,
+        finfo = xp.finfo(dtype)
+        # See above comment on FTZ inference. We explicitly infer with a
+        # complex array, in case complex arrays have different FTZ behaviour
+        # than arrays of the respective composite float.
+        if allow_subnormal is None:
+            subnormal = next_down(finfo.smallest_normal, width=finfo.bits)
+            x = xp.asarray(complex(subnormal, subnormal), dtype=dtype)
+            builtin_x = complex(x)
+            allow_subnormal = builtin_x.real != 0 and builtin_x.imag != 0
+        return st.complex_numbers(
             allow_nan=allow_nan,
             allow_infinity=allow_infinity,
             allow_subnormal=allow_subnormal,
+            width=finfo.bits * 2,
         )
 
-        return st.builds(complex, floats, floats)  # type: ignore[arg-type]
-
 
 class ArrayStrategy(st.SearchStrategy):
     def __init__(
@@ -945,7 +936,7 @@ def make_strategies_namespace(
         allow_subnormal: Optional[bool] = None,
         exclude_min: Optional[bool] = None,
         exclude_max: Optional[bool] = None,
-    ) -> st.SearchStrategy[Union[bool, int, float]]:
+    ) -> st.SearchStrategy[Union[bool, int, float, complex]]:
         return _from_dtype(
             xp,
             api_version,  # type: ignore[arg-type]
@@ -959,6 +950,28 @@ def make_strategies_namespace(
             exclude_max=exclude_max,
         )
 
+    # torch.full() does not accept integers as the shape argument (n.b.
+    # technically "size" in torch), but such behaviour is expected in
+    # xps.arrays(), so we copy xp and patch in a working function.
+    if xp is sys.modules.get("torch", object()):
+
+        class PatchedArrayModule:
+            def __getattr__(self, attr):
+                return getattr(xp, attr)
+
+            @property
+            def __name__(self):
+                return "torch<modified>"
+
+            def full(self, shape, *a, **kw):
+                if isinstance(shape, int):
+                    shape = (shape,)
+                return xp.full(shape, *a, **kw)
+
+        arrays_xp = PatchedArrayModule()
+    else:
+        arrays_xp = xp
+
     @defines_strategy(force_reusable_values=True)
     def arrays(
         dtype: Union[
@@ -971,7 +984,7 @@ def make_strategies_namespace(
         unique: bool = False,
     ) -> st.SearchStrategy:
         return _arrays(
-            xp,
+            arrays_xp,
             api_version,  # type: ignore[arg-type]
             dtype,
             shape,
@@ -1128,7 +1141,7 @@ if np is not None:
 
     mock_xp = SimpleNamespace(
         __name__="mock",
-        __array_api_version__="2021.12",
+        __array_api_version__="2022.12",
         # Data types
         int8=np.int8,
         int16=np.int16,
diff --git a/hypothesis-python/src/hypothesis/extra/cli.py b/hypothesis-python/src/hypothesis/extra/cli.py
index ac530bce1..df908f638 100644
--- a/hypothesis-python/src/hypothesis/extra/cli.py
+++ b/hypothesis-python/src/hypothesis/extra/cli.py
@@ -269,7 +269,15 @@ else:
         multiple=True,
         help="dotted name of exception(s) to ignore",
     )
-    def write(func, writer, except_, style):  # noqa: D301  # \b disables autowrap
+    @click.option(
+        "--annotate/--no-annotate",
+        default=None,
+        help="force ghostwritten tests to be type-annotated (or not).  "
+        "By default, match the code to test.",
+    )
+    def write(
+        func, writer, except_, style, annotate
+    ):  # noqa: D301  # \b disables autowrap
         """`hypothesis write` writes property-based tests for you!
 
         Type annotations are helpful but not required for our advanced introspection
@@ -278,6 +286,7 @@ else:
         \b
             hypothesis write gzip
             hypothesis write numpy.matmul
+            hypothesis write pandas.from_dummies
             hypothesis write re.compile --except re.error
             hypothesis write --equivalent ast.literal_eval eval
             hypothesis write --roundtrip json.dumps json.loads
@@ -287,7 +296,7 @@ else:
         # NOTE: if you want to call this function from Python, look instead at the
         # ``hypothesis.extra.ghostwriter`` module.  Click-decorated functions have
         # a different calling convention, and raise SystemExit instead of returning.
-        kwargs = {"except_": except_ or (), "style": style}
+        kwargs = {"except_": except_ or (), "style": style, "annotate": annotate}
         if writer is None:
             writer = "magic"
         elif writer == "idempotent" and len(func) > 1:
diff --git a/hypothesis-python/src/hypothesis/extra/ghostwriter.py b/hypothesis-python/src/hypothesis/extra/ghostwriter.py
index d13fc1100..1ea7b760e 100644
--- a/hypothesis-python/src/hypothesis/extra/ghostwriter.py
+++ b/hypothesis-python/src/hypothesis/extra/ghostwriter.py
@@ -33,6 +33,7 @@ generally do their best to write you a useful test.  You can also use
 
           hypothesis write gzip
           hypothesis write numpy.matmul
+          hypothesis write pandas.from_dummies
           hypothesis write re.compile --except re.error
           hypothesis write --equivalent ast.literal_eval eval
           hypothesis write --roundtrip json.dumps json.loads
@@ -40,14 +41,16 @@ generally do their best to write you a useful test.  You can also use
           hypothesis write --binary-op operator.add
 
     Options:
-      --roundtrip                start by testing write/read or encode/decode!
-      --equivalent               very useful when optimising or refactoring code
-      --errors-equivalent        --equivalent, but also allows consistent errors
-      --idempotent               check that f(x) == f(f(x))
-      --binary-op                associativity, commutativity, identity element
-      --style [pytest|unittest]  pytest-style function, or unittest-style method?
-      -e, --except OBJ_NAME      dotted name of exception(s) to ignore
-      -h, --help                 Show this message and exit.
+      --roundtrip                 start by testing write/read or encode/decode!
+      --equivalent                very useful when optimising or refactoring code
+      --errors-equivalent         --equivalent, but also allows consistent errors
+      --idempotent                check that f(x) == f(f(x))
+      --binary-op                 associativity, commutativity, identity element
+      --style [pytest|unittest]   pytest-style function, or unittest-style method?
+      -e, --except OBJ_NAME       dotted name of exception(s) to ignore
+      --annotate / --no-annotate  force ghostwritten tests to be type-annotated
+                                  (or not).  By default, match the code to test.
+      -h, --help                  Show this message and exit.
 
 .. tip::
 
@@ -86,9 +89,13 @@ from typing import (
     TYPE_CHECKING,
     Any,
     Callable,
+    DefaultDict,
     Dict,
+    ForwardRef,
+    Iterable,
     List,
     Mapping,
+    NamedTuple,
     Optional,
     Set,
     Tuple,
@@ -101,7 +108,7 @@ import black
 
 from hypothesis import Verbosity, find, settings, strategies as st
 from hypothesis.errors import InvalidArgument
-from hypothesis.internal.compat import get_type_hints
+from hypothesis.internal.compat import get_args, get_origin, get_type_hints
 from hypothesis.internal.reflection import get_signature, is_mock
 from hypothesis.internal.validation import check_type
 from hypothesis.provisional import domains
@@ -134,7 +141,7 @@ IMPORT_SECTION = """
 
 TEMPLATE = """
 @given({given_args})
-def test_{test_kind}_{func_name}({arg_names}):
+def test_{test_kind}_{func_name}({arg_names}){return_annotation}:
 {test_body}
 """
 
@@ -438,7 +445,6 @@ def _guess_strategy_by_argname(name: str) -> st.SearchStrategy:
 
 def _get_params(func: Callable) -> Dict[str, inspect.Parameter]:
     """Get non-vararg parameters of `func` as an ordered dict."""
-    var_param_kinds = (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
     try:
         params = list(get_signature(func).parameters.values())
     except Exception:
@@ -486,6 +492,13 @@ def _get_params(func: Callable) -> Dict[str, inspect.Parameter]:
             # If we haven't managed to recover a signature through the tricks above,
             # we're out of ideas and should just re-raise the exception.
             raise
+    return _params_to_dict(params)
+
+
+def _params_to_dict(
+    params: Iterable[inspect.Parameter],
+) -> Dict[str, inspect.Parameter]:
+    var_param_kinds = (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
     return OrderedDict((p.name, p) for p in params if p.kind not in var_param_kinds)
 
 
@@ -584,8 +597,16 @@ def _imports_for_strategy(strategy):
     # If we have a lazy from_type strategy, because unwrapping it gives us an
     # error or invalid syntax, import that type and we're done.
     if isinstance(strategy, LazyStrategy):
-        if strategy.function is st.from_type:
-            return _imports_for_object(strategy._LazyStrategy__args[0])
+        if strategy.function.__name__ in (
+            st.from_type.__name__,
+            st.from_regex.__name__,
+        ):
+            return {
+                imp
+                for arg in set(strategy._LazyStrategy__args)
+                | set(strategy._LazyStrategy__kwargs.values())
+                for imp in _imports_for_object(arg)
+            }
         elif _get_module(strategy.function).startswith("hypothesis.extra."):
             return {(_get_module(strategy.function), strategy.function.__name__)}
             module = _get_module(strategy.function).replace("._array_helpers", ".numpy")
@@ -760,6 +781,7 @@ def _make_test_body(
     style: str,
     given_strategies: Optional[Mapping[str, Union[str, st.SearchStrategy]]] = None,
     imports: Optional[ImportSet] = None,
+    annotate: bool,
 ) -> Tuple[ImportSet, str]:
     # A set of modules to import - we might add to this later.  The import code
     # is written later, so we can have one import section for multiple magic()
@@ -791,12 +813,18 @@ def _make_test_body(
         test_body = f"{test_body}\n{assertions}"
 
     # Indent our test code to form the body of a function or method.
-    argnames = (["self"] if style == "unittest" else []) + list(given_strategies)
+    argnames = ["self"] if style == "unittest" else []
+    if annotate:
+        argnames.extend(_annotate_args(given_strategies, funcs, imports))
+    else:
+        argnames.extend(given_strategies)
+
     body = TEMPLATE.format(
         given_args=given_args,
         test_kind=ghost,
         func_name="_".join(_get_qualname(f).replace(".", "_") for f in funcs),
         arg_names=", ".join(argnames),
+        return_annotation=" -> None" if annotate else "",
         test_body=indent(test_body, prefix="    "),
     )
 
@@ -812,6 +840,177 @@ def _make_test_body(
     return imports, body
 
 
+def _annotate_args(
+    argnames: Iterable[str], funcs: Iterable[Callable], imports: ImportSet
+) -> Iterable[str]:
+    arg_parameters: DefaultDict[str, Set[Any]] = defaultdict(set)
+    for func in funcs:
+        try:
+            params = tuple(get_signature(func, eval_str=True).parameters.values())
+        except Exception:
+            # don't add parameters if the annotations could not be evaluated
+            pass
+        else:
+            for key, param in _params_to_dict(params).items():
+                if param.annotation != inspect.Parameter.empty:
+                    arg_parameters[key].add(param.annotation)
+
+    for argname in argnames:
+        parameters = arg_parameters.get(argname)
+        annotation = _parameters_to_annotation_name(parameters, imports)
+        if annotation is None:
+            yield argname
+        else:
+            yield f"{argname}: {annotation}"
+
+
+class _AnnotationData(NamedTuple):
+    type_name: str
+    imports: Set[str]
+
+
+def _parameters_to_annotation_name(
+    parameters: Optional[Iterable[Any]], imports: ImportSet
+) -> Optional[str]:
+    if parameters is None:
+        return None
+    annotations = tuple(
+        annotation
+        for annotation in map(_parameter_to_annotation, parameters)
+        if annotation is not None
+    )
+    if not annotations:
+        return None
+    if len(annotations) == 1:
+        type_name, new_imports = annotations[0]
+        imports.update(new_imports)
+        return type_name
+    joined = _join_generics(("typing.Union", {"typing"}), annotations)
+    if joined is None:
+        return None
+    imports.update(joined.imports)
+    return joined.type_name
+
+
+def _join_generics(
+    origin_type_data: Optional[Tuple[str, Set[str]]],
+    annotations: Iterable[Optional[_AnnotationData]],
+) -> Optional[_AnnotationData]:
+    if origin_type_data is None:
+        return None
+
+    # because typing.Optional is converted to a Union, it also contains None
+    # since typing.Optional only accepts one type variable, we need to remove it
+    if origin_type_data is not None and origin_type_data[0] == "typing.Optional":
+        annotations = (
+            annotation
+            for annotation in annotations
+            if annotation is None or annotation.type_name != "None"
+        )
+
+    origin_type, imports = origin_type_data
+    joined = _join_argument_annotations(annotations)
+    if joined is None or not joined[0]:
+        return None
+
+    arg_types, new_imports = joined
+    imports.update(new_imports)
+    return _AnnotationData("{}[{}]".format(origin_type, ", ".join(arg_types)), imports)
+
+
+def _join_argument_annotations(
+    annotations: Iterable[Optional[_AnnotationData]],
+) -> Optional[Tuple[List[str], Set[str]]]:
+    imports: Set[str] = set()
+    arg_types: List[str] = []
+
+    for annotation in annotations:
+        if annotation is None:
+            return None
+        arg_types.append(annotation.type_name)
+        imports.update(annotation.imports)
+
+    return arg_types, imports
+
+
+def _parameter_to_annotation(parameter: Any) -> Optional[_AnnotationData]:
+    # if a ForwardRef could not be resolved
+    if isinstance(parameter, str):
+        return None
+
+    if isinstance(parameter, ForwardRef):
+        forwarded_value = parameter.__forward_value__
+        if forwarded_value is None:
+            return None
+        return _parameter_to_annotation(forwarded_value)
+
+    # the arguments of Callable are in a list
+    if isinstance(parameter, list):
+        joined = _join_argument_annotations(
+            _parameter_to_annotation(param) for param in parameter
+        )
+        if joined is None:
+            return None
+        arg_type_names, new_imports = joined
+        return _AnnotationData("[{}]".format(", ".join(arg_type_names)), new_imports)
+
+    if isinstance(parameter, type):
+        if parameter.__module__ == "builtins":
+            return _AnnotationData(
+                "None" if parameter.__name__ == "NoneType" else parameter.__name__,
+                set(),
+            )
+
+        type_name = f"{parameter.__module__}.{parameter.__name__}"
+
+        # the types.UnionType does not support type arguments and needs to be translated
+        if type_name == "types.UnionType":
+            return _AnnotationData("typing.Union", {"typing"})
+    else:
+        if hasattr(parameter, "__module__") and hasattr(parameter, "__name__"):
+            type_name = f"{parameter.__module__}.{parameter.__name__}"
+        else:
+            type_name = str(parameter)
+
+    origin_type = get_origin(parameter)
+
+    # if not generic or no generic arguments
+    if origin_type is None or origin_type == parameter:
+        return _AnnotationData(type_name, set(type_name.rsplit(".", maxsplit=1)[:-1]))
+
+    arg_types = get_args(parameter)
+
+    # typing types get translated to classes that don't support generics
+    origin_annotation: Optional[_AnnotationData]
+    if type_name.startswith("typing."):
+        try:
+            new_type_name = type_name[: type_name.index("[")]
+        except ValueError:
+            new_type_name = type_name
+        origin_annotation = _AnnotationData(new_type_name, {"typing"})
+    else:
+        origin_annotation = _parameter_to_annotation(origin_type)
+
+    if arg_types:
+        return _join_generics(
+            origin_annotation,
+            (_parameter_to_annotation(arg_type) for arg_type in arg_types),
+        )
+    return origin_annotation
+
+
+def _are_annotations_used(*functions: Callable) -> bool:
+    for function in functions:
+        try:
+            params = get_signature(function).parameters.values()
+        except Exception:
+            pass
+        else:
+            if any(param.annotation != inspect.Parameter.empty for param in params):
+                return True
+    return False
+
+
 def _make_test(imports: ImportSet, body: str) -> str:
     # Discarding "builtins." and "__main__" probably isn't particularly useful
     # for user code, but important for making a good impression in demos.
@@ -880,6 +1079,7 @@ def magic(
     *modules_or_functions: Union[Callable, types.ModuleType],
     except_: Except = (),
     style: str = "pytest",
+    annotate: Optional[bool] = None,
 ) -> str:
     """Guess which ghostwriters to use, for a module or collection of functions.
 
@@ -952,6 +1152,9 @@ def magic(
             except (TypeError, ValueError):
                 pass
 
+    if annotate is None:
+        annotate = _are_annotations_used(*functions)
+
     imports = set()
     parts = []
 
@@ -984,7 +1187,11 @@ def magic(
                 for other in sorted(
                     n for n in by_name if n.split(".")[-1] == inverse_name
                 ):
-                    make_(_make_roundtrip_body, (by_name.pop(name), by_name.pop(other)))
+                    make_(
+                        _make_roundtrip_body,
+                        (by_name.pop(name), by_name.pop(other)),
+                        annotate=annotate,
+                    )
                     break
                 else:
                     try:
@@ -996,7 +1203,11 @@ def magic(
                     except Exception:
                         pass
                     else:
-                        make_(_make_roundtrip_body, (by_name.pop(name), other_func))
+                        make_(
+                            _make_roundtrip_body,
+                            (by_name.pop(name), other_func),
+                            annotate=annotate,
+                        )
 
     # Look for equivalent functions: same name, all required arguments of any can
     # be found in all signatures, and if all have return-type annotations they match.
@@ -1008,7 +1219,7 @@ def magic(
             sentinel = object()
             returns = {get_type_hints(f).get("return", sentinel) for f in group}
             if len(returns - {sentinel}) <= 1:
-                make_(_make_equiv_body, group)
+                make_(_make_equiv_body, group, annotate=annotate)
                 for f in group:
                     by_name.pop(_get_qualname(f, include_module=True))
 
@@ -1022,14 +1233,14 @@ def magic(
             a, b = hints.values()
             arg1, arg2 = params
             if a == b and len(arg1) == len(arg2) <= 3:
-                make_(_make_binop_body, func)
+                make_(_make_binop_body, func, annotate=annotate)
                 del by_name[name]
 
     # Look for Numpy ufuncs or gufuncs, and write array-oriented tests for them.
     if "numpy" in sys.modules:
         for name, func in sorted(by_name.items()):
             if _is_probably_ufunc(func):
-                make_(_make_ufunc_body, func)
+                make_(_make_ufunc_body, func, annotate=annotate)
                 del by_name[name]
 
     # For all remaining callables, just write a fuzz-test.  In principle we could
@@ -1037,12 +1248,22 @@ def magic(
     # be worth the trouble when it's so easy for the user to specify themselves.
     for _, f in sorted(by_name.items()):
         make_(
-            _make_test_body, f, test_body=_write_call(f, except_=except_), ghost="fuzz"
+            _make_test_body,
+            f,
+            test_body=_write_call(f, except_=except_),
+            ghost="fuzz",
+            annotate=annotate,
         )
     return _make_test(imports, "\n".join(parts))
 
 
-def fuzz(func: Callable, *, except_: Except = (), style: str = "pytest") -> str:
+def fuzz(
+    func: Callable,
+    *,
+    except_: Except = (),
+    style: str = "pytest",
+    annotate: Optional[bool] = None,
+) -> str:
     """Write source code for a property-based test of ``func``.
 
     The resulting test checks that valid input only leads to expected exceptions.
@@ -1086,17 +1307,27 @@ def fuzz(func: Callable, *, except_: Except = (), style: str = "pytest") -> str:
     except_ = _check_except(except_)
     _check_style(style)
 
+    if annotate is None:
+        annotate = _are_annotations_used(func)
+
     imports, body = _make_test_body(
         func,
         test_body=_write_call(func, except_=except_),
         except_=except_,
         ghost="fuzz",
         style=style,
+        annotate=annotate,
     )
     return _make_test(imports, body)
 
 
-def idempotent(func: Callable, *, except_: Except = (), style: str = "pytest") -> str:
+def idempotent(
+    func: Callable,
+    *,
+    except_: Except = (),
+    style: str = "pytest",
+    annotate: Optional[bool] = None,
+) -> str:
     """Write source code for a property-based test of ``func``.
 
     The resulting test checks that if you call ``func`` on it's own output,
@@ -1136,6 +1367,9 @@ def idempotent(func: Callable, *, except_: Except = (), style: str = "pytest") -
     except_ = _check_except(except_)
     _check_style(style)
 
+    if annotate is None:
+        annotate = _are_annotations_used(func)
+
     imports, body = _make_test_body(
         func,
         test_body="result = {}\nrepeat = {}".format(
@@ -1146,11 +1380,12 @@ def idempotent(func: Callable, *, except_: Except = (), style: str = "pytest") -
         assertions=_assert_eq(style, "result", "repeat"),
         ghost="idempotent",
         style=style,
+        annotate=annotate,
     )
     return _make_test(imports, body)
 
 
-def _make_roundtrip_body(funcs, except_, style):
+def _make_roundtrip_body(funcs, except_, style, annotate):
     first_param = next(iter(_get_params(funcs[0])))
     test_lines = [
         _write_call(funcs[0], assign="value0", except_=except_),
@@ -1166,10 +1401,16 @@ def _make_roundtrip_body(funcs, except_, style):
         assertions=_assert_eq(style, first_param, f"value{len(funcs) - 1}"),
         ghost="roundtrip",
         style=style,
+        annotate=annotate,
     )
 
 
-def roundtrip(*funcs: Callable, except_: Except = (), style: str = "pytest") -> str:
+def roundtrip(
+    *funcs: Callable,
+    except_: Except = (),
+    style: str = "pytest",
+    annotate: Optional[bool] = None,
+) -> str:
     """Write source code for a property-based test of ``funcs``.
 
     The resulting test checks that if you call the first function, pass the result
@@ -1190,10 +1431,14 @@ def roundtrip(*funcs: Callable, except_: Except = (), style: str = "pytest") ->
             raise InvalidArgument(f"Got non-callable funcs[{i}]={f!r}")
     except_ = _check_except(except_)
     _check_style(style)
-    return _make_test(*_make_roundtrip_body(funcs, except_, style))
 
+    if annotate is None:
+        annotate = _are_annotations_used(*funcs)
+
+    return _make_test(*_make_roundtrip_body(funcs, except_, style, annotate))
 
-def _make_equiv_body(funcs, except_, style):
+
+def _make_equiv_body(funcs, except_, style, annotate):
     var_names = [f"result_{f.__name__}" for f in funcs]
     if len(set(var_names)) < len(var_names):
         var_names = [f"result_{i}_{ f.__name__}" for i, f in enumerate(funcs)]
@@ -1212,6 +1457,7 @@ def _make_equiv_body(funcs, except_, style):
         assertions=assertions,
         ghost="equivalent",
         style=style,
+        annotate=annotate,
     )
 
 
@@ -1234,7 +1480,7 @@ else:
 """.rstrip()
 
 
-def _make_equiv_errors_body(funcs, except_, style):
+def _make_equiv_errors_body(funcs, except_, style, annotate):
     var_names = [f"result_{f.__name__}" for f in funcs]
     if len(set(var_names)) < len(var_names):
         var_names = [f"result_{i}_{ f.__name__}" for i, f in enumerate(funcs)]
@@ -1267,6 +1513,7 @@ def _make_equiv_errors_body(funcs, except_, style):
         except_=(),
         ghost="equivalent",
         style=style,
+        annotate=annotate,
     )
     return imports | extra_imports, source_code
 
@@ -1276,6 +1523,7 @@ def equivalent(
     allow_same_errors: bool = False,
     except_: Except = (),
     style: str = "pytest",
+    annotate: Optional[bool] = None,
 ) -> str:
     """Write source code for a property-based test of ``funcs``.
 
@@ -1303,10 +1551,14 @@ def equivalent(
     check_type(bool, allow_same_errors, "allow_same_errors")
     except_ = _check_except(except_)
     _check_style(style)
+
+    if annotate is None:
+        annotate = _are_annotations_used(*funcs)
+
     if allow_same_errors and not any(issubclass(Exception, ex) for ex in except_):
-        imports, source_code = _make_equiv_errors_body(funcs, except_, style)
+        imports, source_code = _make_equiv_errors_body(funcs, except_, style, annotate)
     else:
-        imports, source_code = _make_equiv_body(funcs, except_, style)
+        imports, source_code = _make_equiv_body(funcs, except_, style, annotate)
     return _make_test(imports, source_code)
 
 
@@ -1323,6 +1575,7 @@ def binary_operation(
     distributes_over: Optional[Callable[[X, X], X]] = None,
     except_: Except = (),
     style: str = "pytest",
+    annotate: Optional[bool] = None,
 ) -> str:
     """Write property tests for the binary operation ``func``.
 
@@ -1363,6 +1616,10 @@ def binary_operation(
         raise InvalidArgument(
             "You must select at least one property of the binary operation to test."
         )
+
+    if annotate is None:
+        annotate = _are_annotations_used(func)
+
     imports, body = _make_binop_body(
         func,
         associative=associative,
@@ -1371,6 +1628,7 @@ def binary_operation(
         distributes_over=distributes_over,
         except_=except_,
         style=style,
+        annotate=annotate,
     )
     return _make_test(imports, body)
 
@@ -1384,6 +1642,7 @@ def _make_binop_body(
     distributes_over: Optional[Callable[[X, X], X]] = None,
     except_: Tuple[Type[Exception], ...],
     style: str,
+    annotate: bool,
 ) -> Tuple[ImportSet, str]:
     strategies = _get_strategies(func)
     operands, b = (strategies.pop(p) for p in list(_get_params(func))[:2])
@@ -1413,6 +1672,7 @@ def _make_binop_body(
             assertions=assertions,
             style=style,
             given_strategies={**strategies, **{n: operands_name for n in args}},
+            annotate=annotate,
         )
         all_imports.update(imports)
         if style == "unittest":
@@ -1506,7 +1766,13 @@ def _make_binop_body(
     )
 
 
-def ufunc(func: Callable, *, except_: Except = (), style: str = "pytest") -> str:
+def ufunc(
+    func: Callable,
+    *,
+    except_: Except = (),
+    style: str = "pytest",
+    annotate: Optional[bool] = None,
+) -> str:
     """Write a property-based test for the :doc:`array ufunc <numpy:reference/ufuncs>` ``func``.
 
     The resulting test checks that your ufunc or :doc:`gufunc
@@ -1522,11 +1788,16 @@ def ufunc(func: Callable, *, except_: Except = (), style: str = "pytest") -> str
         raise InvalidArgument(f"func={func!r} does not seem to be a ufunc")
     except_ = _check_except(except_)
     _check_style(style)
-    return _make_test(*_make_ufunc_body(func, except_=except_, style=style))
 
+    if annotate is None:
+        annotate = _are_annotations_used(func)
+
+    return _make_test(
+        *_make_ufunc_body(func, except_=except_, style=style, annotate=annotate)
+    )
 
-def _make_ufunc_body(func, *, except_, style):
 
+def _make_ufunc_body(func, *, except_, style, annotate):
     import hypothesis.extra.numpy as npst
 
     if func.signature is None:
@@ -1570,4 +1841,5 @@ def _make_ufunc_body(func, *, except_, style):
         style=style,
         given_strategies={"data": st.data(), "shapes": shapes, "types": types},
         imports={("hypothesis.extra.numpy", "arrays")},
+        annotate=annotate,
     )
diff --git a/hypothesis-python/src/hypothesis/extra/numpy.py b/hypothesis-python/src/hypothesis/extra/numpy.py
index 6e2c7ec9b..e2e1e095e 100644
--- a/hypothesis-python/src/hypothesis/extra/numpy.py
+++ b/hypothesis-python/src/hypothesis/extra/numpy.py
@@ -34,6 +34,7 @@ from hypothesis.internal.conjecture import utils as cu
 from hypothesis.internal.coverage import check_function
 from hypothesis.internal.reflection import proxies
 from hypothesis.internal.validation import check_type
+from hypothesis.strategies._internal.numbers import Real
 from hypothesis.strategies._internal.strategies import T, check_strategy
 from hypothesis.strategies._internal.utils import defines_strategy
 
@@ -81,16 +82,18 @@ def from_dtype(
     allow_subnormal: Optional[bool] = None,
     exclude_min: Optional[bool] = None,
     exclude_max: Optional[bool] = None,
+    min_magnitude: Real = 0,
+    max_magnitude: Optional[Real] = None,
 ) -> st.SearchStrategy[Any]:
     """Creates a strategy which can generate any value of the given dtype.
 
-    Compatible ``**kwargs`` are passed to the inferred strategy function for
-    integers, floats, and strings.  This allows you to customise the min and max
-    values, control the length or contents of strings, or exclude non-finite
-    numbers.  This is particularly useful when kwargs are passed through from
+    Compatible parameters are passed to the inferred strategy function while
+    inapplicable ones are ignored.
+    This allows you, for example, to customise the min and max values,
+    control the length or contents of strings, or exclude non-finite
+    numbers. This is particularly useful when kwargs are passed through from
     :func:`arrays` which allow a variety of numeric dtypes, as it seamlessly
-    handles the ``width`` or representable bounds for you.  See :issue:`2552`
-    for more detail.
+    handles the ``width`` or representable bounds for you.
     """
     check_type(np.dtype, dtype, "dtype")
     kwargs = {k: v for k, v in locals().items() if k != "dtype" and v is not None}
@@ -136,14 +139,16 @@ def from_dtype(
             ),
         )
     elif dtype.kind == "c":
-        # If anyone wants to add a `width` argument to `complex_numbers()`, we would
-        # accept a pull request and add passthrough support for magnitude bounds,
-        # but it's a low priority otherwise.
-        if dtype.itemsize == 8:
-            float32 = st.floats(width=32, **compat_kw("allow_nan", "allow_infinity"))
-            result = st.builds(complex, float32, float32)
-        else:
-            result = st.complex_numbers(**compat_kw("allow_nan", "allow_infinity"))
+        result = st.complex_numbers(
+            width=min(8 * dtype.itemsize, 128),  # convert from bytes to bits
+            **compat_kw(
+                "min_magnitude",
+                "max_magnitude",
+                "allow_nan",
+                "allow_infinity",
+                "allow_subnormal",
+            ),
+        )
     elif dtype.kind in ("S", "a"):
         # Numpy strings are null-terminated; only allow round-trippable values.
         # `itemsize == 0` means 'fixed length determined at array creation'
@@ -409,7 +414,6 @@ def arrays(
     .. code-block:: pycon
 
       >>> import numpy as np
-      >>> from hypothesis import strategies as st
       >>> arrays(np.int8, (2, 3)).example()
       array([[-8,  6,  3],
              [-6,  4,  6]], dtype=int8)
diff --git a/hypothesis-python/src/hypothesis/extra/pandas/impl.py b/hypothesis-python/src/hypothesis/extra/pandas/impl.py
index a1220d6b3..85fab8106 100644
--- a/hypothesis-python/src/hypothesis/extra/pandas/impl.py
+++ b/hypothesis-python/src/hypothesis/extra/pandas/impl.py
@@ -59,7 +59,6 @@ def infer_dtype_if_necessary(dtype, values, elements, draw):
 
 @check_function
 def elements_and_dtype(elements, dtype, source=None):
-
     if source is None:
         prefix = ""
     else:
@@ -166,6 +165,7 @@ DEFAULT_MAX_SIZE = 10
 def range_indexes(
     min_size: int = 0,
     max_size: Optional[int] = None,
+    name: st.SearchStrategy[Optional[str]] = st.none(),
 ) -> st.SearchStrategy[pandas.RangeIndex]:
     """Provides a strategy which generates an :class:`~pandas.Index` whose
     values are 0, 1, ..., n for some n.
@@ -175,13 +175,16 @@ def range_indexes(
     * min_size is the smallest number of elements the index can have.
     * max_size is the largest number of elements the index can have. If None
       it will default to some suitable value based on min_size.
+    * name is the name of the index. If st.none(), the index will have no name.
     """
     check_valid_size(min_size, "min_size")
     check_valid_size(max_size, "max_size")
     if max_size is None:
         max_size = min([min_size + DEFAULT_MAX_SIZE, 2**63 - 1])
     check_valid_interval(min_size, max_size, "min_size", "max_size")
-    return st.integers(min_size, max_size).map(pandas.RangeIndex)
+    check_strategy(name)
+
+    return st.builds(pandas.RangeIndex, st.integers(min_size, max_size), name=name)
 
 
 @cacheable
diff --git a/hypothesis-python/src/hypothesis/internal/compat.py b/hypothesis-python/src/hypothesis/internal/compat.py
index 34b712e68..decb35e32 100644
--- a/hypothesis-python/src/hypothesis/internal/compat.py
+++ b/hypothesis-python/src/hypothesis/internal/compat.py
@@ -17,7 +17,7 @@ from functools import partial
 from typing import Any, ForwardRef, Tuple
 
 try:
-    from typing import get_args
+    from typing import get_args as get_args
 except ImportError:
     # remove at Python 3.7 end-of-life
     from collections.abc import Callable as _Callable
@@ -46,6 +46,31 @@ except ImportError:
         return ()
 
 
+try:
+    from typing import get_origin as get_origin
+except ImportError:
+    # remove at Python 3.7 end-of-life
+    from collections.abc import Callable as _Callable
+
+    def get_origin(tp: Any) -> typing.Optional[Any]:  # pragma: no cover
+        """Get the unsubscripted version of a type.
+        This supports generic types, Callable, Tuple, Union, Literal, Final and ClassVar.
+        Return None for unsupported types. Examples::
+            get_origin(Literal[42]) is Literal
+            get_origin(int) is None
+            get_origin(ClassVar[int]) is ClassVar
+            get_origin(Generic) is Generic
+            get_origin(Generic[T]) is Generic
+            get_origin(Union[T, int]) is Union
+            get_origin(List[Tuple[T, T]][int]) == list
+        """
+        if hasattr(tp, "__origin__"):
+            return tp.__origin__
+        if tp is typing.Generic:
+            return typing.Generic
+        return None
+
+
 try:
     BaseExceptionGroup = BaseExceptionGroup
     ExceptionGroup = ExceptionGroup  # pragma: no cover
@@ -69,6 +94,7 @@ else:
             Concatenate, ParamSpec = None, None
 
 PYPY = platform.python_implementation() == "PyPy"
+GRAALPY = platform.python_implementation() == "GraalVM"
 WINDOWS = platform.system() == "Windows"
 
 
@@ -155,7 +181,6 @@ def get_type_hints(thing):
                     and is_a_type(p.annotation)
                     and p.annotation is not p.empty
                 ):
-
                     p_hint = p.annotation
 
                     # Defer to `get_type_hints` if signature annotation is, or
diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/dfas.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/dfas.py
index 8fb42a33b..81ea67a15 100644
--- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/dfas.py
+++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinking/dfas.py
@@ -261,7 +261,7 @@ def normalize(
 
     runner = ConjectureRunner(
         test_function,
-        settings=settings(database=None, suppress_health_check=HealthCheck.all()),
+        settings=settings(database=None, suppress_health_check=list(HealthCheck)),
         ignore_limits=True,
         random=random,
     )
diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py
index 0133ceaaf..a8df18375 100644
--- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py
+++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py
@@ -207,7 +207,7 @@ def biased_coin(
         # The logic here is a bit complicated and special cased to make it
         # play better with the shrinker.
 
-        # We imagine partitioning the real interval [0, 1] into 256 equal parts
+        # We imagine partitioning the real interval [0, 1] into 2**n equal parts
         # and looking at each part and whether its interior is wholly <= p
         # or wholly >= p. At most one part can be neither.
 
@@ -251,9 +251,9 @@ def biased_coin(
             else:
                 i = data.draw_bits(bits, forced=int(forced))
 
-            # We always label the region that causes us to repeat the loop as
-            # 255 so that shrinking this byte never causes us to need to draw
-            # more data.
+            # We always choose the region that causes us to repeat the loop as
+            # the maximum value, so that shrinking the drawn bits never causes
+            # us to need to draw more data.
             if partial and i == size - 1:
                 p = remainder
                 continue
@@ -311,7 +311,6 @@ class Sampler:
     table: List[Tuple[int, int, float]]  # (base_idx, alt_idx, alt_chance)
 
     def __init__(self, weights: Sequence[float]):
-
         n = len(weights)
 
         table: "list[list[int | float | None]]" = [[i, None, None] for i in range(n)]
diff --git a/hypothesis-python/src/hypothesis/internal/entropy.py b/hypothesis-python/src/hypothesis/internal/entropy.py
index bf7f3a9f0..fb0ca2b27 100644
--- a/hypothesis-python/src/hypothesis/internal/entropy.py
+++ b/hypothesis-python/src/hypothesis/internal/entropy.py
@@ -19,7 +19,7 @@ from weakref import WeakValueDictionary
 
 import hypothesis.core
 from hypothesis.errors import HypothesisWarning, InvalidArgument
-from hypothesis.internal.compat import PYPY
+from hypothesis.internal.compat import GRAALPY, PYPY
 
 if TYPE_CHECKING:
     if sys.version_info >= (3, 8):
@@ -59,7 +59,7 @@ class NumpyRandomWrapper:
 NP_RANDOM = None
 
 
-if not PYPY:
+if not (PYPY or GRAALPY):
 
     def _get_platform_base_refcount(r: Any) -> int:
         return sys.getrefcount(r)
@@ -68,7 +68,7 @@ if not PYPY:
     # the given platform / version of Python.
     _PLATFORM_REF_COUNT = _get_platform_base_refcount(object())
 else:  # pragma: no cover
-    # PYPY doesn't have `sys.getrefcount`
+    # PYPY and GRAALPY don't have `sys.getrefcount`
     _PLATFORM_REF_COUNT = -1
 
 
@@ -118,8 +118,8 @@ def register_random(r: RandomLike) -> None:
     if r in RANDOMS_TO_MANAGE.values():
         return
 
-    if not PYPY:  # pragma: no branch
-        # PYPY does not have `sys.getrefcount`
+    if not (PYPY or GRAALPY):  # pragma: no branch
+        # PYPY and GRAALPY do not have `sys.getrefcount`
         gc.collect()
         if not gc.get_referrers(r):
             if sys.getrefcount(r) <= _PLATFORM_REF_COUNT:
diff --git a/hypothesis-python/src/hypothesis/internal/reflection.py b/hypothesis-python/src/hypothesis/internal/reflection.py
index 5e66b4471..f64b527e4 100644
--- a/hypothesis-python/src/hypothesis/internal/reflection.py
+++ b/hypothesis-python/src/hypothesis/internal/reflection.py
@@ -127,7 +127,9 @@ def check_signature(sig: inspect.Signature) -> None:
             )
 
 
-def get_signature(target: Any, *, follow_wrapped: bool = True) -> inspect.Signature:
+def get_signature(
+    target: Any, *, follow_wrapped: bool = True, eval_str: bool = False
+) -> inspect.Signature:
     # Special case for use of `@unittest.mock.patch` decorator, mimicking the
     # behaviour of getfullargspec instead of reporting unusable arguments.
     patches = getattr(target, "patchings", None)
@@ -164,7 +166,15 @@ def get_signature(target: Any, *, follow_wrapped: bool = True) -> inspect.Signat
             return sig.replace(
                 parameters=[v for k, v in sig.parameters.items() if k != "self"]
             )
-    sig = inspect.signature(target, follow_wrapped=follow_wrapped)
+    # eval_str is only supported by Python 3.10 and newer
+    if sys.version_info[:2] >= (3, 10):
+        sig = inspect.signature(
+            target, follow_wrapped=follow_wrapped, eval_str=eval_str
+        )
+    else:
+        sig = inspect.signature(
+            target, follow_wrapped=follow_wrapped
+        )  # pragma: no cover
     check_signature(sig)
     return sig
 
diff --git a/hypothesis-python/src/hypothesis/internal/scrutineer.py b/hypothesis-python/src/hypothesis/internal/scrutineer.py
index 6c5075114..c3a53154c 100644
--- a/hypothesis-python/src/hypothesis/internal/scrutineer.py
+++ b/hypothesis-python/src/hypothesis/internal/scrutineer.py
@@ -54,8 +54,10 @@ UNHELPFUL_LOCATIONS = (
     f"{sep}inspect.py",
     f"{sep}re.py",
     f"{sep}re{sep}__init__.py",  # refactored in Python 3.11
-    # Quite rarely, the first AFNP line is in Pytest's assertion-rewriting module.
+    f"{sep}warnings.py",
+    # Quite rarely, the first AFNP line is in Pytest's internals.
     f"{sep}_pytest{sep}assertion{sep}rewrite.py",
+    f"{sep}_pytest{sep}_io{sep}saferepr.py",
 )
 
 
diff --git a/hypothesis-python/src/hypothesis/stateful.py b/hypothesis-python/src/hypothesis/stateful.py
index 1c66d88ee..69e1ac5b1 100644
--- a/hypothesis-python/src/hypothesis/stateful.py
+++ b/hypothesis-python/src/hypothesis/stateful.py
@@ -42,7 +42,7 @@ from hypothesis._settings import (
     note_deprecation,
     settings as Settings,
 )
-from hypothesis.control import current_build_context
+from hypothesis.control import _current_build_context, current_build_context
 from hypothesis.core import TestFunc, given
 from hypothesis.errors import InvalidArgument, InvalidDefinition
 from hypothesis.internal.conjecture import utils as cu
@@ -87,7 +87,7 @@ class TestCaseProperty:  # pragma: no cover
         raise AttributeError("Cannot delete TestCase")
 
 
-def run_state_machine_as_test(state_machine_factory, *, settings=None):
+def run_state_machine_as_test(state_machine_factory, *, settings=None, _min_steps=0):
     """Run a state machine definition as a test, either silently doing nothing
     or printing a minimal breaking program and raising an exception.
 
@@ -100,8 +100,13 @@ def run_state_machine_as_test(state_machine_factory, *, settings=None):
             settings = state_machine_factory.TestCase.settings
             check_type(Settings, settings, "state_machine_factory.TestCase.settings")
         except AttributeError:
-            settings = Settings(deadline=None, suppress_health_check=HealthCheck.all())
+            settings = Settings(deadline=None, suppress_health_check=list(HealthCheck))
     check_type(Settings, settings, "settings")
+    check_type(int, _min_steps, "_min_steps")
+    if _min_steps < 0:
+        # Because settings can vary via e.g. profiles, settings.stateful_step_count
+        # overrides this argument and we don't bother cross-validating.
+        raise InvalidArgument(f"_min_steps={_min_steps} must be non-negative.")
 
     @settings
     @given(st.data())
@@ -129,24 +134,13 @@ def run_state_machine_as_test(state_machine_factory, *, settings=None):
                 # 2 ** -16 during normal operation but force a stop when we've
                 # generated enough steps.
                 cd.start_example(STATE_MACHINE_RUN_LABEL)
-                if steps_run == 0:
-                    cd.draw_bits(16, forced=1)
-                elif steps_run >= max_steps:
-                    cd.draw_bits(16, forced=0)
+                must_stop = None
+                if steps_run >= max_steps:
+                    must_stop = True
+                elif steps_run <= _min_steps:
+                    must_stop = False
+                if cu.biased_coin(cd, 2**-16, forced=must_stop):
                     break
-                else:
-                    # All we really care about is whether this value is zero
-                    # or non-zero, so if it's > 1 we discard it and insert a
-                    # replacement value after
-                    cd.start_example(SHOULD_CONTINUE_LABEL)
-                    should_continue_value = cd.draw_bits(16)
-                    if should_continue_value > 1:
-                        cd.stop_example(discard=True)
-                        cd.draw_bits(16, forced=int(bool(should_continue_value)))
-                    else:
-                        cd.stop_example()
-                        if should_continue_value == 0:
-                            break
                 steps_run += 1
 
                 # Choose a rule to run, preferring an initialize rule if there are
@@ -256,7 +250,9 @@ class RuleBasedStateMachine(metaclass=StateMachineMeta):
         self.name_counter = 1
         self.names_to_values: Dict[str, Any] = {}
         self.__stream = StringIO()
-        self.__printer = RepresentationPrinter(self.__stream)
+        self.__printer = RepresentationPrinter(
+            self.__stream, context=_current_build_context.value
+        )
         self._initialize_rules_to_run = copy(self.initialize_rules())
         self._rules_strategy = RuleStrategy(self)
 
@@ -394,7 +390,7 @@ class RuleBasedStateMachine(metaclass=StateMachineMeta):
     @lru_cache()
     def _to_test_case(cls):
         class StateMachineTestCase(TestCase):
-            settings = Settings(deadline=None, suppress_health_check=HealthCheck.all())
+            settings = Settings(deadline=None, suppress_health_check=list(HealthCheck))
 
             def runTest(self):
                 run_state_machine_as_test(cls)
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
index f54321f54..2b528e39e 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
@@ -46,7 +46,7 @@ from uuid import UUID
 import attr
 
 from hypothesis._settings import note_deprecation
-from hypothesis.control import cleanup, note
+from hypothesis.control import cleanup, current_build_context, note
 from hypothesis.errors import InvalidArgument, ResolutionFailed
 from hypothesis.internal.cathetus import cathetus
 from hypothesis.internal.charmap import as_general_categories
@@ -833,11 +833,10 @@ class BuildsStrategy(SearchStrategy):
         self.kwargs = kwargs
 
     def do_draw(self, data):
+        args = [data.draw(a) for a in self.args]
+        kwargs = {k: data.draw(v) for k, v in self.kwargs.items()}
         try:
-            return self.target(
-                *(data.draw(a) for a in self.args),
-                **{k: data.draw(v) for k, v in self.kwargs.items()},
-            )
+            obj = self.target(*args, **kwargs)
         except TypeError as err:
             if (
                 isinstance(self.target, type)
@@ -867,6 +866,9 @@ class BuildsStrategy(SearchStrategy):
                 ) from err
             raise
 
+        current_build_context().record_call(obj, self.target, args, kwargs)
+        return obj
+
     def validate(self):
         tuples(*self.args).validate()
         fixed_dictionaries(self.kwargs).validate()
@@ -1628,8 +1630,10 @@ def complex_numbers(
     allow_infinity: Optional[bool] = None,
     allow_nan: Optional[bool] = None,
     allow_subnormal: bool = True,
+    width: int = 128,
 ) -> SearchStrategy[complex]:
-    """Returns a strategy that generates complex numbers.
+    """Returns a strategy that generates :class:`~python:complex`
+    numbers.
 
     This strategy draws complex numbers with constrained magnitudes.
     The ``min_magnitude`` and ``max_magnitude`` parameters should be
@@ -1640,13 +1644,22 @@ def complex_numbers(
     is an error to enable ``allow_nan``.  If ``max_magnitude`` is finite,
     it is an error to enable ``allow_infinity``.
 
-    ``allow_subnormal`` is applied to each part of the complex number
-    separately, as for :func:`~hypothesis.strategies.floats`.
+    ``allow_infinity``, ``allow_nan``, and ``allow_subnormal`` are
+    applied to each part of the complex number separately, as for
+    :func:`~hypothesis.strategies.floats`.
 
     The magnitude constraints are respected up to a relative error
     of (around) floating-point epsilon, due to implementation via
     the system ``sqrt`` function.
 
+    The ``width`` argument specifies the maximum number of bits of precision
+    required to represent the entire generated complex number.
+    Valid values are 32, 64 or 128, which correspond to the real and imaginary
+    components each having width 16, 32 or 64, respectively.
+    Passing ``width=64`` will still use the builtin 128-bit
+    :class:`~python:complex` class, but always for values which can be
+    exactly represented as two 32-bit floats.
+
     Examples from this strategy shrink by shrinking their real and
     imaginary parts, as :func:`~hypothesis.strategies.floats`.
 
@@ -1675,8 +1688,15 @@ def complex_numbers(
             f"Cannot have allow_nan={allow_nan!r}, min_magnitude={min_magnitude!r} "
             f"max_magnitude={max_magnitude!r}"
         )
-
     check_type(bool, allow_subnormal, "allow_subnormal")
+    if width not in (32, 64, 128):
+        raise InvalidArgument(
+            f"width={width!r}, but must be 32, 64 or 128 (other complex dtypes "
+            "such as complex192 or complex256 are not supported)"
+            # For numpy, these types would be supported (but not by CPython):
+            # https://numpy.org/doc/stable/reference/arrays.scalars.html#complex-floating-point-types
+        )
+    component_width = width // 2
     allow_kw = {
         "allow_nan": allow_nan,
         "allow_infinity": allow_infinity,
@@ -1684,6 +1704,7 @@ def complex_numbers(
         # then allow_subnormal=True would be an error with the min_value to the floats()
         # strategy for the real part.  We therefore replace True with None.
         "allow_subnormal": None if allow_subnormal else allow_subnormal,
+        "width": component_width,
     }
 
     if min_magnitude == 0 and max_magnitude is None:
@@ -1835,7 +1856,7 @@ class DataObject:
         check_strategy(strategy, "strategy")
         result = self.conjecture_data.draw(strategy)
         self.count += 1
-        printer = RepresentationPrinter()
+        printer = RepresentationPrinter(context=current_build_context())
         printer.text(f"Draw {self.count}")
         printer.text(": " if label is None else f" ({label}): ")
         printer.pretty(result)
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py b/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
index 380c8732c..bd506e9d1 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
@@ -401,7 +401,7 @@ def floats(
 
     The width argument specifies the maximum number of bits of precision
     required to represent the generated float. Valid values are 16, 32, or 64.
-    Passing ``width=32`` will still use the builtin 64-bit ``float`` class,
+    Passing ``width=32`` will still use the builtin 64-bit :class:`~python:float` class,
     but always for values which can be exactly represented as a 32-bit float.
 
     The exclude_min and exclude_max argument can be used to generate numbers
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/regex.py b/hypothesis-python/src/hypothesis/strategies/_internal/regex.py
index b93e8d192..2e6611377 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/regex.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/regex.py
@@ -15,8 +15,8 @@ try:  # pragma: no cover
     import re._constants as sre
     import re._parser as sre_parse
 except ImportError:  # Python < 3.11
-    import sre_constants as sre  # type: ignore
-    import sre_parse  # type: ignore
+    import sre_constants as sre
+    import sre_parse
 
 from hypothesis import reject, strategies as st
 from hypothesis.internal.charmap import as_general_categories, categories
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py
index edc719113..66e661598 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py
@@ -329,7 +329,7 @@ class SearchStrategy(Generic[Ex]):
             deadline=None,
             verbosity=Verbosity.quiet,
             phases=(Phase.generate,),
-            suppress_health_check=HealthCheck.all(),
+            suppress_health_check=list(HealthCheck),
         )
         def example_generating_inner_function(ex):
             self.__examples.append(ex)
@@ -835,8 +835,10 @@ class MappedSearchStrategy(SearchStrategy[Ex]):
                 i = data.index
                 try:
                     data.start_example(MAPPED_SEARCH_STRATEGY_DO_DRAW_LABEL)
-                    result = self.pack(data.draw(self.mapped_strategy))  # type: ignore
+                    x = data.draw(self.mapped_strategy)
+                    result = self.pack(x)  # type: ignore
                     data.stop_example()
+                    _current_build_context.value.record_call(result, self.pack, [x], {})
                     return result
                 except UnsatisfiedAssumption:
                     data.stop_example(discard=True)
diff --git a/hypothesis-python/src/hypothesis/vendor/pretty.py b/hypothesis-python/src/hypothesis/vendor/pretty.py
index 13b31f639..f6e2c2837 100644
--- a/hypothesis-python/src/hypothesis/vendor/pretty.py
+++ b/hypothesis-python/src/hypothesis/vendor/pretty.py
@@ -66,13 +66,15 @@ import datetime
 import re
 import struct
 import types
-from collections import deque
+import warnings
+from collections import defaultdict, deque
 from contextlib import contextmanager
 from io import StringIO
 from math import copysign, isnan
 
 __all__ = [
     "pretty",
+    "IDKey",
     "RepresentationPrinter",
 ]
 
@@ -100,6 +102,17 @@ def pretty(obj):
     return printer.getvalue()
 
 
+class IDKey:
+    def __init__(self, value):
+        self.value = value
+
+    def __hash__(self) -> int:
+        return hash((type(self), id(self.value)))
+
+    def __eq__(self, __o: object) -> bool:
+        return isinstance(__o, type(self)) and id(self.value) == id(__o.value)
+
+
 class RepresentationPrinter:
     """Special pretty printer that has a `pretty` method that calls the pretty
     printer for a python object.
@@ -110,7 +123,13 @@ class RepresentationPrinter:
 
     """
 
-    def __init__(self, output=None):
+    def __init__(self, output=None, *, context=None):
+        """Pass the output stream, and optionally the current build context.
+
+        We use the context to represent objects constructed by strategies by showing
+        *how* they were constructed, and add annotations showing which parts of the
+        minimal failing example can vary without changing the test result.
+        """
         self.broken = False
         self.output = StringIO() if output is None else output
         self.max_width = 79
@@ -130,6 +149,11 @@ class RepresentationPrinter:
         self.singleton_pprinters = _singleton_pprinters.copy()
         self.type_pprinters = _type_pprinters.copy()
         self.deferred_pprinters = _deferred_type_pprinters.copy()
+        if context is None:
+            self.known_object_printers = defaultdict(list)
+        else:
+            self.known_object_printers = context.known_object_printers
+        assert all(isinstance(k, IDKey) for k in self.known_object_printers)
 
     def pretty(self, obj):
         """Pretty print the given object."""
@@ -174,6 +198,24 @@ class RepresentationPrinter:
                                 meth = cls._repr_pretty_
                                 if callable(meth):
                                     return meth(obj, self, cycle)
+                # Now check for object-specific printers which show how this
+                # object was constructed (a Hypothesis special feature).
+                printers = self.known_object_printers[IDKey(obj)]
+                if len(printers) == 1:
+                    return printers[0](obj, self, cycle)
+                elif printers:
+                    # We've ended up with multiple registered functions for the same
+                    # object, which must have been returned from multiple calls due to
+                    # e.g. memoization.  If they all return the same string, we'll use
+                    # the first; otherwise we'll pretend that *none* were registered.
+                    strs = set()
+                    for f in printers:
+                        p = RepresentationPrinter()
+                        f(obj, p, cycle)
+                        strs.add(p.getvalue())
+                    if len(strs) == 1:
+                        return printers[0](obj, self, cycle)
+
                 # A user-provided repr. Find newlines and replace them with p.break_()
                 return _repr_pprint(obj, self, cycle)
         finally:
@@ -299,7 +341,7 @@ class RepresentationPrinter:
         self.flush()
         return self.output.getvalue()
 
-    def repr_call(self, func_name, args, kwargs, *, force_split=False):
+    def repr_call(self, func_name, args, kwargs, *, force_split=None):
         """Helper function to represent a function call.
 
         - func_name, args, and kwargs should all be pretty obvious.
@@ -311,24 +353,22 @@ class RepresentationPrinter:
             func_name = f"({func_name})"
         self.text(func_name)
         all_args = [(None, v) for v in args] + list(kwargs.items())
-        if not force_split:
+        if force_split is None:
             # We're OK with printing this call on a single line, but will it fit?
             # If not, we'd rather fall back to one-argument-per-line instead.
             p = RepresentationPrinter()
-            for k, v in all_args:
-                if k:
-                    p.text(f"{k}=")
-                p.pretty(v)
-                p.text(", ")
-            force_split = self.max_width <= self.output_width + len(p.getvalue())
+            p.stack = self.stack.copy()
+            p.known_object_printers = self.known_object_printers
+            p.repr_call("_" * self.output_width, args, kwargs, force_split=False)
+            s = p.getvalue()
+            force_split = "\n" in s
 
         with self.group(indent=4, open="(", close=""):
-            all_args = [(None, v) for v in args] + list(kwargs.items())
             for i, (k, v) in enumerate(all_args):
                 if force_split:
                     self.break_()
-                elif i:
-                    self.breakable()
+                else:
+                    self.breakable(" " if i else "")
                 if k:
                     self.text(f"{k}=")
                 self.pretty(v)
@@ -505,13 +545,18 @@ def _dict_pprinter_factory(start, end, basetype=None):
         if cycle:
             return p.text("{...}")
         with p.group(1, start, end):
-            for idx, key in p._enumerate(obj):
-                if idx:
-                    p.text(",")
-                    p.breakable()
-                p.pretty(key)
-                p.text(": ")
-                p.pretty(obj[key])
+            # If the dict contains both "" and b"" (empty string and empty bytes), we
+            # ignore the BytesWarning raised by `python -bb` mode.  We can't use
+            # `.items()` because it might be a non-`dict` type of mapping.
+            with warnings.catch_warnings():
+                warnings.simplefilter("ignore", BytesWarning)
+                for idx, key in p._enumerate(obj):
+                    if idx:
+                        p.text(",")
+                        p.breakable()
+                    p.pretty(key)
+                    p.text(": ")
+                    p.pretty(obj[key])
 
     inner.__name__ = f"_dict_pprinter_factory({start!r}, {end!r}, {basetype!r})"
     return inner
@@ -718,9 +763,14 @@ def _repr_dataframe(obj, p, cycle):  # pragma: no cover
     p.break_()
 
 
+def _repr_enum(obj, p, cycle):
+    p.text(type(obj).__name__ + "." + obj.name)
+
+
 for_type_by_name("collections", "defaultdict", _defaultdict_pprint)
 for_type_by_name("collections", "OrderedDict", _ordereddict_pprint)
 for_type_by_name("ordereddict", "OrderedDict", _ordereddict_pprint)
 for_type_by_name("collections", "deque", _deque_pprint)
 for_type_by_name("collections", "Counter", _counter_pprint)
 for_type_by_name("pandas.core.frame", "DataFrame", _repr_dataframe)
+for_type_by_name("enum", "Enum", _repr_enum)
diff --git a/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt b/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt
index 30cf8b9fa..031db28bd 100644
--- a/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt
+++ b/hypothesis-python/src/hypothesis/vendor/tlds-alpha-by-domain.txt
@@ -1,4 +1,4 @@
-# Version 2022123100, Last Updated Sat Dec 31 07:07:01 2022 UTC
+# Version 2023031100, Last Updated Sat Mar 11 07:07:01 2023 UTC
 AAA
 AARP
 ABARTH
@@ -739,7 +739,6 @@ LUXURY
 LV
 LY
 MA
-MACYS
 MADRID
 MAIF
 MAISON
diff --git a/hypothesis-python/src/hypothesis/version.py b/hypothesis-python/src/hypothesis/version.py
index 0e1e63b27..3c013d9b1 100644
--- a/hypothesis-python/src/hypothesis/version.py
+++ b/hypothesis-python/src/hypothesis/version.py
@@ -8,5 +8,5 @@
 # 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/.
 
-__version_info__ = (6, 61, 2)
+__version_info__ = (6, 69, 0)
 __version__ = ".".join(map(str, __version_info__))
diff --git a/requirements/coverage.txt b/requirements/coverage.txt
index bed6446c0..09557ca83 100644
--- a/requirements/coverage.txt
+++ b/requirements/coverage.txt
@@ -10,13 +10,13 @@ attrs==22.2.0
     # via
     #   hypothesis (hypothesis-python/setup.py)
     #   pytest
-black==22.12.0
+black==23.1.0
     # via -r requirements/coverage.in
 click==8.1.3
     # via
     #   -r requirements/coverage.in
     #   black
-coverage==7.0.1
+coverage==7.2.1
     # via -r requirements/coverage.in
 dpcontracts==0.6.0
     # via -r requirements/coverage.in
@@ -26,53 +26,55 @@ exceptiongroup==1.1.0 ; python_version < "3.11"
     #   pytest
 execnet==1.9.0
     # via pytest-xdist
-fakeredis==2.4.0
+fakeredis==2.10.0
     # via -r requirements/coverage.in
-iniconfig==1.1.1
+iniconfig==2.0.0
     # via pytest
 lark==1.1.5
     # via -r requirements/coverage.in
 libcst==0.4.9
     # via -r requirements/coverage.in
-mypy-extensions==0.4.3
+mypy-extensions==1.0.0
     # via
     #   black
     #   typing-inspect
-numpy==1.24.1
+numpy==1.24.2
     # via
     #   -r requirements/coverage.in
     #   pandas
-packaging==22.0
-    # via pytest
-pandas==1.5.2
+packaging==23.0
+    # via
+    #   black
+    #   pytest
+pandas==1.5.3
     # via -r requirements/coverage.in
-pathspec==0.10.3
+pathspec==0.11.0
     # via black
 pexpect==4.8.0
     # via -r requirements/test.in
-platformdirs==2.6.2
+platformdirs==3.1.1
     # via black
 pluggy==1.0.0
     # via pytest
 ptyprocess==0.7.0
     # via pexpect
-pytest==7.2.0
+pytest==7.2.2
     # via
     #   -r requirements/test.in
     #   pytest-xdist
-pytest-xdist==3.1.0
+pytest-xdist==3.2.0
     # via -r requirements/test.in
 python-dateutil==2.8.2
     # via
     #   -r requirements/coverage.in
     #   pandas
-pytz==2022.7
+pytz==2022.7.1
     # via
     #   -r requirements/coverage.in
     #   pandas
 pyyaml==6.0
     # via libcst
-redis==4.4.0
+redis==4.5.1
     # via fakeredis
 six==1.16.0
     # via python-dateutil
@@ -84,7 +86,7 @@ tomli==2.0.1
     # via
     #   black
     #   pytest
-typing-extensions==4.4.0
+typing-extensions==4.5.0
     # via
     #   -r requirements/coverage.in
     #   libcst
diff --git a/requirements/test.txt b/requirements/test.txt
index 10e5580cf..ac3cbcd79 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -14,9 +14,9 @@ exceptiongroup==1.1.0 ; python_version < "3.11"
     #   pytest
 execnet==1.9.0
     # via pytest-xdist
-iniconfig==1.1.1
+iniconfig==2.0.0
     # via pytest
-packaging==22.0
+packaging==23.0
     # via pytest
 pexpect==4.8.0
     # via -r requirements/test.in
@@ -24,11 +24,11 @@ pluggy==1.0.0
     # via pytest
 ptyprocess==0.7.0
     # via pexpect
-pytest==7.2.0
+pytest==7.2.2
     # via
     #   -r requirements/test.in
     #   pytest-xdist
-pytest-xdist==3.1.0
+pytest-xdist==3.2.0
     # via -r requirements/test.in
 sortedcontainers==2.4.0
     # via hypothesis (hypothesis-python/setup.py)
diff --git a/requirements/tools.txt b/requirements/tools.txt
index bea4434e8..feee7e7cf 100644
--- a/requirements/tools.txt
+++ b/requirements/tools.txt
@@ -4,7 +4,7 @@
 #
 #    ./build.sh upgrade-requirements
 #
-alabaster==0.7.12
+alabaster==0.7.13
     # via sphinx
 asgiref==3.6.0
     # via django
@@ -17,23 +17,23 @@ attrs==22.2.0
     #   flake8-bugbear
     #   hypothesis (hypothesis-python/setup.py)
     #   pytest
-autoflake==2.0.0
+autoflake==2.0.2
     # via shed
-babel==2.11.0
+babel==2.12.1
     # via sphinx
 backcall==0.2.0
     # via ipython
-bandit==1.7.4
+bandit==1.7.5
     # via flake8-bandit
-beautifulsoup4==4.11.1
+beautifulsoup4==4.11.2
     # via sphinx-codeautolink
-black==22.12.0
+black==23.1.0
     # via shed
-bleach==5.0.1
+bleach==6.0.0
     # via readme-renderer
-build==0.9.0
+build==0.10.0
     # via pip-tools
-cachetools==5.2.0
+cachetools==5.3.0
     # via tox
 certifi==2022.12.7
     # via requests
@@ -41,35 +41,37 @@ cffi==1.15.1
     # via cryptography
 chardet==5.1.0
     # via tox
-charset-normalizer==2.1.1
+charset-normalizer==3.1.0
     # via requests
 click==8.1.3
     # via
     #   black
     #   pip-tools
-codespell==2.2.2
+codespell==2.2.4
     # via -r requirements/tools.in
 colorama==0.4.6
     # via tox
 com2ann==0.3.0
     # via shed
-commonmark==0.9.1
-    # via rich
-coverage==7.0.1
+coverage==7.2.1
     # via -r requirements/tools.in
-cryptography==38.0.4
-    # via secretstorage
+cryptography==39.0.2
+    # via
+    #   secretstorage
+    #   types-pyopenssl
+    #   types-redis
 decorator==5.1.1
     # via ipython
 distlib==0.3.6
     # via virtualenv
-django==4.1.4
+django==4.1.7
     # via -r requirements/tools.in
-docutils==0.19
+docutils==0.18.1
     # via
     #   readme-renderer
     #   restructuredtext-lint
     #   sphinx
+    #   sphinx-rtd-theme
 dpcontracts==0.6.0
     # via -r requirements/tools.in
 exceptiongroup==1.1.0 ; python_version < "3.11"
@@ -99,7 +101,7 @@ flake8-2020==1.7.0
     # via -r requirements/tools.in
 flake8-bandit==4.1.1
     # via -r requirements/tools.in
-flake8-bugbear==22.12.6
+flake8-bugbear==23.2.13
     # via -r requirements/tools.in
 flake8-builtins==2.1.0
     # via -r requirements/tools.in
@@ -107,7 +109,7 @@ flake8-comprehensions==3.10.1
     # via -r requirements/tools.in
 flake8-datetimez==20.10.0
     # via -r requirements/tools.in
-flake8-docstrings==1.6.0
+flake8-docstrings==1.7.0
     # via -r requirements/tools.in
 flake8-mutable==1.2.0
     # via -r requirements/tools.in
@@ -119,7 +121,7 @@ flake8-plugin-utils==1.3.2
     # via
     #   flake8-pytest-style
     #   flake8-return
-flake8-pytest-style==1.6.0
+flake8-pytest-style==1.7.2
     # via -r requirements/tools.in
 flake8-return==1.2.0
     # via -r requirements/tools.in
@@ -127,21 +129,21 @@ flake8-simplify==0.19.3
     # via -r requirements/tools.in
 gitdb==4.0.10
     # via gitpython
-gitpython==3.1.30
+gitpython==3.1.31
     # via bandit
 idna==3.4
     # via requests
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==5.2.0
+importlib-metadata==6.0.0
     # via
     #   keyring
     #   twine
-iniconfig==1.1.1
+iniconfig==2.0.0
     # via pytest
-ipython==8.7.0
+ipython==8.11.0
     # via -r requirements/tools.in
-isort==5.11.4
+isort==5.12.0
     # via shed
 jaraco-classes==3.2.3
     # via keyring
@@ -161,25 +163,30 @@ libcst==0.4.9
     # via
     #   -r requirements/tools.in
     #   shed
-markupsafe==2.1.1
+markdown-it-py==2.2.0
+    # via rich
+markupsafe==2.1.2
     # via jinja2
 matplotlib-inline==0.1.6
     # via ipython
 mccabe==0.7.0
     # via flake8
-more-itertools==9.0.0
+mdurl==0.1.2
+    # via markdown-it-py
+more-itertools==9.1.0
     # via jaraco-classes
-mypy==0.991
+mypy==1.1.1
     # via -r requirements/tools.in
-mypy-extensions==0.4.3
+mypy-extensions==1.0.0
     # via
     #   black
     #   mypy
     #   typing-inspect
 nodeenv==1.7.0
     # via pyright
-packaging==22.0
+packaging==23.0
     # via
+    #   black
     #   build
     #   pyproject-api
     #   pytest
@@ -187,21 +194,19 @@ packaging==22.0
     #   tox
 parso==0.8.3
     # via jedi
-pathspec==0.10.3
+pathspec==0.11.0
     # via black
-pbr==5.11.0
+pbr==5.11.1
     # via stevedore
-pep517==0.13.0
-    # via build
 pexpect==4.8.0
     # via ipython
 pickleshare==0.7.5
     # via ipython
-pip-tools==6.12.1
+pip-tools==6.12.3
     # via -r requirements/tools.in
-pkginfo==1.9.2
+pkginfo==1.9.6
     # via twine
-platformdirs==2.6.2
+platformdirs==3.1.1
     # via
     #   black
     #   tox
@@ -210,7 +215,7 @@ pluggy==1.0.0
     # via
     #   pytest
     #   tox
-prompt-toolkit==3.0.36
+prompt-toolkit==3.0.38
     # via ipython
 ptyprocess==0.7.0
     # via pexpect
@@ -220,28 +225,28 @@ pycodestyle==2.10.0
     # via flake8
 pycparser==2.21
     # via cffi
-pydocstyle==6.1.1
+pydocstyle==6.3.0
     # via flake8-docstrings
 pyflakes==3.0.1
     # via
     #   autoflake
     #   flake8
-pygments==2.13.0
+pygments==2.14.0
     # via
     #   ipython
     #   readme-renderer
     #   rich
     #   sphinx
-pyproject-api==1.2.1
+pyproject-api==1.5.0
     # via tox
-pyright==1.1.286
+pyproject-hooks==1.0.0
+    # via build
+pyright==1.1.298
     # via -r requirements/tools.in
-pytest==7.2.0
+pytest==7.2.2
     # via -r requirements/tools.in
 python-dateutil==2.8.2
     # via -r requirements/tools.in
-pytz==2022.7
-    # via babel
 pyupgrade==3.3.1
     # via shed
 pyyaml==6.0
@@ -250,7 +255,7 @@ pyyaml==6.0
     #   libcst
 readme-renderer==37.3
     # via twine
-requests==2.28.1
+requests==2.28.2
     # via
     #   -r requirements/tools.in
     #   requests-toolbelt
@@ -262,11 +267,13 @@ restructuredtext-lint==1.4.0
     # via -r requirements/tools.in
 rfc3986==2.0.0
     # via twine
-rich==13.0.0
-    # via twine
+rich==13.3.2
+    # via
+    #   bandit
+    #   twine
 secretstorage==3.3.3
     # via keyring
-shed==0.10.8
+shed==2023.3.1
     # via -r requirements/tools.in
 six==1.16.0
     # via
@@ -281,30 +288,32 @@ snowballstemmer==2.2.0
     #   sphinx
 sortedcontainers==2.4.0
     # via hypothesis (hypothesis-python/setup.py)
-soupsieve==2.3.2.post1
+soupsieve==2.4
     # via beautifulsoup4
-sphinx==6.0.0
+sphinx==6.1.3
     # via
     #   -r requirements/tools.in
     #   sphinx-codeautolink
     #   sphinx-hoverxref
     #   sphinx-rtd-theme
-sphinx-codeautolink==0.12.1
+sphinx-codeautolink==0.14.1
     # via -r requirements/tools.in
 sphinx-hoverxref==1.3.0
     # via -r requirements/tools.in
-sphinx-rtd-theme==0.5.1
+sphinx-rtd-theme==1.2.0
     # via -r requirements/tools.in
 sphinx-selective-exclude==1.0.3
     # via -r requirements/tools.in
-sphinxcontrib-applehelp==1.0.2
+sphinxcontrib-applehelp==1.0.4
     # via sphinx
 sphinxcontrib-devhelp==1.0.2
     # via sphinx
-sphinxcontrib-htmlhelp==2.0.0
+sphinxcontrib-htmlhelp==2.0.1
     # via sphinx
-sphinxcontrib-jquery==3.0.0
-    # via sphinx-hoverxref
+sphinxcontrib-jquery==2.0.0
+    # via
+    #   sphinx-hoverxref
+    #   sphinx-rtd-theme
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
 sphinxcontrib-qthelp==1.0.3
@@ -315,7 +324,7 @@ sqlparse==0.4.3
     # via django
 stack-data==0.6.2
     # via ipython
-stevedore==4.1.1
+stevedore==5.0.0
     # via bandit
 tokenize-rt==5.0.0
     # via pyupgrade
@@ -325,13 +334,13 @@ tomli==2.0.1
     #   black
     #   build
     #   mypy
-    #   pep517
     #   pyproject-api
+    #   pyproject-hooks
     #   pytest
     #   tox
-tox==4.1.2
+tox==4.4.6
     # via -r requirements/tools.in
-traitlets==5.8.0
+traitlets==5.9.0
     # via
     #   ipython
     #   matplotlib-inline
@@ -341,11 +350,13 @@ types-click==7.1.8
     # via -r requirements/tools.in
 types-pkg-resources==0.1.3
     # via -r requirements/tools.in
-types-pytz==2022.7.0.0
+types-pyopenssl==23.0.0.4
+    # via types-redis
+types-pytz==2022.7.1.2
     # via -r requirements/tools.in
-types-redis==4.3.21.7
+types-redis==4.5.1.5
     # via -r requirements/tools.in
-typing-extensions==4.4.0
+typing-extensions==4.5.0
     # via
     #   -r requirements/tools.in
     #   flake8-noqa
@@ -355,25 +366,25 @@ typing-extensions==4.4.0
     #   typing-inspect
 typing-inspect==0.8.0
     # via libcst
-urllib3==1.26.13
+urllib3==1.26.15
     # via
     #   requests
     #   twine
-virtualenv==20.17.1
+virtualenv==20.20.0
     # via tox
-wcwidth==0.2.5
+wcwidth==0.2.6
     # via prompt-toolkit
 webencodings==0.5.1
     # via bleach
 wheel==0.38.4
     # via pip-tools
-zipp==3.11.0
+zipp==3.15.0
     # via importlib-metadata
 
 # The following packages are considered to be unsafe in a requirements file:
-pip==22.3.1
+pip==23.0.1
     # via pip-tools
-setuptools==65.6.3
+setuptools==67.6.0
     # via
     #   nodeenv
     #   pip-tools
diff --git a/tooling/src/hypothesistooling/__main__.py b/tooling/src/hypothesistooling/__main__.py
index b4999d27f..b2f490dd9 100644
--- a/tooling/src/hypothesistooling/__main__.py
+++ b/tooling/src/hypothesistooling/__main__.py
@@ -382,8 +382,8 @@ PYTHONS = {
     "3.7": "3.7.16",
     "3.8": "3.8.16",
     "3.9": "3.9.16",
-    "3.10": "3.10.9",
-    "3.11": "3.11.1",
+    "3.10": "3.10.10",
+    "3.11": "3.11.2",
     "3.12": "3.12-dev",
     "pypy3.7": "pypy3.7-7.3.9",
     "pypy3.8": "pypy3.8-7.3.11",
diff --git a/whole-repo-tests/test_mypy.py b/whole-repo-tests/test_mypy.py
index 7cc2d2eec..ffdcb8c10 100644
--- a/whole-repo-tests/test_mypy.py
+++ b/whole-repo-tests/test_mypy.py
@@ -16,7 +16,7 @@ import pytest
 from hypothesistooling.projects.hypothesispython import PYTHON_SRC
 from hypothesistooling.scripts import pip_tool, tool_path
 
-PYTHON_VERSIONS = ["3.7", "3.8", "3.9", "3.10", "3.11"]
+PYTHON_VERSIONS = ["3.8", "3.9", "3.10", "3.11"]
 
 
 def test_mypy_passes_on_hypothesis():

test_patch: diff --git a/hypothesis-python/tests/array_api/common.py b/hypothesis-python/tests/array_api/common.py
index cf706d970..122195824 100644
--- a/hypothesis-python/tests/array_api/common.py
+++ b/hypothesis-python/tests/array_api/common.py
@@ -29,8 +29,7 @@ __all__ = [
 ]
 
 
-# This should be updated to the next spec release, which should include complex numbers
-MIN_VER_FOR_COMPLEX: NominalVersion = "draft"
+MIN_VER_FOR_COMPLEX: NominalVersion = "2022.12"
 if len(RELEASED_VERSIONS) > 1:
     assert MIN_VER_FOR_COMPLEX == RELEASED_VERSIONS[1]
 
diff --git a/hypothesis-python/tests/array_api/test_partial_adoptors.py b/hypothesis-python/tests/array_api/test_partial_adoptors.py
index 2f0f41062..b9ed5e849 100644
--- a/hypothesis-python/tests/array_api/test_partial_adoptors.py
+++ b/hypothesis-python/tests/array_api/test_partial_adoptors.py
@@ -8,8 +8,8 @@
 # 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/.
 
+import sys
 from copy import copy
-from functools import lru_cache
 from types import SimpleNamespace
 from typing import Tuple
 
@@ -30,7 +30,6 @@ from hypothesis.extra.array_api import (
 MOCK_WARN_MSG = f"determine.*{mock_xp.__name__}.*Array API"
 
 
-@lru_cache()
 def make_mock_xp(*, exclude: Tuple[str, ...] = ()) -> SimpleNamespace:
     xp = copy(mock_xp)
     assert isinstance(exclude, tuple)  # sanity check
@@ -138,3 +137,27 @@ def test_raises_on_invalid_dunder_version():
     xp.__array_api_version__ = None
     with pytest.raises(InvalidArgument):
         make_strategies_namespace(xp)
+
+
+@pytest.mark.filterwarnings(f"ignore:.*{MOCK_WARN_MSG}.*")
+def test_patch_torch_full(monkeypatch):
+    """When xp is torch, full() is patched so xps.arrays() can work.
+
+    See https://github.com/HypothesisWorks/hypothesis/pull/3542
+    """
+    xp = make_mock_xp()
+    old_full = copy(xp.full)
+
+    def bad_full(shape, *a, **kw):
+        bad_full.__counter += 1
+        if isinstance(shape, int):
+            raise AttributeError("xp.full() has not been patched correctly")
+        return old_full(shape, *a, **kw)
+
+    bad_full.__counter = 0
+    xp.full = bad_full
+    with monkeypatch.context() as m:
+        m.setitem(sys.modules, "torch", xp)
+        xps = make_strategies_namespace(xp)
+        xps.arrays(xp.int8, 5).example()
+        assert bad_full.__counter > 0  # sanity check
diff --git a/hypothesis-python/tests/common/debug.py b/hypothesis-python/tests/common/debug.py
index 8c75d2974..7f0b51a08 100644
--- a/hypothesis-python/tests/common/debug.py
+++ b/hypothesis-python/tests/common/debug.py
@@ -50,7 +50,7 @@ def minimal(definition, condition=lambda x: True, settings=None, timeout_after=1
     @given(definition)
     @Settings(
         parent=settings,
-        suppress_health_check=HealthCheck.all(),
+        suppress_health_check=list(HealthCheck),
         report_multiple_bugs=False,
         derandomize=True,
         database=None,
diff --git a/hypothesis-python/tests/common/utils.py b/hypothesis-python/tests/common/utils.py
index 3af74df6c..fb8a65e95 100644
--- a/hypothesis-python/tests/common/utils.py
+++ b/hypothesis-python/tests/common/utils.py
@@ -89,7 +89,7 @@ class ExcInfo:
     pass
 
 
-def fails_with(e):
+def fails_with(e, *, match=None):
     def accepts(f):
         @proxies(f)
         def inverted_test(*arguments, **kwargs):
@@ -98,7 +98,7 @@ def fails_with(e):
             # the `raises` context manager so that any problems in rigging the
             # PRNG don't accidentally count as the expected failure.
             with deterministic_PRNG():
-                with raises(e):
+                with raises(e, match=match):
                     f(*arguments, **kwargs)
 
         return inverted_test
diff --git a/hypothesis-python/tests/conjecture/common.py b/hypothesis-python/tests/conjecture/common.py
index 51f770c1c..1d177bc31 100644
--- a/hypothesis-python/tests/conjecture/common.py
+++ b/hypothesis-python/tests/conjecture/common.py
@@ -21,7 +21,7 @@ SOME_LABEL = calc_label_from_name("some label")
 
 
 TEST_SETTINGS = settings(
-    max_examples=5000, database=None, suppress_health_check=HealthCheck.all()
+    max_examples=5000, database=None, suppress_health_check=list(HealthCheck)
 )
 
 
@@ -56,7 +56,7 @@ def shrinking_from(start):
                 settings=settings(
                     max_examples=5000,
                     database=None,
-                    suppress_health_check=HealthCheck.all(),
+                    suppress_health_check=list(HealthCheck),
                 ),
             )
             runner.cached_test_function(start)
diff --git a/hypothesis-python/tests/conjecture/test_choice_tree.py b/hypothesis-python/tests/conjecture/test_choice_tree.py
index 5d566cb44..2e6c9c4b6 100644
--- a/hypothesis-python/tests/conjecture/test_choice_tree.py
+++ b/hypothesis-python/tests/conjecture/test_choice_tree.py
@@ -70,7 +70,6 @@ def test_all_filtered_child():
 
 
 def test_skips_over_exhausted_children():
-
     results = []
 
     def f(chooser):
diff --git a/hypothesis-python/tests/conjecture/test_data_tree.py b/hypothesis-python/tests/conjecture/test_data_tree.py
index d869a0ba8..91029d14e 100644
--- a/hypothesis-python/tests/conjecture/test_data_tree.py
+++ b/hypothesis-python/tests/conjecture/test_data_tree.py
@@ -19,7 +19,7 @@ from hypothesis.internal.conjecture.datatree import DataTree
 from hypothesis.internal.conjecture.engine import ConjectureRunner
 
 TEST_SETTINGS = settings(
-    max_examples=5000, database=None, suppress_health_check=HealthCheck.all()
+    max_examples=5000, database=None, suppress_health_check=list(HealthCheck)
 )
 
 
diff --git a/hypothesis-python/tests/conjecture/test_engine.py b/hypothesis-python/tests/conjecture/test_engine.py
index 5177bb20b..45b500165 100644
--- a/hypothesis-python/tests/conjecture/test_engine.py
+++ b/hypothesis-python/tests/conjecture/test_engine.py
@@ -483,7 +483,7 @@ def test_debug_data(capsys):
         settings=settings(
             max_examples=5000,
             database=None,
-            suppress_health_check=HealthCheck.all(),
+            suppress_health_check=list(HealthCheck),
             verbosity=Verbosity.debug,
         ),
     )
@@ -722,7 +722,7 @@ def test_database_clears_secondary_key():
     runner = ConjectureRunner(
         f,
         settings=settings(
-            max_examples=1, database=database, suppress_health_check=HealthCheck.all()
+            max_examples=1, database=database, suppress_health_check=list(HealthCheck)
         ),
         database_key=key,
     )
@@ -755,7 +755,7 @@ def test_database_uses_values_from_secondary_key():
     runner = ConjectureRunner(
         f,
         settings=settings(
-            max_examples=1, database=database, suppress_health_check=HealthCheck.all()
+            max_examples=1, database=database, suppress_health_check=list(HealthCheck)
         ),
         database_key=key,
     )
@@ -789,7 +789,7 @@ def test_exit_because_max_iterations():
     runner = ConjectureRunner(
         f,
         settings=settings(
-            max_examples=1, database=None, suppress_health_check=HealthCheck.all()
+            max_examples=1, database=None, suppress_health_check=list(HealthCheck)
         ),
     )
 
@@ -1220,7 +1220,7 @@ def test_populates_the_pareto_front():
             settings=settings(
                 max_examples=5000,
                 database=InMemoryExampleDatabase(),
-                suppress_health_check=HealthCheck.all(),
+                suppress_health_check=list(HealthCheck),
             ),
             database_key=b"stuff",
         )
@@ -1241,7 +1241,7 @@ def test_pareto_front_contains_smallest_valid_when_not_targeting():
             settings=settings(
                 max_examples=5000,
                 database=InMemoryExampleDatabase(),
-                suppress_health_check=HealthCheck.all(),
+                suppress_health_check=list(HealthCheck),
             ),
             database_key=b"stuff",
         )
@@ -1262,7 +1262,7 @@ def test_pareto_front_contains_different_interesting_reasons():
             settings=settings(
                 max_examples=5000,
                 database=InMemoryExampleDatabase(),
-                suppress_health_check=HealthCheck.all(),
+                suppress_health_check=list(HealthCheck),
             ),
             database_key=b"stuff",
         )
@@ -1285,7 +1285,7 @@ def test_database_contains_only_pareto_front():
         runner = ConjectureRunner(
             test,
             settings=settings(
-                max_examples=500, database=db, suppress_health_check=HealthCheck.all()
+                max_examples=500, database=db, suppress_health_check=list(HealthCheck)
             ),
             database_key=b"stuff",
         )
@@ -1326,7 +1326,7 @@ def test_clears_defunct_pareto_front():
             settings=settings(
                 max_examples=10000,
                 database=db,
-                suppress_health_check=HealthCheck.all(),
+                suppress_health_check=list(HealthCheck),
                 phases=[Phase.reuse],
             ),
             database_key=b"stuff",
diff --git a/hypothesis-python/tests/conjecture/test_pareto.py b/hypothesis-python/tests/conjecture/test_pareto.py
index 521f2ccf9..01c0f8ed5 100644
--- a/hypothesis-python/tests/conjecture/test_pareto.py
+++ b/hypothesis-python/tests/conjecture/test_pareto.py
@@ -29,7 +29,7 @@ def test_pareto_front_contains_different_interesting_reasons():
             settings=settings(
                 max_examples=5000,
                 database=InMemoryExampleDatabase(),
-                suppress_health_check=HealthCheck.all(),
+                suppress_health_check=list(HealthCheck),
             ),
             database_key=b"stuff",
         )
@@ -52,7 +52,7 @@ def test_database_contains_only_pareto_front():
         runner = ConjectureRunner(
             test,
             settings=settings(
-                max_examples=500, database=db, suppress_health_check=HealthCheck.all()
+                max_examples=500, database=db, suppress_health_check=list(HealthCheck)
             ),
             database_key=b"stuff",
         )
@@ -93,7 +93,7 @@ def test_clears_defunct_pareto_front():
             settings=settings(
                 max_examples=10000,
                 database=db,
-                suppress_health_check=HealthCheck.all(),
+                suppress_health_check=list(HealthCheck),
                 phases=[Phase.reuse],
             ),
             database_key=b"stuff",
@@ -121,7 +121,7 @@ def test_down_samples_the_pareto_front():
             settings=settings(
                 max_examples=1000,
                 database=db,
-                suppress_health_check=HealthCheck.all(),
+                suppress_health_check=list(HealthCheck),
                 phases=[Phase.reuse],
             ),
             database_key=b"stuff",
@@ -151,7 +151,7 @@ def test_stops_loading_pareto_front_if_interesting():
             settings=settings(
                 max_examples=1000,
                 database=db,
-                suppress_health_check=HealthCheck.all(),
+                suppress_health_check=list(HealthCheck),
                 phases=[Phase.reuse],
             ),
             database_key=b"stuff",
diff --git a/hypothesis-python/tests/conjecture/test_utils.py b/hypothesis-python/tests/conjecture/test_utils.py
index 595f5dece..02e6b6946 100644
--- a/hypothesis-python/tests/conjecture/test_utils.py
+++ b/hypothesis-python/tests/conjecture/test_utils.py
@@ -118,7 +118,7 @@ def test_too_small_to_be_useful_coin():
 @example([0, 2, 47])
 @settings(
     deadline=None,
-    suppress_health_check=HealthCheck.all(),
+    suppress_health_check=list(HealthCheck),
     phases=[Phase.explicit] if IN_COVERAGE_TESTS else settings.default.phases,
 )
 @given(st.lists(st.fractions(min_value=0, max_value=1), min_size=1))
diff --git a/hypothesis-python/tests/cover/test_async_def.py b/hypothesis-python/tests/cover/test_async_def.py
index d0b8bcdf8..67a17c672 100644
--- a/hypothesis-python/tests/cover/test_async_def.py
+++ b/hypothesis-python/tests/cover/test_async_def.py
@@ -15,7 +15,6 @@ from hypothesis import assume, given, strategies as st
 
 
 class TestAsyncioRun(TestCase):
-
     timeout = 5
 
     def execute_example(self, f):
diff --git a/hypothesis-python/tests/cover/test_asyncio.py b/hypothesis-python/tests/cover/test_asyncio.py
index 5fc1bc368..294063b74 100644
--- a/hypothesis-python/tests/cover/test_asyncio.py
+++ b/hypothesis-python/tests/cover/test_asyncio.py
@@ -29,7 +29,6 @@ else:
 
 
 class TestAsyncio(TestCase):
-
     timeout = 5
 
     def setUp(self):
diff --git a/hypothesis-python/tests/cover/test_custom_reprs.py b/hypothesis-python/tests/cover/test_custom_reprs.py
index 4669d3ee9..384044ed6 100644
--- a/hypothesis-python/tests/cover/test_custom_reprs.py
+++ b/hypothesis-python/tests/cover/test_custom_reprs.py
@@ -8,9 +8,11 @@
 # 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/.
 
+import re
+
 import pytest
 
-from hypothesis import given, strategies as st
+from hypothesis import given, settings, strategies as st
 
 
 def test_includes_non_default_args_in_repr():
@@ -64,3 +66,102 @@ def test_errors_are_deferred_until_repr_is_calculated():
 def test_iterables_repr_is_useful(it):
     # fairly hard-coded but useful; also ensures _values are inexhaustible
     assert repr(it) == f"iter({it._values!r})"
+
+
+class Foo:
+    def __init__(self, x: int) -> None:
+        self.x = x
+
+
+class Bar(Foo):
+    pass
+
+
+def test_reprs_as_created():
+    @given(foo=st.builds(Foo), bar=st.from_type(Bar), baz=st.none().map(Foo))
+    @settings(print_blob=False, max_examples=10_000)
+    def inner(foo, bar, baz):
+        assert baz.x is None
+        assert foo.x <= 0 or bar.x >= 0
+
+    with pytest.raises(AssertionError) as err:
+        inner()
+    expected = """
+Falsifying example: inner(
+    foo=Foo(x=1),
+    bar=Bar(x=-1),
+    baz=Foo(None),
+)
+"""
+    assert "\n".join(err.value.__notes__).strip() == expected.strip()
+
+
+def test_reprs_as_created_interactive():
+    @given(st.data())
+    @settings(print_blob=False, max_examples=10_000)
+    def inner(data):
+        bar = data.draw(st.builds(Bar, st.just(10)))
+        assert not bar.x
+
+    with pytest.raises(AssertionError) as err:
+        inner()
+    expected = """
+Falsifying example: inner(
+    data=data(...),
+)
+Draw 1: Bar(10)
+"""
+    assert "\n".join(err.value.__notes__).strip() == expected.strip()
+
+
+CONSTANT_FOO = Foo(None)
+
+
+def some_foo(*_):
+    return CONSTANT_FOO
+
+
+def test_as_created_reprs_fallback_for_distinct_calls_same_obj():
+    # If we have *different* calls which return the *same* object, we skip our
+    # nice repr because it's unclear which one we should use.
+    @given(st.builds(some_foo), st.builds(some_foo, st.none()))
+    @settings(print_blob=False, max_examples=10_000)
+    def inner(a, b):
+        assert a is not b
+
+    with pytest.raises(AssertionError) as err:
+        inner()
+    expected_re = r"""
+Falsifying example: inner\(
+    a=<.*Foo object at 0x[0-9A-Fa-f]+>,
+    b=<.*Foo object at 0x[0-9A-Fa-f]+>,
+\)
+""".strip()
+    got = "\n".join(err.value.__notes__).strip()
+    assert re.fullmatch(expected_re, got), got
+
+
+def test_reprs_as_created_consistent_calls_despite_indentation():
+    aas = "a" * 60
+    strat = st.builds(some_foo, st.just(aas))
+
+    # If we have multiple calls which return the same object, we can print their
+    # nice repr even if varying indentation means that they'll come out different!
+    @given(strat, st.builds(Bar, strat))
+    @settings(print_blob=False, max_examples=10_000)
+    def inner(a, b):
+        assert a == b
+
+    with pytest.raises(AssertionError) as err:
+        inner()
+    expected = f"""
+Falsifying example: inner(
+    a=some_foo({aas!r}),
+    b=Bar(
+        some_foo(
+            {aas!r},
+        ),
+    ),
+)
+"""
+    assert "\n".join(err.value.__notes__).strip() == expected.strip()
diff --git a/hypothesis-python/tests/cover/test_direct_strategies.py b/hypothesis-python/tests/cover/test_direct_strategies.py
index fec2c3768..1a500cdd7 100644
--- a/hypothesis-python/tests/cover/test_direct_strategies.py
+++ b/hypothesis-python/tests/cover/test_direct_strategies.py
@@ -99,6 +99,8 @@ def fn_ktest(*fnkwargs):
     (ds.floats, {"exclude_max": True}),  # because max_value=None
     (ds.floats, {"min_value": 1.8, "width": 32}),
     (ds.floats, {"max_value": 1.8, "width": 32}),
+    (ds.complex_numbers, {"min_magnitude": 1.8, "width": 64}),
+    (ds.complex_numbers, {"max_magnitude": 1.8, "width": 64}),
     (ds.fractions, {"min_value": 2, "max_value": 1}),
     (ds.fractions, {"min_value": math.nan}),
     (ds.fractions, {"max_value": math.nan}),
@@ -149,6 +151,12 @@ def fn_ktest(*fnkwargs):
     (ds.complex_numbers, {"min_magnitude": 3, "max_magnitude": 2}),
     (ds.complex_numbers, {"max_magnitude": 2, "allow_infinity": True}),
     (ds.complex_numbers, {"max_magnitude": 2, "allow_nan": True}),
+    (ds.complex_numbers, {"width": None}),
+    # Conceivable mistake when misunderstanding width for individual component widths:
+    (ds.complex_numbers, {"width": 16}),
+    # Unsupported as of now:
+    (ds.complex_numbers, {"width": 196}),
+    (ds.complex_numbers, {"width": 256}),
     (ds.fixed_dictionaries, {"mapping": "fish"}),
     (ds.fixed_dictionaries, {"mapping": {1: "fish"}}),
     (ds.fixed_dictionaries, {"mapping": {}, "optional": "fish"}),
@@ -246,6 +254,9 @@ def test_validates_keyword_arguments(fn, kwargs):
     (ds.complex_numbers, {"allow_nan": False, "allow_infinity": True}),
     (ds.complex_numbers, {"allow_nan": False, "allow_infinity": False}),
     (ds.complex_numbers, {"max_magnitude": math.inf, "allow_infinity": True}),
+    (ds.complex_numbers, {"width": 32}),
+    (ds.complex_numbers, {"width": 64}),
+    (ds.complex_numbers, {"width": 128}),
     (ds.sampled_from, {"elements": [1]}),
     (ds.sampled_from, {"elements": [1, 2, 3]}),
     (ds.fixed_dictionaries, {"mapping": {1: ds.integers()}}),
diff --git a/hypothesis-python/tests/cover/test_example.py b/hypothesis-python/tests/cover/test_example.py
index 493aba92d..8ad8f5344 100644
--- a/hypothesis-python/tests/cover/test_example.py
+++ b/hypothesis-python/tests/cover/test_example.py
@@ -87,6 +87,14 @@ def test_interactive_example_does_not_emit_warning():
     child.sendline("quit(code=0)")
 
 
+@fails_with(KeyboardInterrupt)
+@example(1)
+@example(2)
+@given(st.none())
+def test_raises_keyboardinterrupt_immediately(_):
+    raise KeyboardInterrupt
+
+
 def identity(decorator):
     # The "identity function hack" from https://peps.python.org/pep-0614/
     # Method-chaining decorators are otherwise a syntax error in Python <= 3.8
@@ -104,3 +112,93 @@ def test_invalid_example_via():
         example(x=False).via(100)  # not a string!
     with pytest.raises(TypeError):
         example(x=False).via("abc", "def")  # too many args
+
+
+@pytest.mark.parametrize(
+    "kw",
+    [
+        {"condition": None},  # must be a bool
+        {"reason": None},  # must be a string
+        {"raises": None},  # not a BaseException (or even a type)
+        {"raises": int},  # not a BaseException
+        {"raises": [Exception]},  # not a tuple
+        {"raises": (None,)},  # tuple containing a non-BaseException
+        {"raises": ()},  # empty tuple doesn't make sense here
+        # raising non-failure exceptions, eg KeyboardInterrupt, is tested below
+    ],
+    ids=repr,
+)
+def test_invalid_example_xfail_arguments(kw):
+    with pytest.raises(InvalidArgument):
+        example(x=False).xfail(**kw)
+
+
+@identity(example(True).xfail())
+@identity(example(True).xfail(reason="ignored for passing tests"))
+@identity(example(True).xfail(raises=KeyError))
+@identity(example(True).xfail(raises=(KeyError, ValueError)))
+@identity(example(True).xfail(True, reason="..."))
+@identity(example(False).xfail(condition=False))
+@given(st.none())
+def test_many_xfail_example_decorators(fails):
+    if fails:
+        raise KeyError
+
+
+@fails_with(AssertionError)
+@identity(example(x=True).xfail(raises=KeyError))
+@given(st.none())
+def test_xfail_reraises_non_specified_exception(x):
+    assert not x
+
+
+@fails_with(
+    InvalidArgument,
+    match=r"@example\(x=True\) raised an expected BaseException\('msg'\), "
+    r"but Hypothesis does not treat this as a test failure",
+)
+@identity(example(True).xfail())
+@given(st.none())
+def test_must_raise_a_failure_exception(x):
+    if x:
+        raise BaseException("msg")
+
+
+@fails_with(
+    AssertionError,
+    match=r"Expected an exception from @example\(x=None\), but no exception was raised.",
+)
+@identity(example(None).xfail())
+@given(st.none())
+def test_error_on_unexpected_pass_base(x):
+    pass
+
+
+@fails_with(
+    AssertionError,
+    match=r"Expected an AssertionError from @example\(x=None\), but no exception was raised.",
+)
+@identity(example(None).xfail(raises=AssertionError))
+@given(st.none())
+def test_error_on_unexpected_pass_single(x):
+    pass
+
+
+@fails_with(
+    AssertionError,
+    match=r"Expected an AssertionError from @example\(x=None\), but no exception was raised.",
+)
+@identity(example(None).xfail(raises=(AssertionError,)))
+@given(st.none())
+def test_error_on_unexpected_pass_single_elem_tuple(x):
+    pass
+
+
+@fails_with(
+    AssertionError,
+    match=r"Expected a KeyError, or ValueError from @example\(x=None\), but no exception was raised.",
+)
+@identity(example(None).xfail(raises=(KeyError, ValueError)))
+@given(st.none())
+def test_error_on_unexpected_pass_multi(x):
+    pass
diff --git a/hypothesis-python/tests/cover/test_flakiness.py b/hypothesis-python/tests/cover/test_flakiness.py
index 62251d11d..18f7f7634 100644
--- a/hypothesis-python/tests/cover/test_flakiness.py
+++ b/hypothesis-python/tests/cover/test_flakiness.py
@@ -101,7 +101,7 @@ def test_failure_sequence_inducing(building, testing, rnd):
     @settings(
         verbosity=Verbosity.quiet,
         database=None,
-        suppress_health_check=HealthCheck.all(),
+        suppress_health_check=list(HealthCheck),
         phases=no_shrink,
     )
     def test(x):
diff --git a/hypothesis-python/tests/cover/test_health_checks.py b/hypothesis-python/tests/cover/test_health_checks.py
index 406c56b23..c1b115159 100644
--- a/hypothesis-python/tests/cover/test_health_checks.py
+++ b/hypothesis-python/tests/cover/test_health_checks.py
@@ -57,7 +57,7 @@ def test_slow_generation_inline_fails_a_health_check():
 def test_default_health_check_can_weaken_specific():
     import random
 
-    @settings(HEALTH_CHECK_SETTINGS, suppress_health_check=HealthCheck.all())
+    @settings(HEALTH_CHECK_SETTINGS, suppress_health_check=list(HealthCheck))
     @given(st.lists(st.integers(), min_size=1))
     def test(x):
         random.choice(x)
@@ -152,22 +152,13 @@ def test_the_slow_test_health_check_can_be_disabled():
 
 def test_the_slow_test_health_only_runs_if_health_checks_are_on():
     @given(st.integers())
-    @settings(suppress_health_check=HealthCheck.all(), deadline=None)
+    @settings(suppress_health_check=list(HealthCheck), deadline=None)
     def a(x):
         time.sleep(1000)
 
     a()
 
 
-def test_returning_non_none_does_not_fail_if_health_check_disabled():
-    @given(st.integers())
-    @settings(suppress_health_check=HealthCheck.all())
-    def a(x):
-        return 1
-
-    a()
-
-
 def test_large_base_example_fails_health_check():
     @given(st.binary(min_size=7000, max_size=7000))
     def test(b):
diff --git a/hypothesis-python/tests/cover/test_numerics.py b/hypothesis-python/tests/cover/test_numerics.py
index de6316460..66d9899e6 100644
--- a/hypothesis-python/tests/cover/test_numerics.py
+++ b/hypothesis-python/tests/cover/test_numerics.py
@@ -31,7 +31,7 @@ from hypothesis.strategies import (
 from tests.common.debug import find_any
 
 
-@settings(suppress_health_check=HealthCheck.all())
+@settings(suppress_health_check=list(HealthCheck))
 @given(data())
 def test_fuzz_floats_bounds(data):
     width = data.draw(sampled_from([64, 32, 16]))
diff --git a/hypothesis-python/tests/cover/test_pretty.py b/hypothesis-python/tests/cover/test_pretty.py
index 568f025e3..549d6a9c2 100644
--- a/hypothesis-python/tests/cover/test_pretty.py
+++ b/hypothesis-python/tests/cover/test_pretty.py
@@ -48,7 +48,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 """
 
 import re
+import warnings
 from collections import Counter, OrderedDict, defaultdict, deque
+from enum import Enum
 
 import pytest
 
@@ -66,7 +68,7 @@ class MyList:
             p.text("MyList(...)")
         else:
             with p.group(3, "MyList(", ")"):
-                for (i, child) in enumerate(self.content):
+                for i, child in enumerate(self.content):
                     if i:
                         p.text(",")
                         p.breakable()
@@ -135,6 +137,12 @@ def test_dict():
     assert pretty.pretty({1: 1}) == "{1: 1}"
     assert pretty.pretty({1: 1, 0: 0}) == "{1: 1, 0: 0}"
 
+    # Check that pretty-printing doesn't trigger a BytesWarning under `python -bb`
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore", BytesWarning)
+        x = {"": 0, b"": 0}
+    assert pretty.pretty(x) == "{'': 0, b'': 0}"
+
 
 def test_tuple():
     assert pretty.pretty(()) == "()"
@@ -613,3 +621,11 @@ def test_repr_call(func_name):
     assert _repr_call(func_name, (aas,), {}) == f"{fn}(\n    {aas!r},\n)"
     assert _repr_call(func_name, (), {"a": 1, "b": 2}) == f"{fn}(a=1, b=2)"
     assert _repr_call(func_name, (), {"x": aas}) == f"{fn}(\n    x={aas!r},\n)"
+
+
+class AnEnum(Enum):
+    SOME_MEMBER = 1
+
+
+def test_pretty_prints_enums_as_code():
+    assert pretty.pretty(AnEnum.SOME_MEMBER) == "AnEnum.SOME_MEMBER"
diff --git a/hypothesis-python/tests/cover/test_random_module.py b/hypothesis-python/tests/cover/test_random_module.py
index f8979db83..01ca6b88c 100644
--- a/hypothesis-python/tests/cover/test_random_module.py
+++ b/hypothesis-python/tests/cover/test_random_module.py
@@ -16,16 +16,17 @@ import pytest
 from hypothesis import core, find, given, register_random, strategies as st
 from hypothesis.errors import HypothesisWarning, InvalidArgument
 from hypothesis.internal import entropy
-from hypothesis.internal.compat import PYPY
+from hypothesis.internal.compat import GRAALPY, PYPY
 from hypothesis.internal.entropy import deterministic_PRNG
 
 
-def gc_on_pypy():
+def gc_collect():
     # CPython uses reference counting, so objects (without circular refs)
     # are collected immediately on `del`, breaking weak references.
-    # PyPy doesn't, so we use this function in tests before counting the
+    # Python implementations with other garbage collection strategies may
+    # or may not, so we use this function in tests before counting the
     # surviving references to ensure that they're deterministic.
-    if PYPY:
+    if PYPY or GRAALPY:
         gc.collect()
 
 
@@ -58,14 +59,14 @@ def test_cannot_register_non_Random():
     "ignore:It looks like `register_random` was passed an object that could be garbage collected"
 )
 def test_registering_a_Random_is_idempotent():
-    gc_on_pypy()
+    gc_collect()
     n_registered = len(entropy.RANDOMS_TO_MANAGE)
     r = random.Random()
     register_random(r)
     register_random(r)
     assert len(entropy.RANDOMS_TO_MANAGE) == n_registered + 1
     del r
-    gc_on_pypy()
+    gc_collect()
     assert len(entropy.RANDOMS_TO_MANAGE) == n_registered
 
 
@@ -134,7 +135,6 @@ def test_given_does_not_pollute_state():
 
 def test_find_does_not_pollute_state():
     with deterministic_PRNG():
-
         find(st.random_module(), lambda r: True)
         state_a = random.getstate()
         state_a2 = core._hypothesis_global_random.getstate()
@@ -151,7 +151,7 @@ def test_find_does_not_pollute_state():
     "ignore:It looks like `register_random` was passed an object that could be garbage collected"
 )
 def test_evil_prng_registration_nonsense():
-    gc_on_pypy()
+    gc_collect()
     n_registered = len(entropy.RANDOMS_TO_MANAGE)
     r1, r2, r3 = random.Random(1), random.Random(2), random.Random(3)
     s2 = r2.getstate()
@@ -166,7 +166,7 @@ def test_evil_prng_registration_nonsense():
 
     with deterministic_PRNG(0):
         del r1
-        gc_on_pypy()
+        gc_collect()
         assert k not in entropy.RANDOMS_TO_MANAGE, "r1 has been garbage-collected"
         assert len(entropy.RANDOMS_TO_MANAGE) == n_registered + 1
 
@@ -230,5 +230,5 @@ def test_register_random_within_nested_function_scope():
         assert len(entropy.RANDOMS_TO_MANAGE) == n_registered + 1
 
     f()
-    gc_on_pypy()
+    gc_collect()
     assert len(entropy.RANDOMS_TO_MANAGE) == n_registered
diff --git a/hypothesis-python/tests/cover/test_settings.py b/hypothesis-python/tests/cover/test_settings.py
index e3ca7376c..b650933cd 100644
--- a/hypothesis-python/tests/cover/test_settings.py
+++ b/hypothesis-python/tests/cover/test_settings.py
@@ -17,6 +17,7 @@ import pytest
 
 from hypothesis import example, given, strategies as st
 from hypothesis._settings import (
+    HealthCheck,
     Phase,
     Verbosity,
     default_variable,
@@ -33,7 +34,7 @@ from hypothesis.errors import (
 from hypothesis.stateful import RuleBasedStateMachine, rule
 from hypothesis.utils.conventions import not_set
 
-from tests.common.utils import counts_calls, fails_with
+from tests.common.utils import counts_calls, fails_with, validate_deprecation
 
 
 def test_has_docstrings():
@@ -482,3 +483,18 @@ def test_note_deprecation_checks_has_codemod():
         match="The `hypothesis codemod` command-line tool",
     ):
         note_deprecation("This is bad", since="2021-01-01", has_codemod=True)
+
+
+def test_deprecated_settings_warn_on_set_settings():
+    with validate_deprecation():
+        settings(suppress_health_check=[HealthCheck.return_value])
+    with validate_deprecation():
+        settings(suppress_health_check=[HealthCheck.not_a_test_method])
+
+
+def test_deprecated_settings_not_in_settings_all_list():
+    al = HealthCheck.all()
+    ls = list(HealthCheck)
+    assert al == ls
+    assert HealthCheck.return_value not in ls
+    assert HealthCheck.not_a_test_method not in ls
diff --git a/hypothesis-python/tests/cover/test_slippage.py b/hypothesis-python/tests/cover/test_slippage.py
index 4561000fc..5f0884822 100644
--- a/hypothesis-python/tests/cover/test_slippage.py
+++ b/hypothesis-python/tests/cover/test_slippage.py
@@ -55,7 +55,7 @@ def test_raises_multiple_failures_with_varying_type():
 
 
 def test_shows_target_scores_with_multiple_failures():
-    @settings(database=None, max_examples=100)
+    @settings(derandomize=True, max_examples=10_000)
     @given(st.integers())
     def test(i):
         target(i)
diff --git a/hypothesis-python/tests/cover/test_stateful.py b/hypothesis-python/tests/cover/test_stateful.py
index ce6f60b55..8a71d30d6 100644
--- a/hypothesis-python/tests/cover/test_stateful.py
+++ b/hypothesis-python/tests/cover/test_stateful.py
@@ -9,18 +9,16 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 import base64
-import sys
 from collections import defaultdict
 
 import pytest
 from _pytest.outcomes import Failed, Skipped
 from pytest import raises
 
-from hypothesis import __version__, reproduce_failure, seed, settings as Settings
+from hypothesis import Phase, __version__, reproduce_failure, seed, settings as Settings
 from hypothesis.control import current_build_context
 from hypothesis.database import ExampleDatabase
 from hypothesis.errors import DidNotReproduce, Flaky, InvalidArgument, InvalidDefinition
-from hypothesis.internal.compat import PYPY
 from hypothesis.internal.entropy import deterministic_PRNG
 from hypothesis.stateful import (
     Bundle,
@@ -646,7 +644,7 @@ state.teardown()
 
 
 def test_invariant_present_in_falsifying_example():
-    @Settings(print_blob=False)
+    @Settings(print_blob=False, phases=tuple(Phase)[:-1])
     class BadRuleWithGoodInvariants(RuleBasedStateMachine):
         def __init__(self):
             super().__init__()
@@ -678,7 +676,7 @@ def test_invariant_present_in_falsifying_example():
     with pytest.raises(ValueError) as err:
         run_state_machine_as_test(BadRuleWithGoodInvariants)
 
-    expected = """\
+    expected = """
 Falsifying example:
 state = BadRuleWithGoodInvariants()
 state.invariant_1()
@@ -690,18 +688,10 @@ state.invariant_1()
 state.invariant_2()
 state.invariant_3()
 state.rule_1()
-state.teardown()"""
-
-    if PYPY or sys.gettrace():  # explain mode disabled in these cases
-        result = "\n".join(err.value.__notes__)
-    else:
-        # Non-PyPy runs include explain mode, but we skip the final line because
-        # it includes the absolute path, which of course varies between machines.
-        expected += """
-Explanation:
-    These lines were always and only run by failing examples:"""
-        result = "\n".join(err.value.__notes__[:-1])
+state.teardown()
+""".strip()
 
+    result = "\n".join(err.value.__notes__).strip()
     assert expected == result
 
 
@@ -1192,3 +1182,34 @@ def test_deprecated_target_consumes_bundle():
     # definition-time already anyway, so it's not *worse* than the status quo.
     with validate_deprecation():
         rule(target=consumes(Bundle("b")))
+
+
+@Settings(stateful_step_count=5)
+class MinStepsMachine(RuleBasedStateMachine):
+    @initialize()
+    def init_a(self):
+        self.a = 0
+
+    @rule()
+    def inc(self):
+        self.a += 1
+
+    @invariant()
+    def not_too_many_steps(self):
+        assert self.a < 10
+
+    def teardown(self):
+        assert self.a >= 2
+
+
+def test_min_steps_argument():
+    # You must pass a non-negative integer...
+    for n_steps in (-1, "nan", 5.0):
+        with pytest.raises(InvalidArgument):
+            run_state_machine_as_test(MinStepsMachine, _min_steps=n_steps)
+
+    # and if you do, we'll take at least that many steps
+    run_state_machine_as_test(MinStepsMachine, _min_steps=3)
+
+    # (oh, and it's OK if you ask for more than we're actually going to take)
+    run_state_machine_as_test(MinStepsMachine, _min_steps=20)
diff --git a/hypothesis-python/tests/cover/test_statistical_events.py b/hypothesis-python/tests/cover/test_statistical_events.py
index 0230e431d..56b373376 100644
--- a/hypothesis-python/tests/cover/test_statistical_events.py
+++ b/hypothesis-python/tests/cover/test_statistical_events.py
@@ -44,7 +44,7 @@ def unique_events(stats):
 
 def test_notes_hard_to_satisfy():
     @given(st.integers())
-    @settings(suppress_health_check=HealthCheck.all())
+    @settings(suppress_health_check=list(HealthCheck))
     def test(i):
         assume(i == 13)
 
diff --git a/hypothesis-python/tests/cover/test_testdecorators.py b/hypothesis-python/tests/cover/test_testdecorators.py
index 2bd583e65..bc0dece41 100644
--- a/hypothesis-python/tests/cover/test_testdecorators.py
+++ b/hypothesis-python/tests/cover/test_testdecorators.py
@@ -254,6 +254,7 @@ def test_is_an_endpoint(x):
 def test_breaks_bounds():
     @fails
     @given(x=integers())
+    @settings(derandomize=True, max_examples=10_000)
     def test_is_bounded(t, x):
         assert x < t
 
@@ -370,7 +371,7 @@ def test_named_tuples_are_of_right_type(litter):
 
 @fails_with(AttributeError)
 @given(integers().map(lambda x: x.nope))
-@settings(suppress_health_check=HealthCheck.all())
+@settings(suppress_health_check=list(HealthCheck))
 def test_fails_in_reify(x):
     pass
 
diff --git a/hypothesis-python/tests/cover/test_verbosity.py b/hypothesis-python/tests/cover/test_verbosity.py
index f26ce8b22..0acc6d0db 100644
--- a/hypothesis-python/tests/cover/test_verbosity.py
+++ b/hypothesis-python/tests/cover/test_verbosity.py
@@ -64,7 +64,6 @@ def test_includes_progress_in_verbose_mode():
 
 
 def test_prints_initial_attempts_on_find():
-
     with capture_verbosity() as o:
 
         def foo():
diff --git a/hypothesis-python/tests/django/toystore/test_basic_configuration.py b/hypothesis-python/tests/django/toystore/test_basic_configuration.py
index 1e08417be..87659a537 100644
--- a/hypothesis-python/tests/django/toystore/test_basic_configuration.py
+++ b/hypothesis-python/tests/django/toystore/test_basic_configuration.py
@@ -17,7 +17,7 @@ from django.test import TestCase as DjangoTestCase
 from hypothesis import HealthCheck, Verbosity, given, settings
 from hypothesis.errors import InvalidArgument
 from hypothesis.extra.django import TestCase, TransactionTestCase
-from hypothesis.internal.compat import PYPY
+from hypothesis.internal.compat import GRAALPY, PYPY
 from hypothesis.strategies import integers
 
 from tests.django.toystore.models import Company
@@ -40,7 +40,7 @@ class TestConstraintsWithTransactions(SomeStuff, TestCase):
     pass
 
 
-if not PYPY:
+if not (PYPY or GRAALPY):
     # xfail
     # This is excessively slow in general, but particularly on pypy. We just
     # disable it altogether there as it's a niche case.
@@ -57,7 +57,7 @@ class TestWorkflow(VanillaTestCase):
         class LocalTest(TestCase):
             @given(integers().map(break_the_db))
             @settings(
-                suppress_health_check=HealthCheck.all(), verbosity=Verbosity.quiet
+                suppress_health_check=list(HealthCheck), verbosity=Verbosity.quiet
             )
             def test_does_not_break_other_things(self, unused):
                 pass
diff --git a/hypothesis-python/tests/ghostwriter/example_code/__init__.py b/hypothesis-python/tests/ghostwriter/example_code/__init__.py
new file mode 100644
index 000000000..fcb1ac653
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/example_code/__init__.py
@@ -0,0 +1,9 @@
+# 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/.
diff --git a/hypothesis-python/tests/ghostwriter/example_code/future_annotations.py b/hypothesis-python/tests/ghostwriter/example_code/future_annotations.py
new file mode 100644
index 000000000..b2a1976db
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/example_code/future_annotations.py
@@ -0,0 +1,34 @@
+# 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 __future__ import annotations
+
+import collections.abc
+
+
+class CustomClass:
+    def __init__(self, number: int) -> None:
+        self.number = number
+
+
+def add_custom_classes(c1: CustomClass, c2: CustomClass | None = None) -> CustomClass:
+    if c2 is None:
+        return CustomClass(c1.number)
+    return CustomClass(c1.number + c2.number)
+
+
+def merge_dicts(
+    map1: collections.abc.Mapping[str, int], map2: collections.abc.Mapping[str, int]
+) -> collections.abc.Mapping[str, int]:
+    return {**map1, **map2}
+
+
+def invalid_types(attr1: int, attr2: UnknownClass, attr3: str) -> None:
+    pass
diff --git a/hypothesis-python/tests/ghostwriter/recorded/add_custom_classes.txt b/hypothesis-python/tests/ghostwriter/recorded/add_custom_classes.txt
new file mode 100644
index 000000000..e315456ec
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/add_custom_classes.txt
@@ -0,0 +1,15 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+import example_code.future_annotations
+import typing
+from example_code.future_annotations import CustomClass
+from hypothesis import given, strategies as st
+
+
+@given(c1=st.builds(CustomClass), c2=st.one_of(st.none(), st.builds(CustomClass)))
+def test_fuzz_add_custom_classes(
+    c1: example_code.future_annotations.CustomClass,
+    c2: typing.Union[example_code.future_annotations.CustomClass, None],
+) -> None:
+    example_code.future_annotations.add_custom_classes(c1=c1, c2=c2)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/addition_op_magic.txt b/hypothesis-python/tests/ghostwriter/recorded/addition_op_magic.txt
index 809f4b05e..23827cb90 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/addition_op_magic.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/addition_op_magic.txt
@@ -8,19 +8,19 @@ add_operands = st.floats()
 
 
 @given(a=add_operands, b=add_operands, c=add_operands)
-def test_associative_binary_operation_add(a, b, c):
+def test_associative_binary_operation_add(a: float, b: float, c) -> None:
     left = test_expected_output.add(a=a, b=test_expected_output.add(a=b, b=c))
     right = test_expected_output.add(a=test_expected_output.add(a=a, b=b), b=c)
     assert left == right, (left, right)
 
 
 @given(a=add_operands, b=add_operands)
-def test_commutative_binary_operation_add(a, b):
+def test_commutative_binary_operation_add(a: float, b: float) -> None:
     left = test_expected_output.add(a=a, b=b)
     right = test_expected_output.add(a=b, b=a)
     assert left == right, (left, right)
 
 
 @given(a=add_operands)
-def test_identity_binary_operation_add(a):
+def test_identity_binary_operation_add(a: float) -> None:
     assert a == test_expected_output.add(a=a, b=0.0)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/addition_op_multimagic.txt b/hypothesis-python/tests/ghostwriter/recorded/addition_op_multimagic.txt
index 5c1e52d3a..194ff573e 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/addition_op_multimagic.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/addition_op_multimagic.txt
@@ -8,7 +8,7 @@ from hypothesis import given, strategies as st
 
 
 @given(a=st.floats(), b=st.floats())
-def test_equivalent_add_add_add(a, b):
+def test_equivalent_add_add_add(a: float, b: float) -> None:
     result_0_add = _operator.add(a, b)
     result_1_add = numpy.add(a, b)
     result_2_add = test_expected_output.add(a=a, b=b)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/division_binop_error_handler.txt b/hypothesis-python/tests/ghostwriter/recorded/division_binop_error_handler.txt
index 869a57989..4248fe575 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/division_binop_error_handler.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/division_binop_error_handler.txt
@@ -8,19 +8,19 @@ divide_operands = st.integers()
 
 
 @given(a=divide_operands, b=divide_operands, c=divide_operands)
-def test_associative_binary_operation_divide(a, b, c):
+def test_associative_binary_operation_divide(a: int, b: int, c) -> None:
     left = test_expected_output.divide(a=a, b=test_expected_output.divide(a=b, b=c))
     right = test_expected_output.divide(a=test_expected_output.divide(a=a, b=b), b=c)
     assert left == right, (left, right)
 
 
 @given(a=divide_operands, b=divide_operands)
-def test_commutative_binary_operation_divide(a, b):
+def test_commutative_binary_operation_divide(a: int, b: int) -> None:
     left = test_expected_output.divide(a=a, b=b)
     right = test_expected_output.divide(a=b, b=a)
     assert left == right, (left, right)
 
 
 @given(a=divide_operands)
-def test_identity_binary_operation_divide(a):
+def test_identity_binary_operation_divide(a: int) -> None:
     assert a == test_expected_output.divide(a=a, b=1)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/division_fuzz_error_handler.txt b/hypothesis-python/tests/ghostwriter/recorded/division_fuzz_error_handler.txt
index 43bf350d0..eeddfcd59 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/division_fuzz_error_handler.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/division_fuzz_error_handler.txt
@@ -6,7 +6,7 @@ from hypothesis import given, reject, strategies as st
 
 
 @given(a=st.integers(), b=st.integers())
-def test_fuzz_divide(a, b):
+def test_fuzz_divide(a: int, b: int) -> None:
     try:
         test_expected_output.divide(a=a, b=b)
     except ZeroDivisionError:
diff --git a/hypothesis-python/tests/ghostwriter/recorded/division_operator_with_annotations.txt b/hypothesis-python/tests/ghostwriter/recorded/division_operator_with_annotations.txt
new file mode 100644
index 000000000..21eb93d17
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/division_operator_with_annotations.txt
@@ -0,0 +1,14 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+import _operator
+from hypothesis import given, strategies as st
+
+# TODO: replace st.nothing() with an appropriate strategy
+
+truediv_operands = st.nothing()
+
+
+@given(a=truediv_operands)
+def test_identity_binary_operation_truediv(a) -> None:
+    assert a == _operator.truediv(a, "identity element here")
diff --git a/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_arithmeticerror_handler.txt b/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_arithmeticerror_handler.txt
index 825f64294..31f9f7d42 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_arithmeticerror_handler.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_arithmeticerror_handler.txt
@@ -7,7 +7,7 @@ from hypothesis import given, reject, strategies as st
 
 
 @given(a=st.integers(), b=st.integers())
-def test_roundtrip_divide_mul(a, b):
+def test_roundtrip_divide_mul(a: int, b: int) -> None:
     try:
         value0 = test_expected_output.divide(a=a, b=b)
         value1 = _operator.mul(value0, b)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_error_handler.txt b/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_error_handler.txt
index 719d9067a..7f2b36076 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_error_handler.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_error_handler.txt
@@ -7,7 +7,7 @@ from hypothesis import given, reject, strategies as st
 
 
 @given(a=st.integers(), b=st.integers())
-def test_roundtrip_divide_mul(a, b):
+def test_roundtrip_divide_mul(a: int, b: int) -> None:
     try:
         value0 = test_expected_output.divide(a=a, b=b)
     except ZeroDivisionError:
diff --git a/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_error_handler_without_annotations.txt b/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_error_handler_without_annotations.txt
new file mode 100644
index 000000000..719d9067a
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_error_handler_without_annotations.txt
@@ -0,0 +1,16 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+import _operator
+import test_expected_output
+from hypothesis import given, reject, strategies as st
+
+
+@given(a=st.integers(), b=st.integers())
+def test_roundtrip_divide_mul(a, b):
+    try:
+        value0 = test_expected_output.divide(a=a, b=b)
+    except ZeroDivisionError:
+        reject()
+    value1 = _operator.mul(value0, b)
+    assert a == value1, (a, value1)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_typeerror_handler.txt b/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_typeerror_handler.txt
index 99c4ac40c..4fc16c796 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_typeerror_handler.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/division_roundtrip_typeerror_handler.txt
@@ -7,7 +7,7 @@ from hypothesis import given, reject, strategies as st
 
 
 @given(a=st.integers(), b=st.integers())
-def test_roundtrip_divide_mul(a, b):
+def test_roundtrip_divide_mul(a: int, b: int) -> None:
     try:
         try:
             value0 = test_expected_output.divide(a=a, b=b)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/fuzz_classmethod.txt b/hypothesis-python/tests/ghostwriter/recorded/fuzz_classmethod.txt
index 2f41f4445..331b80840 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/fuzz_classmethod.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/fuzz_classmethod.txt
@@ -6,5 +6,5 @@ from hypothesis import given, strategies as st
 
 
 @given(arg=st.integers())
-def test_fuzz_A_Class_a_classmethod(arg):
+def test_fuzz_A_Class_a_classmethod(arg: int) -> None:
     test_expected_output.A_Class.a_classmethod(arg=arg)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/fuzz_sorted_with_annotations.txt b/hypothesis-python/tests/ghostwriter/recorded/fuzz_sorted_with_annotations.txt
new file mode 100644
index 000000000..dcfdde733
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/fuzz_sorted_with_annotations.txt
@@ -0,0 +1,13 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+from hypothesis import given, strategies as st
+
+
+@given(
+    iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())),
+    key=st.none(),
+    reverse=st.booleans(),
+)
+def test_fuzz_sorted(iterable, key, reverse) -> None:
+    sorted(iterable, key=key, reverse=reverse)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/fuzz_staticmethod.txt b/hypothesis-python/tests/ghostwriter/recorded/fuzz_staticmethod.txt
index 1ae8f7920..9a8cce2eb 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/fuzz_staticmethod.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/fuzz_staticmethod.txt
@@ -6,5 +6,5 @@ from hypothesis import given, strategies as st
 
 
 @given(arg=st.integers())
-def test_fuzz_A_Class_a_staticmethod(arg):
+def test_fuzz_A_Class_a_staticmethod(arg: int) -> None:
     test_expected_output.A_Class.a_staticmethod(arg=arg)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/hypothesis_module_magic.txt b/hypothesis-python/tests/ghostwriter/recorded/hypothesis_module_magic.txt
index 4b514590c..75ddec676 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/hypothesis_module_magic.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/hypothesis_module_magic.txt
@@ -1,7 +1,11 @@
 # This test code was written by the `hypothesis.extra.ghostwriter` module
 # and is provided under the Creative Commons Zero public domain dedication.
 
+import datetime
 import hypothesis
+import hypothesis.strategies
+import hypothesis.strategies._internal.strategies
+import random
 import typing
 from hypothesis import given, settings, strategies as st
 from hypothesis.strategies._internal.strategies import Ex
@@ -9,12 +13,12 @@ from random import Random
 
 
 @given(condition=st.from_type(object))
-def test_fuzz_assume(condition):
+def test_fuzz_assume(condition: object) -> None:
     hypothesis.assume(condition=condition)
 
 
 @given(value=st.text())
-def test_fuzz_event(value):
+def test_fuzz_event(value: str) -> None:
     hypothesis.event(value=value)
 
 
@@ -25,7 +29,15 @@ def test_fuzz_event(value):
     random=st.one_of(st.none(), st.builds(Random)),
     database_key=st.one_of(st.none(), st.binary()),
 )
-def test_fuzz_find(specifier, condition, settings, random, database_key):
+def test_fuzz_find(
+    specifier: hypothesis.strategies.SearchStrategy[
+        hypothesis.strategies._internal.strategies.Ex
+    ],
+    condition: typing.Callable[[typing.Any], bool],
+    settings: typing.Union[hypothesis.settings, None],
+    random: typing.Union[random.Random, None],
+    database_key: typing.Union[bytes, None],
+) -> None:
     hypothesis.find(
         specifier=specifier,
         condition=condition,
@@ -36,22 +48,22 @@ def test_fuzz_find(specifier, condition, settings, random, database_key):
 
 
 @given(value=st.text())
-def test_fuzz_note(value):
+def test_fuzz_note(value: str) -> None:
     hypothesis.note(value=value)
 
 
 @given(r=st.builds(Random))
-def test_fuzz_register_random(r):
+def test_fuzz_register_random(r: random.Random) -> None:
     hypothesis.register_random(r=r)
 
 
 @given(version=st.text(), blob=st.binary())
-def test_fuzz_reproduce_failure(version, blob):
+def test_fuzz_reproduce_failure(version: str, blob: bytes) -> None:
     hypothesis.reproduce_failure(version=version, blob=blob)
 
 
 @given(seed=st.from_type(typing.Hashable))
-def test_fuzz_seed(seed):
+def test_fuzz_seed(seed: typing.Hashable) -> None:
     hypothesis.seed(seed=seed)
 
 
@@ -69,18 +81,18 @@ def test_fuzz_seed(seed):
     print_blob=st.just(not_set),
 )
 def test_fuzz_settings(
-    parent,
-    max_examples,
-    derandomize,
+    parent: typing.Union[hypothesis.settings, None],
+    max_examples: int,
+    derandomize: bool,
     database,
     verbosity,
     phases,
-    stateful_step_count,
-    report_multiple_bugs,
+    stateful_step_count: int,
+    report_multiple_bugs: bool,
     suppress_health_check,
-    deadline,
-    print_blob,
-):
+    deadline: typing.Union[int, float, datetime.timedelta, None],
+    print_blob: bool,
+) -> None:
     hypothesis.settings(
         parent=parent,
         max_examples=max_examples,
@@ -97,20 +109,22 @@ def test_fuzz_settings(
 
 
 @given(name=st.text())
-def test_fuzz_settings_get_profile(name):
+def test_fuzz_settings_get_profile(name: str) -> None:
     hypothesis.settings.get_profile(name=name)
 
 
 @given(name=st.text())
-def test_fuzz_settings_load_profile(name):
+def test_fuzz_settings_load_profile(name: str) -> None:
     hypothesis.settings.load_profile(name=name)
 
 
 @given(name=st.text(), parent=st.one_of(st.none(), st.builds(settings)))
-def test_fuzz_settings_register_profile(name, parent):
+def test_fuzz_settings_register_profile(
+    name: str, parent: typing.Union[hypothesis.settings, None]
+) -> None:
     hypothesis.settings.register_profile(name=name, parent=parent)
 
 
 @given(observation=st.one_of(st.floats(), st.integers()), label=st.text())
-def test_fuzz_target(observation, label):
+def test_fuzz_target(observation: typing.Union[int, float], label: str) -> None:
     hypothesis.target(observation=observation, label=label)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/invalid_types.txt b/hypothesis-python/tests/ghostwriter/recorded/invalid_types.txt
new file mode 100644
index 000000000..930a9d3de
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/invalid_types.txt
@@ -0,0 +1,12 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+import example_code.future_annotations
+from hypothesis import given, strategies as st
+
+# TODO: replace st.nothing() with appropriate strategies
+
+
+@given(attr1=st.nothing(), attr2=st.nothing(), attr3=st.nothing())
+def test_fuzz_invalid_types(attr1, attr2, attr3) -> None:
+    example_code.future_annotations.invalid_types(attr1=attr1, attr2=attr2, attr3=attr3)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/magic_base64_roundtrip_with_annotations.txt b/hypothesis-python/tests/ghostwriter/recorded/magic_base64_roundtrip_with_annotations.txt
new file mode 100644
index 000000000..9677067a4
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/magic_base64_roundtrip_with_annotations.txt
@@ -0,0 +1,14 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+import base64
+from hypothesis import given, strategies as st
+
+# TODO: replace st.nothing() with an appropriate strategy
+
+
+@given(altchars=st.none(), s=st.nothing(), validate=st.booleans())
+def test_roundtrip_b64encode_b64decode(altchars, s, validate) -> None:
+    value0 = base64.b64encode(s=s, altchars=altchars)
+    value1 = base64.b64decode(s=value0, altchars=altchars, validate=validate)
+    assert s == value1, (s, value1)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/magic_class.txt b/hypothesis-python/tests/ghostwriter/recorded/magic_class.txt
index ad2c6302f..b106a9e2c 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/magic_class.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/magic_class.txt
@@ -6,15 +6,15 @@ from hypothesis import given, strategies as st
 
 
 @given()
-def test_fuzz_A_Class():
+def test_fuzz_A_Class() -> None:
     test_expected_output.A_Class()
 
 
 @given(arg=st.integers())
-def test_fuzz_A_Class_a_classmethod(arg):
+def test_fuzz_A_Class_a_classmethod(arg: int) -> None:
     test_expected_output.A_Class.a_classmethod(arg=arg)
 
 
 @given(arg=st.integers())
-def test_fuzz_A_Class_a_staticmethod(arg):
+def test_fuzz_A_Class_a_staticmethod(arg: int) -> None:
     test_expected_output.A_Class.a_staticmethod(arg=arg)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/merge_dicts.txt b/hypothesis-python/tests/ghostwriter/recorded/merge_dicts.txt
new file mode 100644
index 000000000..f3d3d9e24
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/merge_dicts.txt
@@ -0,0 +1,23 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+import collections.abc
+import example_code.future_annotations
+from collections import ChainMap
+from hypothesis import given, strategies as st
+
+
+@given(
+    map1=st.one_of(
+        st.dictionaries(keys=st.text(), values=st.integers()),
+        st.dictionaries(keys=st.text(), values=st.integers()).map(ChainMap),
+    ),
+    map2=st.one_of(
+        st.dictionaries(keys=st.text(), values=st.integers()),
+        st.dictionaries(keys=st.text(), values=st.integers()).map(ChainMap),
+    ),
+)
+def test_fuzz_merge_dicts(
+    map1: collections.abc.Mapping[str, int], map2: collections.abc.Mapping[str, int]
+) -> None:
+    example_code.future_annotations.merge_dicts(map1=map1, map2=map2)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/optional_parameter.txt b/hypothesis-python/tests/ghostwriter/recorded/optional_parameter.txt
new file mode 100644
index 000000000..84cef04fa
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/optional_parameter.txt
@@ -0,0 +1,11 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+import test_expected_output
+import typing
+from hypothesis import given, strategies as st
+
+
+@given(a=st.floats(), b=st.one_of(st.none(), st.floats()))
+def test_fuzz_optional_parameter(a: float, b: typing.Optional[float]) -> None:
+    test_expected_output.optional_parameter(a=a, b=b)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/optional_parameter_pre_py_3_9.txt b/hypothesis-python/tests/ghostwriter/recorded/optional_parameter_pre_py_3_9.txt
new file mode 100644
index 000000000..33492768e
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/optional_parameter_pre_py_3_9.txt
@@ -0,0 +1,11 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+import test_expected_output
+import typing
+from hypothesis import given, strategies as st
+
+
+@given(a=st.floats(), b=st.one_of(st.none(), st.floats()))
+def test_fuzz_optional_parameter(a: float, b: typing.Union[float, None]) -> None:
+    test_expected_output.optional_parameter(a=a, b=b)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/optional_union_parameter.txt b/hypothesis-python/tests/ghostwriter/recorded/optional_union_parameter.txt
new file mode 100644
index 000000000..871586623
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/optional_union_parameter.txt
@@ -0,0 +1,13 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+import test_expected_output
+import typing
+from hypothesis import given, strategies as st
+
+
+@given(a=st.floats(), b=st.one_of(st.none(), st.floats(), st.integers()))
+def test_fuzz_optional_union_parameter(
+    a: float, b: typing.Union[float, int, None]
+) -> None:
+    test_expected_output.optional_union_parameter(a=a, b=b)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/sorted_self_equivalent_with_annotations.txt b/hypothesis-python/tests/ghostwriter/recorded/sorted_self_equivalent_with_annotations.txt
new file mode 100644
index 000000000..658b29514
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/sorted_self_equivalent_with_annotations.txt
@@ -0,0 +1,17 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+from hypothesis import given, strategies as st
+
+
+@given(
+    iterable=st.one_of(st.iterables(st.integers()), st.iterables(st.text())),
+    key=st.none(),
+    reverse=st.booleans(),
+)
+def test_equivalent_sorted_sorted_sorted(iterable, key, reverse) -> None:
+    result_0_sorted = sorted(iterable, key=key, reverse=reverse)
+    result_1_sorted = sorted(iterable, key=key, reverse=reverse)
+    result_2_sorted = sorted(iterable, key=key, reverse=reverse)
+    assert result_0_sorted == result_1_sorted, (result_0_sorted, result_1_sorted)
+    assert result_0_sorted == result_2_sorted, (result_0_sorted, result_2_sorted)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/timsort_idempotent.txt b/hypothesis-python/tests/ghostwriter/recorded/timsort_idempotent.txt
index 4f40d6486..82b343d62 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/timsort_idempotent.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/timsort_idempotent.txt
@@ -2,11 +2,12 @@
 # and is provided under the Creative Commons Zero public domain dedication.
 
 import test_expected_output
+import typing
 from hypothesis import given, strategies as st
 
 
 @given(seq=st.one_of(st.binary(), st.lists(st.integers())))
-def test_idempotent_timsort(seq):
+def test_idempotent_timsort(seq: typing.Sequence[int]) -> None:
     result = test_expected_output.timsort(seq=seq)
     repeat = test_expected_output.timsort(seq=result)
     assert result == repeat, (result, repeat)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/timsort_idempotent_asserts.txt b/hypothesis-python/tests/ghostwriter/recorded/timsort_idempotent_asserts.txt
index a3c6c1d5b..7e0a6f50a 100644
--- a/hypothesis-python/tests/ghostwriter/recorded/timsort_idempotent_asserts.txt
+++ b/hypothesis-python/tests/ghostwriter/recorded/timsort_idempotent_asserts.txt
@@ -2,11 +2,12 @@
 # and is provided under the Creative Commons Zero public domain dedication.
 
 import test_expected_output
+import typing
 from hypothesis import given, reject, strategies as st
 
 
 @given(seq=st.one_of(st.binary(), st.lists(st.integers())))
-def test_idempotent_timsort(seq):
+def test_idempotent_timsort(seq: typing.Sequence[int]) -> None:
     try:
         result = test_expected_output.timsort(seq=seq)
         repeat = test_expected_output.timsort(seq=result)
diff --git a/hypothesis-python/tests/ghostwriter/recorded/union_sequence_parameter.txt b/hypothesis-python/tests/ghostwriter/recorded/union_sequence_parameter.txt
new file mode 100644
index 000000000..36b4b36c9
--- /dev/null
+++ b/hypothesis-python/tests/ghostwriter/recorded/union_sequence_parameter.txt
@@ -0,0 +1,13 @@
+# This test code was written by the `hypothesis.extra.ghostwriter` module
+# and is provided under the Creative Commons Zero public domain dedication.
+
+import test_expected_output
+import typing
+from hypothesis import given, strategies as st
+
+
+@given(items=st.one_of(st.binary(), st.lists(st.one_of(st.floats(), st.integers()))))
+def test_fuzz_union_sequence_parameter(
+    items: typing.Sequence[typing.Union[float, int]]
+) -> None:
+    test_expected_output.union_sequence_parameter(items=items)
diff --git a/hypothesis-python/tests/ghostwriter/test_expected_output.py b/hypothesis-python/tests/ghostwriter/test_expected_output.py
index f694e86c0..60a3c4f4d 100644
--- a/hypothesis-python/tests/ghostwriter/test_expected_output.py
+++ b/hypothesis-python/tests/ghostwriter/test_expected_output.py
@@ -21,10 +21,15 @@ import operator
 import pathlib
 import re
 import sys
-from typing import Sequence
+from typing import Optional, Sequence, Union
 
 import numpy
 import pytest
+from example_code.future_annotations import (
+    add_custom_classes,
+    invalid_types,
+    merge_dicts,
+)
 
 import hypothesis
 from hypothesis.extra import ghostwriter
@@ -82,18 +87,64 @@ def divide(a: int, b: int) -> float:
     return a / b
 
 
+def optional_parameter(a: float, b: Optional[float]) -> float:
+    return optional_union_parameter(a, b)
+
+
+def optional_union_parameter(a: float, b: Optional[Union[float, int]]) -> float:
+    return a if b is None else a + b
+
+
+if sys.version_info[:2] >= (3, 10):
+
+    def union_sequence_parameter(items: Sequence[float | int]) -> float:
+        return sum(items)
+
+else:
+
+    def union_sequence_parameter(items: Sequence[Union[float, int]]) -> float:
+        return sum(items)
+
+
 # Note: for some of the `expected` outputs, we replace away some small
 #       parts which vary between minor versions of Python.
 @pytest.mark.parametrize(
     "data",
     [
         ("fuzz_sorted", ghostwriter.fuzz(sorted)),
+        ("fuzz_sorted_with_annotations", ghostwriter.fuzz(sorted, annotate=True)),
         ("fuzz_with_docstring", ghostwriter.fuzz(with_docstring)),
         ("fuzz_classmethod", ghostwriter.fuzz(A_Class.a_classmethod)),
         ("fuzz_staticmethod", ghostwriter.fuzz(A_Class.a_staticmethod)),
         ("fuzz_ufunc", ghostwriter.fuzz(numpy.add)),
         ("magic_gufunc", ghostwriter.magic(numpy.matmul)),
+        pytest.param(
+            ("optional_parameter", ghostwriter.magic(optional_parameter)),
+            marks=pytest.mark.skipif("sys.version_info[:2] < (3, 9)"),
+        ),
+        pytest.param(
+            ("optional_parameter_pre_py_3_9", ghostwriter.magic(optional_parameter)),
+            marks=pytest.mark.skipif("sys.version_info[:2] >= (3, 9)"),
+        ),
+        ("optional_union_parameter", ghostwriter.magic(optional_union_parameter)),
+        ("union_sequence_parameter", ghostwriter.magic(union_sequence_parameter)),
+        pytest.param(
+            ("add_custom_classes", ghostwriter.magic(add_custom_classes)),
+            marks=pytest.mark.skipif("sys.version_info[:2] < (3, 10)"),
+        ),
+        pytest.param(
+            ("merge_dicts", ghostwriter.magic(merge_dicts)),
+            marks=pytest.mark.skipif("sys.version_info[:2] < (3, 10)"),
+        ),
+        pytest.param(
+            ("invalid_types", ghostwriter.magic(invalid_types)),
+            marks=pytest.mark.skipif("sys.version_info[:2] < (3, 10)"),
+        ),
         ("magic_base64_roundtrip", ghostwriter.magic(base64.b64encode)),
+        (
+            "magic_base64_roundtrip_with_annotations",
+            ghostwriter.magic(base64.b64encode, annotate=True),
+        ),
         ("re_compile", ghostwriter.fuzz(re.compile)),
         (
             "re_compile_except",
@@ -114,6 +165,10 @@ def divide(a: int, b: int) -> float:
         ),
         ("eval_equivalent", ghostwriter.equivalent(eval, ast.literal_eval)),
         ("sorted_self_equivalent", ghostwriter.equivalent(sorted, sorted, sorted)),
+        (
+            "sorted_self_equivalent_with_annotations",
+            ghostwriter.equivalent(sorted, sorted, sorted, annotate=True),
+        ),
         ("addition_op_magic", ghostwriter.magic(add)),
         ("addition_op_multimagic", ghostwriter.magic(add, operator.add, numpy.add)),
         ("division_fuzz_error_handler", ghostwriter.fuzz(divide)),
@@ -125,6 +180,10 @@ def divide(a: int, b: int) -> float:
             "division_roundtrip_error_handler",
             ghostwriter.roundtrip(divide, operator.mul),
         ),
+        (
+            "division_roundtrip_error_handler_without_annotations",
+            ghostwriter.roundtrip(divide, operator.mul, annotate=False),
+        ),
         (
             "division_roundtrip_arithmeticerror_handler",
             ghostwriter.roundtrip(divide, operator.mul, except_=ArithmeticError),
@@ -139,6 +198,12 @@ def divide(a: int, b: int) -> float:
                 operator.truediv, associative=False, commutative=False
             ),
         ),
+        (
+            "division_operator_with_annotations",
+            ghostwriter.binary_operation(
+                operator.truediv, associative=False, commutative=False, annotate=True
+            ),
+        ),
         (
             "multiplication_operator",
             ghostwriter.binary_operation(
diff --git a/hypothesis-python/tests/ghostwriter/test_ghostwriter.py b/hypothesis-python/tests/ghostwriter/test_ghostwriter.py
index 93b13c2e2..7c6e310bc 100644
--- a/hypothesis-python/tests/ghostwriter/test_ghostwriter.py
+++ b/hypothesis-python/tests/ghostwriter/test_ghostwriter.py
@@ -42,6 +42,7 @@ from hypothesis.errors import InvalidArgument, Unsatisfiable
 from hypothesis.extra import cli, ghostwriter
 from hypothesis.internal.compat import BaseExceptionGroup
 from hypothesis.strategies import builds, from_type, just, lists
+from hypothesis.strategies._internal.core import from_regex
 from hypothesis.strategies._internal.lazy import LazyStrategy
 
 varied_excepts = pytest.mark.parametrize("ex", [(), ValueError, (TypeError, re.error)])
@@ -315,7 +316,12 @@ class MyError(UnicodeDecodeError):
 )
 def test_exception_deduplication(exceptions, output):
     _, body = ghostwriter._make_test_body(
-        lambda: None, ghost="", test_body="pass", except_=exceptions, style="pytest"
+        lambda: None,
+        ghost="",
+        test_body="pass",
+        except_=exceptions,
+        style="pytest",
+        annotate=False,
     )
     assert f"except {output}:" in body
 
@@ -426,6 +432,16 @@ def test_unrepr_identity_elem():
             lists(builds(Decimal)),
             {("decimal", "Decimal")},
         ),
+        # find the needed import for from_regex if needed
+        (
+            from_regex(re.compile(".+")),
+            {"re"},
+        ),
+        # but don't add superfluous imports
+        (
+            from_regex(".+"),
+            set(),
+        ),
     ],
 )
 def test_get_imports_for_strategy(strategy, imports):
diff --git a/hypothesis-python/tests/ghostwriter/test_ghostwriter_cli.py b/hypothesis-python/tests/ghostwriter/test_ghostwriter_cli.py
index dddd434a5..ffd1fca4f 100644
--- a/hypothesis-python/tests/ghostwriter/test_ghostwriter_cli.py
+++ b/hypothesis-python/tests/ghostwriter/test_ghostwriter_cli.py
@@ -48,6 +48,9 @@ from hypothesis.extra.ghostwriter import (
         ("hypothesis.errors.StopTest", lambda: fuzz(StopTest)),
         # Search for identity element does not print e.g. "You can use @seed ..."
         ("--binary-op operator.add", lambda: binary_operation(operator.add)),
+        # Annotations are passed correctly
+        ("sorted --annotate", lambda: fuzz(sorted, annotate=True)),
+        ("sorted --no-annotate", lambda: fuzz(sorted, annotate=False)),
     ],
 )
 def test_cli_python_equivalence(cli, code):
diff --git a/hypothesis-python/tests/nocover/test_boundary_exploration.py b/hypothesis-python/tests/nocover/test_boundary_exploration.py
index 083e92044..7b5d7d208 100644
--- a/hypothesis-python/tests/nocover/test_boundary_exploration.py
+++ b/hypothesis-python/tests/nocover/test_boundary_exploration.py
@@ -18,7 +18,7 @@ from tests.common.utils import no_shrink
 
 
 @pytest.mark.parametrize("strat", [st.text(min_size=5)])
-@settings(phases=no_shrink, deadline=None, suppress_health_check=HealthCheck.all())
+@settings(phases=no_shrink, deadline=None, suppress_health_check=list(HealthCheck))
 @given(st.data())
 def test_explore_arbitrary_function(strat, data):
     cache = {}
diff --git a/hypothesis-python/tests/nocover/test_explore_arbitrary_languages.py b/hypothesis-python/tests/nocover/test_explore_arbitrary_languages.py
index 82632791e..307076863 100644
--- a/hypothesis-python/tests/nocover/test_explore_arbitrary_languages.py
+++ b/hypothesis-python/tests/nocover/test_explore_arbitrary_languages.py
@@ -105,7 +105,7 @@ def run_language_test_for(root, data, seed):
         settings=settings(
             max_examples=1,
             database=None,
-            suppress_health_check=HealthCheck.all(),
+            suppress_health_check=list(HealthCheck),
             verbosity=Verbosity.quiet,
             # Restore the global default phases, so that we don't inherit the
             # phases setting from the outer test.
@@ -121,7 +121,7 @@ def run_language_test_for(root, data, seed):
 
 
 @settings(
-    suppress_health_check=HealthCheck.all(),
+    suppress_health_check=list(HealthCheck),
     deadline=None,
     phases=set(settings.default.phases) - {Phase.shrink},
 )
diff --git a/hypothesis-python/tests/nocover/test_given_error_conditions.py b/hypothesis-python/tests/nocover/test_given_error_conditions.py
index 7d88e8bac..5ed442936 100644
--- a/hypothesis-python/tests/nocover/test_given_error_conditions.py
+++ b/hypothesis-python/tests/nocover/test_given_error_conditions.py
@@ -17,7 +17,7 @@ from hypothesis.strategies import integers
 
 def test_raises_unsatisfiable_if_all_false():
     @given(integers())
-    @settings(max_examples=50, suppress_health_check=HealthCheck.all())
+    @settings(max_examples=50, suppress_health_check=list(HealthCheck))
     def test_assume_false(x):
         reject()
 
diff --git a/hypothesis-python/tests/nocover/test_integer_ranges.py b/hypothesis-python/tests/nocover/test_integer_ranges.py
index f8c949e16..793d9633a 100644
--- a/hypothesis-python/tests/nocover/test_integer_ranges.py
+++ b/hypothesis-python/tests/nocover/test_integer_ranges.py
@@ -10,7 +10,7 @@
 
 import pytest
 
-from hypothesis import given
+from hypothesis import given, settings
 from hypothesis.internal.conjecture.utils import integer_range
 from hypothesis.strategies import integers
 from hypothesis.strategies._internal.strategies import SearchStrategy
@@ -47,14 +47,16 @@ def test_intervals_shrink_to_center(lower_center_upper):
 def test_bounded_integers_distribution_of_bit_width_issue_1387_regression():
     values = []
 
+    @settings(database=None, max_examples=1000)
     @given(integers(0, 1e100))
     def test(x):
-        values.append(x)
+        if 2 <= x <= int(1e100) - 2:  # skip forced-endpoints
+            values.append(x)
 
     test()
 
     # We draw from a shaped distribution up to 128bit ~7/8 of the time, and
     # uniformly the rest.  So we should get some very large but not too many.
     huge = sum(x > 1e97 for x in values)
-    assert huge != 0
+    assert huge != 0 or len(values) < 800
     assert huge <= 0.3 * len(values)  # expected ~1/8
diff --git a/hypothesis-python/tests/nocover/test_testdecorators.py b/hypothesis-python/tests/nocover/test_testdecorators.py
index 64b732371..a62744e59 100644
--- a/hypothesis-python/tests/nocover/test_testdecorators.py
+++ b/hypothesis-python/tests/nocover/test_testdecorators.py
@@ -17,7 +17,7 @@ from hypothesis.errors import InvalidArgument, Unsatisfiable
 
 
 def test_contains_the_test_function_name_in_the_exception_string():
-    look_for_one = settings(max_examples=1, suppress_health_check=HealthCheck.all())
+    look_for_one = settings(max_examples=1, suppress_health_check=list(HealthCheck))
 
     @given(st.integers())
     @look_for_one
diff --git a/hypothesis-python/tests/numpy/test_from_dtype.py b/hypothesis-python/tests/numpy/test_from_dtype.py
index 5b4850d18..937330adc 100644
--- a/hypothesis-python/tests/numpy/test_from_dtype.py
+++ b/hypothesis-python/tests/numpy/test_from_dtype.py
@@ -8,15 +8,18 @@
 # 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/.
 
+import sys
+
 import numpy as np
 import pytest
 
 from hypothesis import assume, given, settings, strategies as st
 from hypothesis.errors import InvalidArgument
 from hypothesis.extra import numpy as nps
+from hypothesis.internal.floats import width_smallest_normals
 from hypothesis.strategies._internal import SearchStrategy
 
-from tests.common.debug import find_any
+from tests.common.debug import assert_no_examples, find_any
 
 STANDARD_TYPES = [
     np.dtype(t)
@@ -196,9 +199,29 @@ def test_arrays_gives_useful_error_on_inconsistent_time_unit():
         (float, {"allow_nan": False}, lambda x: not np.isnan(x)),
         (float, {"allow_infinity": False}, lambda x: not np.isinf(x)),
         (float, {"allow_nan": False, "allow_infinity": False}, np.isfinite),
+        # Complex numbers: bounds and excluding nonfinites
         (complex, {"allow_nan": False}, lambda x: not np.isnan(x)),
         (complex, {"allow_infinity": False}, lambda x: not np.isinf(x)),
         (complex, {"allow_nan": False, "allow_infinity": False}, np.isfinite),
+        (
+            complex,
+            {"min_magnitude": 1e3},
+            lambda x: abs(x) >= 1e3 * (1 - sys.float_info.epsilon),
+        ),
+        (
+            complex,
+            {"max_magnitude": 1e2},
+            lambda x: abs(x) <= 1e2 * (1 + sys.float_info.epsilon),
+        ),
+        (
+            complex,
+            {"min_magnitude": 1, "max_magnitude": 1e6},
+            lambda x: (
+                (1 - sys.float_info.epsilon)
+                <= abs(x)
+                <= 1e6 * (1 + sys.float_info.epsilon)
+            ),
+        ),
         # Integer bounds, limited to the representable range
         ("int8", {"min_value": -1, "max_value": 1}, lambda x: -1 <= x <= 1),
         ("uint8", {"min_value": 1, "max_value": 2}, lambda x: 1 <= x <= 2),
@@ -222,3 +245,36 @@ def test_customize_structured_dtypes(x):
     assert len(name) >= 1
     assert 0 <= age <= 255
     assert not np.isnan(score)
+
+
+@pytest.mark.parametrize("allow_subnormal", [False, True])
+@pytest.mark.parametrize("width", [32, 64])
+def test_float_subnormal_generation(allow_subnormal, width):
+    dtype = np.dtype(f"float{width}")
+    strat = nps.from_dtype(dtype, allow_subnormal=allow_subnormal).filter(
+        lambda n: n != 0
+    )
+    smallest_normal = width_smallest_normals[width]
+    condition = lambda n: -smallest_normal < n < smallest_normal
+    if allow_subnormal:
+        find_any(strat, condition)
+    else:
+        assert_no_examples(strat, condition)
+
+
+@pytest.mark.parametrize("allow_subnormal", [False, True])
+@pytest.mark.parametrize("width", [64, 128])
+def test_complex_subnormal_generation(allow_subnormal, width):
+    dtype = np.dtype(f"complex{width}")
+    strat = nps.from_dtype(dtype, allow_subnormal=allow_subnormal).filter(
+        lambda n: n.real != 0 and n.imag != 0
+    )
+    smallest_normal = width_smallest_normals[width / 2]
+    condition = lambda n: (
+        -smallest_normal < n.real < smallest_normal
+        or -smallest_normal < n.imag < smallest_normal
+    )
+    if allow_subnormal:
+        find_any(strat, condition)
+    else:
+        assert_no_examples(strat, condition)
diff --git a/hypothesis-python/tests/numpy/test_gen_data.py b/hypothesis-python/tests/numpy/test_gen_data.py
index 795ffb93a..d15a3eda9 100644
--- a/hypothesis-python/tests/numpy/test_gen_data.py
+++ b/hypothesis-python/tests/numpy/test_gen_data.py
@@ -964,7 +964,6 @@ def test_mutually_broadcastable_shapes_can_generate_arbitrary_ndims(
 def test_mutually_broadcastable_shapes_can_generate_interesting_singletons(
     base_shape, max_dims
 ):
-
     find_any(
         nps.mutually_broadcastable_shapes(
             num_shapes=2,
@@ -1221,3 +1220,14 @@ def test_basic_indices_generate_valid_indexers(
 def test_array_owns_memory(x: np.ndarray):
     assert x.base is None
     assert x[...].base is x
+
+
+@given(st.data())
+def test_no_recursion_in_multi_line_reprs_issue_3560(data):
+    data.draw(nps.arrays(shape=(2,), dtype=float).map(lambda x: x))
+    data.draw(
+        nps.arrays(
+            shape=(2,),
+            dtype=float,
+        ).map(lambda x: x)
+    )
diff --git a/hypothesis-python/tests/pandas/test_argument_validation.py b/hypothesis-python/tests/pandas/test_argument_validation.py
index 7980cefc4..398737709 100644
--- a/hypothesis-python/tests/pandas/test_argument_validation.py
+++ b/hypothesis-python/tests/pandas/test_argument_validation.py
@@ -73,6 +73,7 @@ BAD_ARGS = [
     e(pdst.range_indexes, 1, 0),
     e(pdst.range_indexes, min_size="0"),
     e(pdst.range_indexes, max_size="1"),
+    e(pdst.range_indexes, name=""),
     e(pdst.series),
     e(pdst.series, dtype="not a dtype"),
     e(pdst.series, elements="not a strategy"),
diff --git a/hypothesis-python/tests/pandas/test_data_frame.py b/hypothesis-python/tests/pandas/test_data_frame.py
index 1a0789c33..7949cc6fd 100644
--- a/hypothesis-python/tests/pandas/test_data_frame.py
+++ b/hypothesis-python/tests/pandas/test_data_frame.py
@@ -137,7 +137,7 @@ def column_strategy(draw):
     )
 
 
-@given(pdst.data_frames(pdst.columns(1, dtype=np.dtype("<M8[ns]"))))
+@given(pdst.data_frames(pdst.columns(1, dtype=np.dtype("M8[ns]"))))
 def test_data_frames_with_timestamp_columns(df):
     pass
 
@@ -185,7 +185,17 @@ def test_arbitrary_data_frames(data):
         column_name = data_frame_columns[i]
         values = df[column_name]
         if c.unique:
-            assert len(set(values)) == len(values)
+            # NA values should always be treated as unique to each other, so we
+            # just ignore them here. Note NA values in the ecosystem can have
+            # different identity behaviours, e.g.
+            #
+            #     >>> set([float("nan"), float("nan")])
+            #     {nan, nan}
+            #     >>> set([pd.NaT, pd.NaT])
+            #     {NaT}
+            #
+            non_na_values = values.dropna()
+            assert len(set(non_na_values)) == len(non_na_values)
 
 
 @given(
diff --git a/hypothesis-python/tests/pandas/test_indexes.py b/hypothesis-python/tests/pandas/test_indexes.py
index ac5caeba5..d76da5a22 100644
--- a/hypothesis-python/tests/pandas/test_indexes.py
+++ b/hypothesis-python/tests/pandas/test_indexes.py
@@ -47,7 +47,7 @@ def test_unique_indexes_of_many_small_values(ix):
 
 
 @given(pdst.indexes(dtype="int8", name=st.just("test_name")))
-def test_name_passed_on(s):
+def test_name_passed_on_indexes(s):
     assert s.name == "test_name"
 
 
@@ -62,6 +62,11 @@ def test_arbitrary_range_index(i, j, data):
     data.draw(pdst.range_indexes(i, j))
 
 
+@given(pdst.range_indexes(name=st.just("test_name")))
+def test_name_passed_on_range_indexes(s):
+    assert s.name == "test_name"
+
+
 @given(pdst.range_indexes())
 def test_basic_range_indexes(ix):
     assert isinstance(ix, pandas.RangeIndex)
diff --git a/hypothesis-python/tests/pytest/test_statistics.py b/hypothesis-python/tests/pytest/test_statistics.py
index 8ec8368df..4c7ba3036 100644
--- a/hypothesis-python/tests/pytest/test_statistics.py
+++ b/hypothesis-python/tests/pytest/test_statistics.py
@@ -35,7 +35,7 @@ def test_all_valid(x):
     pass
 
 
-@settings(max_examples=100, suppress_health_check=HealthCheck.all())
+@settings(max_examples=100, suppress_health_check=list(HealthCheck))
 @given(integers())
 def test_iterations(x):
     assume(x == 13)
diff --git a/hypothesis-python/tests/quality/test_discovery_ability.py b/hypothesis-python/tests/quality/test_discovery_ability.py
index 0e6d9c037..1d5c7ae4f 100644
--- a/hypothesis-python/tests/quality/test_discovery_ability.py
+++ b/hypothesis-python/tests/quality/test_discovery_ability.py
@@ -23,6 +23,7 @@ import math
 import re
 
 from hypothesis import HealthCheck, settings as Settings
+from hypothesis.control import BuildContext
 from hypothesis.errors import UnsatisfiedAssumption
 from hypothesis.internal import reflection
 from hypothesis.internal.conjecture.engine import ConjectureRunner
@@ -73,14 +74,15 @@ def define_test(specifier, predicate, condition=None, p=0.5, suppress_health_che
             )
 
         def test_function(data):
-            try:
-                value = data.draw(specifier)
-            except UnsatisfiedAssumption:
-                data.mark_invalid()
-            if not _condition(value):
-                data.mark_invalid()
-            if predicate(value):
-                data.mark_interesting()
+            with BuildContext(data):
+                try:
+                    value = data.draw(specifier)
+                except UnsatisfiedAssumption:
+                    data.mark_invalid()
+                if not _condition(value):
+                    data.mark_invalid()
+                if predicate(value):
+                    data.mark_interesting()
 
         successes = 0
         actual_runs = 0
diff --git a/hypothesis-python/tests/quality/test_float_shrinking.py b/hypothesis-python/tests/quality/test_float_shrinking.py
index 082942ed6..7beccc2f6 100644
--- a/hypothesis-python/tests/quality/test_float_shrinking.py
+++ b/hypothesis-python/tests/quality/test_float_shrinking.py
@@ -39,7 +39,7 @@ def test_can_shrink_in_variable_sized_context(n):
 @example(1.7976931348623157e308)
 @example(1.5)
 @given(st.floats(min_value=0, allow_infinity=False, allow_nan=False))
-@settings(deadline=None, suppress_health_check=HealthCheck.all())
+@settings(deadline=None, suppress_health_check=list(HealthCheck))
 def test_shrinks_downwards_to_integers(f):
     g = minimal(
         st.floats().filter(lambda x: x >= f),
@@ -50,7 +50,7 @@ def test_shrinks_downwards_to_integers(f):
 
 @example(1)
 @given(st.integers(1, 2**16 - 1))
-@settings(deadline=None, suppress_health_check=HealthCheck.all(), max_examples=10)
+@settings(deadline=None, suppress_health_check=list(HealthCheck), max_examples=10)
 def test_shrinks_downwards_to_integers_when_fractional(b):
     g = minimal(
         st.floats().filter(lambda x: b < x < 2**53 and int(x) != x),
diff --git a/hypothesis-python/tests/quality/test_integers.py b/hypothesis-python/tests/quality/test_integers.py
index 647fc55b5..6da4c5eba 100644
--- a/hypothesis-python/tests/quality/test_integers.py
+++ b/hypothesis-python/tests/quality/test_integers.py
@@ -47,7 +47,7 @@ def problems(draw):
 @example((1, b"\x00\x00\x06\x01"))
 @example(problem=(32768, b"\x03\x01\x00\x00\x00\x00\x00\x01\x00\x02\x01"))
 @settings(
-    suppress_health_check=HealthCheck.all(),
+    suppress_health_check=list(HealthCheck),
     deadline=None,
     max_examples=10,
     verbosity=Verbosity.normal,
@@ -75,7 +75,7 @@ def test_always_reduces_integers_to_smallest_suitable_sizes(problem):
         f,
         random=Random(0),
         settings=settings(
-            suppress_health_check=HealthCheck.all(),
+            suppress_health_check=list(HealthCheck),
             phases=(Phase.shrink,),
             database=None,
             verbosity=Verbosity.debug,
diff --git a/hypothesis-python/tests/quality/test_normalization.py b/hypothesis-python/tests/quality/test_normalization.py
index 835538004..a5b64181f 100644
--- a/hypothesis-python/tests/quality/test_normalization.py
+++ b/hypothesis-python/tests/quality/test_normalization.py
@@ -14,6 +14,7 @@ from random import Random
 import pytest
 
 from hypothesis import strategies as st
+from hypothesis.control import BuildContext
 from hypothesis.errors import UnsatisfiedAssumption
 from hypothesis.internal.conjecture.shrinking import dfas
 
@@ -35,7 +36,6 @@ def normalize_kwargs(request):
     ids=repr,
 )
 def test_common_strategies_normalize_small_values(strategy, n, normalize_kwargs):
-
     excluded = list(map(repr, islice(iter_values(strategy, unique_by=repr), n)))
 
     def test_function(data):
@@ -53,11 +53,12 @@ def test_common_strategies_normalize_small_values(strategy, n, normalize_kwargs)
 @pytest.mark.parametrize("strategy", [st.emails(), st.complex_numbers()], ids=repr)
 def test_harder_strategies_normalize_to_minimal(strategy, normalize_kwargs):
     def test_function(data):
-        try:
-            v = data.draw(strategy)
-        except UnsatisfiedAssumption:
-            data.mark_invalid()
-        data.output = repr(v)
-        data.mark_interesting()
+        with BuildContext(data):
+            try:
+                v = data.draw(strategy)
+            except UnsatisfiedAssumption:
+                data.mark_invalid()
+            data.output = repr(v)
+            data.mark_interesting()
 
     dfas.normalize(repr(strategy), test_function, random=Random(0), **normalize_kwargs)
diff --git a/hypothesis-python/tests/quality/test_poisoned_trees.py b/hypothesis-python/tests/quality/test_poisoned_trees.py
index 60b7ec5f3..19600ecab 100644
--- a/hypothesis-python/tests/quality/test_poisoned_trees.py
+++ b/hypothesis-python/tests/quality/test_poisoned_trees.py
@@ -53,7 +53,7 @@ LOTS = 10**6
 
 TEST_SETTINGS = settings(
     database=None,
-    suppress_health_check=HealthCheck.all(),
+    suppress_health_check=list(HealthCheck),
     max_examples=LOTS,
     deadline=None,
 )
diff --git a/hypothesis-python/tests/quality/test_shrinking_order.py b/hypothesis-python/tests/quality/test_shrinking_order.py
index b9692caf2..e9b9388b4 100644
--- a/hypothesis-python/tests/quality/test_shrinking_order.py
+++ b/hypothesis-python/tests/quality/test_shrinking_order.py
@@ -42,7 +42,7 @@ def learner_for(strategy):
         settings=settings(
             database=None,
             verbosity=Verbosity.quiet,
-            suppress_health_check=HealthCheck.all(),
+            suppress_health_check=list(HealthCheck),
         ),
         random=Random(0),
         ignore_limits=True,
diff --git a/hypothesis-python/tests/quality/test_zig_zagging.py b/hypothesis-python/tests/quality/test_zig_zagging.py
index 440d9d3f5..041649c93 100644
--- a/hypothesis-python/tests/quality/test_zig_zagging.py
+++ b/hypothesis-python/tests/quality/test_zig_zagging.py
@@ -39,7 +39,7 @@ def problem(draw):
 base_settings = settings(
     database=None,
     deadline=None,
-    suppress_health_check=HealthCheck.all(),
+    suppress_health_check=list(HealthCheck),
     max_examples=10,
     verbosity=Verbosity.normal,
     phases=(Phase.explicit, Phase.generate),