Repository: HypothesisWorks/hypothesis (2022-08-02 ~ 2023-01-06)

problem_statement: Error with Django parallel test runner: hypothesis.errors.DeadlineExceeded is unpickleable
```
<snip>
  File "/usr/lib/python3.8/unittest/suite.py", line 122, in run
    test(result)
  File "/root/apps/netdb_demo/lib/python3.8/site-packages/hypothesis/extra/django/_impl.py", line 47, in __call__
    return unittest.TestCase.__call__(self, result)
  File "/usr/lib/python3.8/unittest/case.py", line 736, in __call__
    return self.run(*args, **kwds)
  File "/usr/lib/python3.8/unittest/case.py", line 684, in run
    self._feedErrorsToResult(result, outcome.errors)
  File "/usr/lib/python3.8/unittest/case.py", line 602, in _feedErrorsToResult
    result.addError(test, exc_info)
  File "/root/apps/netdb_demo/lib/python3.8/site-packages/django/test/runner.py", line 197, in addError
    self.check_picklable(test, err)
  File "/root/apps/netdb_demo/lib/python3.8/site-packages/django/test/runner.py", line 130, in check_picklable
    self._confirm_picklable(err)
  File "/root/apps/netdb_demo/lib/python3.8/site-packages/django/test/runner.py", line 104, in _confirm_picklable
    pickle.loads(pickle.dumps(obj))
  File "/root/apps/netdb_demo/lib/python3.8/site-packages/tblib/pickling_support.py", line 26, in unpickle_exception
    inst = func(*args)
TypeError: __init__() missing 1 required positional argument: 'deadline'
```
This happens because Exception by default reduces to arguments passed to its `__init__`, and DeadlineExceeded (and FailedHealthCheck, afaict) changes the signature when calling `super().__init__`.

One solution is to call `super().__init__()` with the same arguments, and override `__str__`; another would be to override `__reduce__`.
date: 2022-08-02
patch: diff --git a/hypothesis-python/src/hypothesis/errors.py b/hypothesis-python/src/hypothesis/errors.py
index 893c9d9f6..06b77de29 100644
--- a/hypothesis-python/src/hypothesis/errors.py
+++ b/hypothesis-python/src/hypothesis/errors.py
@@ -93,12 +93,7 @@ class HypothesisWarning(HypothesisException, Warning):
 
 
 class FailedHealthCheck(_Trimmable):
-    """Raised when a test fails a preliminary healthcheck that occurs before
-    execution."""
-
-    def __init__(self, message, check):
-        super().__init__(message)
-        self.health_check = check
+    """Raised when a test fails a healthcheck."""
 
 
 class NonInteractiveExampleWarning(HypothesisWarning):
@@ -149,6 +144,9 @@ class DeadlineExceeded(_Trimmable):
         self.runtime = runtime
         self.deadline = deadline
 
+    def __reduce__(self):
+        return (type(self), (self.runtime, self.deadline))
+
 
 class StopTest(BaseException):
     """Raised when a test should stop running and return control to
diff --git a/hypothesis-python/src/hypothesis/internal/healthcheck.py b/hypothesis-python/src/hypothesis/internal/healthcheck.py
index 84bc0edb0..d43742e00 100644
--- a/hypothesis-python/src/hypothesis/internal/healthcheck.py
+++ b/hypothesis-python/src/hypothesis/internal/healthcheck.py
@@ -24,4 +24,4 @@ def fail_health_check(settings, message, label):
         f"If you want to disable just this health check, add {label} "
         "to the suppress_health_check settings for this test."
     )
-    raise FailedHealthCheck(message, label)
+    raise FailedHealthCheck(message)

test_patch: diff --git a/hypothesis-python/tests/conjecture/test_engine.py b/hypothesis-python/tests/conjecture/test_engine.py
index 99c4ce549..5177bb20b 100644
--- a/hypothesis-python/tests/conjecture/test_engine.py
+++ b/hypothesis-python/tests/conjecture/test_engine.py
@@ -404,7 +404,7 @@ def fails_health_check(label, **kwargs):
 
         with pytest.raises(FailedHealthCheck) as e:
             runner.run()
-        assert e.value.health_check == label
+        assert str(label) in str(e.value)
         assert not runner.interesting_examples
 
     return accept
diff --git a/hypothesis-python/tests/cover/test_health_checks.py b/hypothesis-python/tests/cover/test_health_checks.py
index 5fd1f0d31..406c56b23 100644
--- a/hypothesis-python/tests/cover/test_health_checks.py
+++ b/hypothesis-python/tests/cover/test_health_checks.py
@@ -176,7 +176,7 @@ def test_large_base_example_fails_health_check():
     with pytest.raises(FailedHealthCheck) as exc:
         test()
 
-    assert exc.value.health_check == HealthCheck.large_base_example
+    assert str(HealthCheck.large_base_example) in str(exc.value)
 
 
 def test_example_that_shrinks_to_overrun_fails_health_check():
@@ -187,7 +187,7 @@ def test_example_that_shrinks_to_overrun_fails_health_check():
     with pytest.raises(FailedHealthCheck) as exc:
         test()
 
-    assert exc.value.health_check == HealthCheck.large_base_example
+    assert str(HealthCheck.large_base_example) in str(exc.value)
 
 
 def test_it_is_an_error_to_suppress_non_iterables():
diff --git a/hypothesis-python/tests/cover/test_regressions.py b/hypothesis-python/tests/cover/test_regressions.py
index 03fab70a0..8f26a4b84 100644
--- a/hypothesis-python/tests/cover/test_regressions.py
+++ b/hypothesis-python/tests/cover/test_regressions.py
@@ -8,12 +8,22 @@
 # 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 pickle
 import random
+from datetime import timedelta
 from unittest.mock import Mock
 
 import pytest
 
-from hypothesis import Verbosity, assume, given, seed, settings, strategies as st
+from hypothesis import (
+    Verbosity,
+    assume,
+    errors,
+    given,
+    seed,
+    settings,
+    strategies as st,
+)
 
 
 def strat():
@@ -111,3 +121,27 @@ def test_prng_state_unpolluted_by_given_issue_1266():
         assert second == third
     else:
         assert second != third
+
+
+exc_instances = [
+    errors.NoSuchExample("foobar", extra="baz"),
+    errors.DeadlineExceeded(
+        runtime=timedelta(seconds=1.5), deadline=timedelta(seconds=1.0)
+    ),
+]
+
+
+@pytest.mark.parametrize("exc", exc_instances, ids=repr)
+def test_exceptions_are_picklable(exc):
+    # See https://github.com/HypothesisWorks/hypothesis/issues/3426
+    pickle.loads(pickle.dumps(exc))
+
+
+def test_no_missed_custom_init_exceptions():
+    untested_errors_with_custom_init = {
+        et
+        for et in vars(errors).values()
+        if isinstance(et, type) and issubclass(et, Exception) and "__init__" in vars(et)
+    } - {type(exc) for exc in exc_instances}
+    print(untested_errors_with_custom_init)
+    assert not untested_errors_with_custom_init


----

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/.readthedocs.yml b/.readthedocs.yml
index ce6b9f07d..975c45312 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -8,8 +8,11 @@ version: 2
 formats: all
 
 # Optionally set the version of Python and requirements required to build your docs
+build:
+   os: ubuntu-22.04
+   tools:
+      python: "3.10"
 python:
-   version: 3.8
    install:
       - requirements: requirements/tools.txt
       - path: hypothesis-python/
diff --git a/build.sh b/build.sh
index 71c9f30ae..29173336d 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.8.13
-    PYTHON=$(pythonloc 3.8.13)/bin/python
+    "$SCRIPTS/ensure-python.sh" 3.10.8
+    PYTHON=$(pythonloc 3.10.8)/bin/python
 fi
 
 TOOL_REQUIREMENTS="$ROOT/requirements/tools.txt"
diff --git a/hypothesis-python/.coveragerc b/hypothesis-python/.coveragerc
index b5a21591f..1deab7668 100644
--- a/hypothesis-python/.coveragerc
+++ b/hypothesis-python/.coveragerc
@@ -1,6 +1,7 @@
 [run]
 branch = True
 omit =
+    **/_hypothesis_ftz_detector.py
     **/_hypothesis_pytestplugin.py
     **/extra/array_api.py
     **/extra/cli.py
@@ -19,6 +20,7 @@ exclude_lines =
     def __copy__
     def __deepcopy__
     except ImportError:
+    except ModuleNotFoundError:
     if PYPY:
     if TYPE_CHECKING:
     if "\w+" in sys\.modules:
diff --git a/hypothesis-python/scripts/basic-test.sh b/hypothesis-python/scripts/basic-test.sh
index 8ad96b1f7..a9851d9b0 100755
--- a/hypothesis-python/scripts/basic-test.sh
+++ b/hypothesis-python/scripts/basic-test.sh
@@ -13,7 +13,7 @@ for k, v in sorted(dict(os.environ).items()):
 pip install .
 
 
-PYTEST="python -bb -X dev -m pytest -nauto"
+PYTEST="python -bb -X dev -m pytest -nauto --durations-min=1.0"
 
 # Run all the no-extra-dependency tests for this version (except slow nocover tests)
 $PYTEST tests/cover tests/pytest
@@ -39,10 +39,11 @@ if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 7))')" = "False
 fi
 
 pip install ".[lark]"
+pip install "$(grep -oE 'lark>=([0-9.]+)' ../hypothesis-python/setup.py | tr '>' =)"
+$PYTEST -Wignore tests/lark/
+pip install "$(grep 'lark==' ../requirements/coverage.txt)"
 $PYTEST tests/lark/
-pip install "$(grep 'lark-parser==' ../requirements/coverage.txt)"
-$PYTEST tests/lark/
-pip uninstall -y lark-parser
+pip uninstall -y lark
 
 if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel == \'final\' and platform.python_implementation() != "PyPy")')" = "True" ] ; then
   pip install ".[codemods,cli]"
@@ -61,7 +62,7 @@ if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel ==
   pip uninstall -y black numpy
 fi
 
-if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 8))')" = "False" ] ; then
+if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 10))')" = "False" ] ; then
   exit 0
 fi
 
diff --git a/hypothesis-python/scripts/other-tests.sh b/hypothesis-python/scripts/other-tests.sh
new file mode 100644
index 000000000..3cc98c82e
--- /dev/null
+++ b/hypothesis-python/scripts/other-tests.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+set -e -o xtrace
+
+HERE="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd "$HERE/.."
+
+pip install .
+
+
+PYTEST="python -bb -X dev -m pytest -nauto --durations-min=1.0"
+
+# Run some tests without docstrings or assertions, to catch bugs
+# like issue #822 in one of the test decorators.  See also #1541.
+PYTHONOPTIMIZE=2 $PYTEST \
+    -W'ignore:assertions not in test modules or plugins will be ignored because assert statements are not executed by the underlying Python interpreter:pytest.PytestConfigWarning' \
+    -W'ignore:Module already imported so cannot be rewritten:pytest.PytestAssertRewriteWarning' \
+    tests/cover/test_testdecorators.py
+
+# Run tests for each extra module while the requirements are installed
+pip install ".[pytz, dateutil, zoneinfo]"
+$PYTEST tests/datetime/
+pip uninstall -y pytz python-dateutil
+
+pip install ".[dpcontracts]"
+$PYTEST tests/dpcontracts/
+pip uninstall -y dpcontracts
+
+pip install "$(grep 'fakeredis==' ../requirements/coverage.txt)"
+$PYTEST tests/redis/
+pip uninstall -y redis fakeredis
+
+pip install "$(grep 'typing-extensions==' ../requirements/coverage.txt)"
+$PYTEST tests/typing_extensions/
+if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 7))')" = "False" ] ; then
+  # Required by importlib_metadata backport, which we don't want to break
+  pip uninstall -y typing_extensions
+fi
+
+pip install ".[lark]"
+pip install "$(grep -oE 'lark>=([0-9.]+)' ../hypothesis-python/setup.py | tr '>' =)"
+$PYTEST -Wignore tests/lark/
+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
+  pip install ".[codemods,cli]"
+  $PYTEST tests/codemods/
+  pip uninstall -y libcst click
+
+  if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 7))')" = "True" ] ; then
+    # Per NEP-29, this is the last version to support Python 3.7
+    pip install numpy==1.21.5
+  else
+    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
+
+  pip install "$(grep 'black==' ../requirements/coverage.txt)"
+  $PYTEST tests/ghostwriter/
+  pip uninstall -y black numpy
+fi
diff --git a/hypothesis-python/setup.cfg b/hypothesis-python/setup.cfg
index 0e358611a..a151fbeff 100644
--- a/hypothesis-python/setup.cfg
+++ b/hypothesis-python/setup.cfg
@@ -1,3 +1,3 @@
 [metadata]
 # This includes the license file in the wheel.
-license_file = LICENSE.txt
+license_files = LICENSE.txt
diff --git a/hypothesis-python/setup.py b/hypothesis-python/setup.py
index e3f665531..21c44d01c 100644
--- a/hypothesis-python/setup.py
+++ b/hypothesis-python/setup.py
@@ -31,14 +31,14 @@ def local_file(name):
 SOURCE = local_file("src")
 README = local_file("README.rst")
 
-setuptools_version = tuple(map(int, setuptools.__version__.split(".")[:2]))
+setuptools_version = tuple(map(int, setuptools.__version__.split(".")[:1]))
 
-if setuptools_version < (36, 2):
+if setuptools_version < (42,):
     # Warning only - very bad if uploading bdist but fine if installing sdist.
     warnings.warn(
-        "This version of setuptools is too old to correctly store "
-        "conditional dependencies in binary wheels.  For more info, see:  "
-        "https://hynek.me/articles/conditional-python-dependencies/"
+        "This version of setuptools is too old to handle license_files "
+        "metadata key.  For more info, see:  "
+        "https://setuptools.pypa.io/en/latest/userguide/declarative_config.html#metadata"
     )
 
 
@@ -57,7 +57,7 @@ extras = {
     "ghostwriter": ["black>=19.10b0"],
     "pytz": ["pytz>=2014.1"],
     "dateutil": ["python-dateutil>=1.4"],
-    "lark": ["lark-parser>=0.6.5"],
+    "lark": ["lark>=0.10.1"],  # probably still works with old `lark-parser` too
     "numpy": ["numpy>=1.9.0"],
     "pandas": ["pandas>=1.0"],
     "pytest": ["pytest>=4.6"],
@@ -66,14 +66,14 @@ 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.1 ; 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
     # https://www.djangoproject.com/download/#supported-versions
     # We also leave the choice of timezone library to the user, since it
     # might be zoneinfo or pytz depending on version and configuration.
-    "django": ["django>=2.2"],
+    "django": ["django>=3.2"],
 }
 
 extras["all"] = sorted(
@@ -102,7 +102,7 @@ setuptools.setup(
     extras_require=extras,
     install_requires=[
         "attrs>=19.2.0",
-        "exceptiongroup>=1.0.0rc8 ; python_version<'3.11'",
+        "exceptiongroup>=1.0.0 ; python_version<'3.11'",
         "sortedcontainers>=2.1.0,<3.0.0",
     ],
     python_requires=">=3.7",
@@ -128,7 +128,7 @@ setuptools.setup(
         "Topic :: Software Development :: Testing",
         "Typing :: Typed",
     ],
-    py_modules=["_hypothesis_pytestplugin"],
+    py_modules=["_hypothesis_pytestplugin", "_hypothesis_ftz_detector"],
     entry_points={
         "pytest11": ["hypothesispytest = _hypothesis_pytestplugin"],
         "console_scripts": ["hypothesis = hypothesis.extra.cli:main"],
diff --git a/hypothesis-python/src/_hypothesis_ftz_detector.py b/hypothesis-python/src/_hypothesis_ftz_detector.py
new file mode 100644
index 000000000..0a0378ae8
--- /dev/null
+++ b/hypothesis-python/src/_hypothesis_ftz_detector.py
@@ -0,0 +1,149 @@
+# 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/.
+
+"""
+This is a toolkit for determining which module set the "flush to zero" flag.
+
+For details, see the docstring and comments in `identify_ftz_culprit()`.  This module
+is defined outside the main Hypothesis namespace so that we can avoid triggering
+import of Hypothesis itself from each subprocess which must import the worker function.
+"""
+
+import importlib
+import sys
+
+KNOWN_EVER_CULPRITS = (
+    # https://moyix.blogspot.com/2022/09/someones-been-messing-with-my-subnormals.html
+    # fmt: off
+    "archive-pdf-tools", "bgfx-python", "bicleaner-ai-glove", "BTrees", "cadbiom",
+    "ctranslate2", "dyNET", "dyNET38", "gevent", "glove-python-binary", "higra",
+    "hybridq", "ikomia", "ioh", "jij-cimod", "lavavu", "lavavu-osmesa", "MulticoreTSNE",
+    "neural-compressor", "nwhy", "openjij", "openturns", "perfmetrics", "pHashPy",
+    "pyace-lite", "pyapr", "pycompadre", "pycompadre-serial", "PyKEP", "pykep",
+    "pylimer-tools", "pyqubo", "pyscf", "PyTAT", "python-prtree", "qiskit-aer",
+    "qiskit-aer-gpu", "RelStorage", "sail-ml", "segmentation", "sente", "sinr",
+    "snapml", "superman", "symengine", "systran-align", "texture-tool", "tsne-mp",
+    "xcsf",
+    # fmt: on
+)
+
+
+def flush_to_zero():
+    # If this subnormal number compares equal to zero we have a problem
+    return 2.0**-1073 == 0
+
+
+def run_in_process(fn, *args):
+    import multiprocessing as mp
+
+    mp.set_start_method("spawn", force=True)
+    q = mp.Queue()
+    p = mp.Process(target=target, args=(q, fn, *args))
+    p.start()
+    retval = q.get()
+    p.join()
+    return retval
+
+
+def target(q, fn, *args):
+    q.put(fn(*args))
+
+
+def always_imported_modules():
+    return flush_to_zero(), set(sys.modules)
+
+
+def modules_imported_by(mod):
+    """Return the set of modules imported transitively by mod."""
+    before = set(sys.modules)
+    try:
+        importlib.import_module(mod)
+    except Exception:
+        return None, set()
+    imports = set(sys.modules) - before
+    return flush_to_zero(), imports
+
+
+# We don't want to redo all the expensive process-spawning checks when we've already
+# done them, so we cache known-good packages and a known-FTZ result if we have one.
+KNOWN_FTZ = None
+CHECKED_CACHE = set()
+
+
+def identify_ftz_culprits():
+    """Find the modules in sys.modules which cause "mod" to be imported."""
+    # If we've run this function before, return the same result.
+    global KNOWN_FTZ
+    if KNOWN_FTZ:
+        return KNOWN_FTZ
+    # Start by determining our baseline: the FTZ and sys.modules state in a fresh
+    # process which has only imported this module and nothing else.
+    always_enables_ftz, always_imports = run_in_process(always_imported_modules)
+    if always_enables_ftz:
+        raise RuntimeError("Python is always in FTZ mode, even without imports!")
+    CHECKED_CACHE.update(always_imports)
+
+    # Next, we'll search through sys.modules looking for a package (or packages) such
+    # that importing them in a new process sets the FTZ state.  As a heuristic, we'll
+    # start with packages known to have ever enabled FTZ, then top-level packages as
+    # a way to eliminate large fractions of the search space relatively quickly.
+    def key(name):
+        """Prefer known-FTZ modules, then top-level packages, then alphabetical."""
+        return (name not in KNOWN_EVER_CULPRITS, name.count("."), name)
+
+    # We'll track the set of modules to be checked, and those which do trigger FTZ.
+    candidates = set(sys.modules) - CHECKED_CACHE
+    triggering_modules = {}
+    while candidates:
+        mod = min(candidates, key=key)
+        candidates.discard(mod)
+        enables_ftz, imports = run_in_process(modules_imported_by, mod)
+        imports -= CHECKED_CACHE
+        if enables_ftz:
+            triggering_modules[mod] = imports
+            candidates &= imports
+        else:
+            candidates -= imports
+            CHECKED_CACHE.update(imports)
+
+    # We only want to report the 'top level' packages which enable FTZ - for example,
+    # if the enabling code is in `a.b`, and `a` in turn imports `a.b`, we prefer to
+    # report `a`.  On the other hand, if `a` does _not_ import `a.b`, as is the case
+    # for `hypothesis.extra.*` modules, then `a` will not be in `triggering_modules`
+    # and we'll report `a.b` here instead.
+    prefixes = tuple(n + "." for n in triggering_modules)
+    result = {k for k in triggering_modules if not k.startswith(prefixes)}
+
+    # Suppose that `bar` enables FTZ, and `foo` imports `bar`.  At this point we're
+    # tracking both, but only want to report the latter.
+    for a in sorted(result):
+        for b in sorted(result):
+            if a in triggering_modules[b] and b not in triggering_modules[a]:
+                result.discard(b)
+
+    # There may be a cyclic dependency which that didn't handle, or simply two
+    # separate modules which both enable FTZ.  We already gave up comprehensive
+    # reporting for speed above (`candidates &= imports`), so we'll also buy
+    # simpler reporting by arbitrarily selecting the alphabetically first package.
+    KNOWN_FTZ = min(result)  # Cache the result - it's likely this will trigger again!
+    return KNOWN_FTZ
+
+
+if __name__ == "__main__":
+    # This would be really really annoying to write automated tests for, so I've
+    # done some manual exploratory testing: `pip install grequests gevent==21.12.0`,
+    # and call print() as desired to observe behavior.
+    import grequests  # noqa
+
+    # To test without skipping to a known answer, uncomment the following line and
+    # change the last element of key from `name` to `-len(name)` so that we check
+    # grequests before gevent.
+    ## KNOWN_EVER_CULPRITS = [c for c in KNOWN_EVER_CULPRITS if c != "gevent"]
+    print(identify_ftz_culprits())
diff --git a/hypothesis-python/src/_hypothesis_pytestplugin.py b/hypothesis-python/src/_hypothesis_pytestplugin.py
index 042f9e62c..afafd52ce 100644
--- a/hypothesis-python/src/_hypothesis_pytestplugin.py
+++ b/hypothesis-python/src/_hypothesis_pytestplugin.py
@@ -178,13 +178,6 @@ else:
                 pass
             core.global_force_seed = seed
 
-        core.pytest_shows_exceptiongroups = (
-            sys.version_info[:2] >= (3, 11)
-            ## See https://github.com/pytest-dev/pytest/issues/9159
-            # or pytest_version >= (7, 2)  # TODO: fill in correct version here
-            or config.getoption("tbstyle", "auto") == "native"
-        )
-
     @pytest.hookimpl(hookwrapper=True)
     def pytest_runtest_call(item):
         __tracebackhide__ = True
@@ -195,6 +188,11 @@ else:
         from hypothesis import core
         from hypothesis.internal.detection import is_hypothesis_test
 
+        ## See https://github.com/pytest-dev/pytest/issues/9159
+        # TODO: add `pytest_version >= (7, 2) or` once the issue above is fixed.
+        core.pytest_shows_exceptiongroups = (
+            item.config.getoption("tbstyle", "auto") == "native"
+        )
         core.running_under_pytest = True
 
         if not is_hypothesis_test(item.obj):
diff --git a/hypothesis-python/src/hypothesis/_settings.py b/hypothesis-python/src/hypothesis/_settings.py
index 238e0af97..4743a666f 100644
--- a/hypothesis-python/src/hypothesis/_settings.py
+++ b/hypothesis-python/src/hypothesis/_settings.py
@@ -8,7 +8,7 @@
 # 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/.
 
-"""A module controlling settings for Hypothesis to use in falsification.
+"""The settings module configures runtime options for Hypothesis.
 
 Either an explicit settings object can be used or the default object on
 this module can be modified.
@@ -120,9 +120,8 @@ class settingsMeta(type):
 
 
 class settings(metaclass=settingsMeta):
-    """A settings object controls a variety of parameters that are used in
-    falsification. These may control both the falsification strategy and the
-    details of the data that is generated.
+    """A settings object configures options including verbosity, runtime controls,
+    persistence, determinism, and more.
 
     Default values are picked up from the settings.default object and
     changes made there will be picked up in newly created settings.
@@ -364,7 +363,12 @@ settings._define_setting(
     validator=_max_examples_validator,
     description="""
 Once this many satisfying examples have been considered without finding any
-counter-example, falsification will terminate.
+counter-example, Hypothesis will stop looking.
+
+Note that we might call your test function fewer times if we find a bug early
+or can tell that we've exhausted the search space; or more if we discard some
+examples due to use of .filter(), assume(), or a few other things that can
+prevent the test case from completing successfully.
 
 The default value is chosen to suit a workflow where the test will be part of
 a suite that is regularly executed locally or on a CI server, balancing total
diff --git a/hypothesis-python/src/hypothesis/core.py b/hypothesis-python/src/hypothesis/core.py
index 14ade3fbb..befea5dcf 100644
--- a/hypothesis-python/src/hypothesis/core.py
+++ b/hypothesis-python/src/hypothesis/core.py
@@ -110,16 +110,15 @@ from hypothesis.strategies._internal.strategies import (
     MappedSearchStrategy,
     SearchStrategy,
 )
-from hypothesis.utils.conventions import infer
 from hypothesis.vendor.pretty import RepresentationPrinter
 from hypothesis.version import __version__
 
-if sys.version_info >= (3, 10):  # pragma: no cover
-    from types import EllipsisType as InferType
+if sys.version_info >= (3, 10):
+    from types import EllipsisType as EllipsisType
 elif TYPE_CHECKING:
-    from builtins import ellipsis as InferType
-else:
-    InferType = type(Ellipsis)
+    from builtins import ellipsis as EllipsisType
+else:  # pragma: no cover
+    EllipsisType = type(Ellipsis)
 
 
 TestFunc = TypeVar("TestFunc", bound=Callable)
@@ -137,25 +136,80 @@ class Example:
     kwargs = attr.ib()
 
 
-def example(*args: Any, **kwargs: Any) -> Callable[[TestFunc], TestFunc]:
+class example:
     """A decorator which ensures a specific example is always tested."""
-    if args and kwargs:
-        raise InvalidArgument(
-            "Cannot mix positional and keyword arguments for examples"
-        )
-    if not (args or kwargs):
-        raise InvalidArgument("An example must provide at least one argument")
 
-    hypothesis_explicit_examples: List[Example] = []
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        if args and kwargs:
+            raise InvalidArgument(
+                "Cannot mix positional and keyword arguments for examples"
+            )
+        if not (args or kwargs):
+            raise InvalidArgument("An example must provide at least one argument")
 
-    def accept(test):
+        self.hypothesis_explicit_examples: List[Example] = []
+        self._this_example = Example(tuple(args), kwargs)
+
+    def __call__(self, test: TestFunc) -> TestFunc:
         if not hasattr(test, "hypothesis_explicit_examples"):
-            test.hypothesis_explicit_examples = hypothesis_explicit_examples
-        test.hypothesis_explicit_examples.append(Example(tuple(args), kwargs))
+            test.hypothesis_explicit_examples = self.hypothesis_explicit_examples  # type: ignore
+        test.hypothesis_explicit_examples.append(self._this_example)  # type: ignore
         return test
 
-    accept.hypothesis_explicit_examples = hypothesis_explicit_examples  # type: ignore
-    return accept
+    def via(self, *whence: str) -> "example":
+        """Attach a machine-readable label noting whence this example came.
+
+        The idea is that tools will be able to add ``@example()`` cases for you, e.g.
+        to maintain a high-coverage set of explicit examples, but also *remove* them
+        if they become redundant - without ever deleting manually-added examples:
+
+        .. code-block:: python
+
+            # You can choose to annotate examples, or not, as you prefer
+            @example(...)
+            @example(...).via("regression test for issue #42")
+
+            # The `hy-` prefix is reserved for automated tooling
+            @example(...).via("hy-failing")
+            @example(...).via("hy-coverage")
+            @example(...).via("hy-target-$label")
+            def test(x):
+                pass
+
+        Note that this "method chaining" syntax requires Python 3.9 or later, for
+        :pep:`614` relaxing grammar restrictions on decorators.  If you need to
+        support older versions of Python, you can use an identity function:
+
+        .. code-block:: python
+
+            def identity(x):
+                return x
+
+
+            @identity(example(...).via("label"))
+            def test(x):
+                pass
+
+        """
+        if len(whence) != 1 or not isinstance(whence[0], str):
+            raise InvalidArgument(".via() must be passed a string")
+        # 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
+        # We want a positional-only argument, and on Python 3.8+ we'll get it.
+        __sig = get_signature(via)
+        via = define_function_signature(
+            name=via.__name__,
+            docstring=via.__doc__,
+            signature=__sig.replace(
+                parameters=[
+                    p.replace(kind=inspect.Parameter.POSITIONAL_ONLY)
+                    for p in __sig.parameters.values()
+                ]
+            ),
+        )(via)
+        del __sig
 
 
 def seed(seed: Hashable) -> Callable[[TestFunc], TestFunc]:
@@ -292,7 +346,7 @@ def is_invalid_test(test, original_sig, given_arguments, given_kwargs):
             f"arguments, but got {len(given_arguments)} {given_arguments!r}"
         )
 
-    if infer in given_arguments:
+    if ... in given_arguments:
         return invalid(
             "... was passed as a positional argument to @given,  but may only be "
             "passed as a keyword argument or as the sole argument of @given"
@@ -437,7 +491,11 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
                     err = new
 
                 yield (fragments_reported, err)
-                if state.settings.report_multiple_bugs and pytest_shows_exceptiongroups:
+                if (
+                    state.settings.report_multiple_bugs
+                    and pytest_shows_exceptiongroups
+                    and not isinstance(err, skip_exceptions_to_reraise())
+                ):
                     continue
                 break
             finally:
@@ -881,12 +939,13 @@ class StateForActualGivenExecution:
                 errors_to_report.append((fragments, err))
             except BaseException as e:
                 # If we have anything for explain-mode, this is the time to report.
-                for line in explanations[falsifying_example.interesting_origin]:
-                    fragments.append(line)
+                fragments.extend(explanations[falsifying_example.interesting_origin])
                 errors_to_report.append(
                     (fragments, e.with_traceback(get_trimmed_traceback()))
                 )
-
+            else:
+                # execute_once() will always raise either the expected error, or Flaky.
+                raise NotImplementedError("This should be unreachable")
             finally:
                 # Whether or not replay actually raised the exception again, we want
                 # to print the reproduce_failure decorator for the failing example.
@@ -899,7 +958,8 @@ class StateForActualGivenExecution:
                 # Mostly useful for ``find`` and ensuring that objects that
                 # hold on to a reference to ``data`` know that it's now been
                 # finished and they can't draw more data from it.
-                ran_example.freeze()
+                ran_example.freeze()  # pragma: no branch
+                # No branch is possible here because we never have an active exception.
         _raise_to_user(errors_to_report, self.settings, report_lines)
 
 
@@ -995,7 +1055,7 @@ class HypothesisHandle:
 
 @overload
 def given(
-    *_given_arguments: Union[SearchStrategy[Any], InferType],
+    *_given_arguments: Union[SearchStrategy[Any], EllipsisType],
 ) -> Callable[
     [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
 ]:  # pragma: no cover
@@ -1004,7 +1064,7 @@ def given(
 
 @overload
 def given(
-    **_given_kwargs: Union[SearchStrategy[Any], InferType],
+    **_given_kwargs: Union[SearchStrategy[Any], EllipsisType],
 ) -> Callable[
     [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
 ]:  # pragma: no cover
@@ -1012,8 +1072,8 @@ def given(
 
 
 def given(
-    *_given_arguments: Union[SearchStrategy[Any], InferType],
-    **_given_kwargs: Union[SearchStrategy[Any], InferType],
+    *_given_arguments: Union[SearchStrategy[Any], EllipsisType],
+    **_given_kwargs: Union[SearchStrategy[Any], EllipsisType],
 ) -> Callable[
     [Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
 ]:
@@ -1066,9 +1126,9 @@ def given(
         new_signature = new_given_signature(original_sig, given_kwargs)
 
         # Use type information to convert "infer" arguments into appropriate strategies.
-        if infer in given_kwargs.values():
+        if ... in given_kwargs.values():
             hints = get_type_hints(test)
-        for name in [name for name, value in given_kwargs.items() if value is infer]:
+        for name in [name for name, value in given_kwargs.items() if value is ...]:
             if name not in hints:
                 return _invalid(
                     f"passed {name}=... for {test.__name__}, but {name} has "
@@ -1193,6 +1253,14 @@ def given(
                 # If we're not going to report multiple bugs, we would have
                 # stopped running explicit examples at the first failure.
                 assert len(errors) == 1 or state.settings.report_multiple_bugs
+
+                # If an explicit example raised a 'skip' exception, ensure it's never
+                # wrapped up in an exception group.  Because we break out of the loop
+                # immediately on finding a skip, if present it's always the last error.
+                if isinstance(errors[-1][1], skip_exceptions_to_reraise()):
+                    # Covered by `test_issue_3453_regression`, just in a subprocess.
+                    del errors[:-1]  # pragma: no cover
+
                 _raise_to_user(errors, state.settings, [], " in explicit examples")
 
             # If there were any explicit examples, they all ran successfully.
diff --git a/hypothesis-python/src/hypothesis/entry_points.py b/hypothesis-python/src/hypothesis/entry_points.py
index 00e2e11ed..e99ac5ffc 100644
--- a/hypothesis-python/src/hypothesis/entry_points.py
+++ b/hypothesis-python/src/hypothesis/entry_points.py
@@ -26,7 +26,7 @@ try:
     def get_entry_points():
         try:
             eps = importlib_metadata.entry_points(group="hypothesis")
-        except TypeError:
+        except TypeError:  # pragma: no cover
             # Load-time selection requires Python >= 3.10 or importlib_metadata >= 3.6,
             # so we'll retain this fallback logic for some time to come.  See also
             # https://importlib-metadata.readthedocs.io/en/latest/using.html
diff --git a/hypothesis-python/src/hypothesis/extra/_array_helpers.py b/hypothesis-python/src/hypothesis/extra/_array_helpers.py
index 8bb49242f..5e6718eea 100644
--- a/hypothesis-python/src/hypothesis/extra/_array_helpers.py
+++ b/hypothesis-python/src/hypothesis/extra/_array_helpers.py
@@ -387,8 +387,8 @@ def mutually_broadcastable_shapes(
         ...     print(strat.example())
         BroadcastableShapes(input_shapes=((4, 1, 3), (4, 2, 3), ()), result_shape=(4, 2, 3))
         BroadcastableShapes(input_shapes=((3,), (1, 3), (2, 3)), result_shape=(2, 3))
-        BroadcastableShapes(input_shapes=((), (), ()), result_shape=(2, 3))
-        BroadcastableShapes(input_shapes=((3,), (), (3,)), result_shape=(2, 3))
+        BroadcastableShapes(input_shapes=((), (), ()), result_shape=())
+        BroadcastableShapes(input_shapes=((3,), (), (3,)), result_shape=(3,))
         BroadcastableShapes(input_shapes=((1, 2, 3), (3,), ()), result_shape=(1, 2, 3))
 
     """
diff --git a/hypothesis-python/src/hypothesis/extra/array_api.py b/hypothesis-python/src/hypothesis/extra/array_api.py
index 0b849d89b..130959345 100644
--- a/hypothesis-python/src/hypothesis/extra/array_api.py
+++ b/hypothesis-python/src/hypothesis/extra/array_api.py
@@ -8,8 +8,12 @@
 # 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 math
 import sys
+
+if sys.version_info[:2] < (3, 8):
+    raise RuntimeError("The Array API standard requires Python 3.8 or later")
+
+import math
 from numbers import Real
 from types import SimpleNamespace
 from typing import (
@@ -17,6 +21,7 @@ from typing import (
     Iterable,
     Iterator,
     List,
+    Literal,
     Mapping,
     NamedTuple,
     Optional,
@@ -25,8 +30,10 @@ from typing import (
     Type,
     TypeVar,
     Union,
+    get_args,
 )
 from warnings import warn
+from weakref import WeakValueDictionary
 
 from hypothesis import strategies as st
 from hypothesis.errors import HypothesisWarning, InvalidArgument
@@ -62,11 +69,20 @@ __all__ = [
 ]
 
 
+RELEASED_VERSIONS = ("2021.12",)
+NOMINAL_VERSIONS = RELEASED_VERSIONS + ("draft",)
+assert sorted(NOMINAL_VERSIONS) == list(NOMINAL_VERSIONS)  # sanity check
+NominalVersion = Literal["2021.12", "draft"]
+assert get_args(NominalVersion) == NOMINAL_VERSIONS  # sanity check
+
+
 INT_NAMES = ("int8", "int16", "int32", "int64")
 UINT_NAMES = ("uint8", "uint16", "uint32", "uint64")
 ALL_INT_NAMES = INT_NAMES + UINT_NAMES
 FLOAT_NAMES = ("float32", "float64")
-NUMERIC_NAMES = ALL_INT_NAMES + FLOAT_NAMES
+REAL_NAMES = ALL_INT_NAMES + FLOAT_NAMES
+COMPLEX_NAMES = ("complex64", "complex128")
+NUMERIC_NAMES = REAL_NAMES + COMPLEX_NAMES
 DTYPE_NAMES = ("bool",) + NUMERIC_NAMES
 
 DataType = TypeVar("DataType")
@@ -106,8 +122,8 @@ def warn_on_missing_dtypes(xp: Any, stubs: List[str]) -> None:
 
 
 def find_castable_builtin_for_dtype(
-    xp: Any, dtype: DataType
-) -> Type[Union[bool, int, float]]:
+    xp: Any, api_version: NominalVersion, dtype: DataType
+) -> Type[Union[bool, int, float, complex]]:
     """Returns builtin type which can have values that are castable to the given
     dtype, according to :xp-ref:`type promotion rules <type_promotion.html>`.
 
@@ -136,13 +152,22 @@ def find_castable_builtin_for_dtype(
 
     stubs.extend(int_stubs)
     stubs.extend(float_stubs)
+
+    if api_version > "2021.12":
+        complex_dtypes, complex_stubs = partition_attributes_and_stubs(
+            xp, COMPLEX_NAMES
+        )
+        if dtype in complex_dtypes:
+            return complex
+        stubs.extend(complex_stubs)
+
     if len(stubs) > 0:
         warn_on_missing_dtypes(xp, stubs)
     raise InvalidArgument(f"dtype={dtype} not recognised in {xp.__name__}")
 
 
 @check_function
-def dtype_from_name(xp: Any, name: str) -> DataType:
+def dtype_from_name(xp: Any, name: str) -> Any:
     if name in DTYPE_NAMES:
         try:
             return getattr(xp, name)
@@ -159,6 +184,7 @@ def dtype_from_name(xp: Any, name: str) -> DataType:
 
 def _from_dtype(
     xp: Any,
+    api_version: NominalVersion,
     dtype: Union[DataType, str],
     *,
     min_value: Optional[Union[int, float]] = None,
@@ -184,11 +210,12 @@ def _from_dtype(
     passed through from :func:`arrays()`, as it seamlessly handles the ``width``
     or other representable bounds for you.
     """
+    # TODO: for next released xp version, add note for complex dtype support
     check_xp_attributes(xp, ["iinfo", "finfo"])
 
     if isinstance(dtype, str):
         dtype = dtype_from_name(xp, dtype)
-    builtin = find_castable_builtin_for_dtype(xp, dtype)
+    builtin = find_castable_builtin_for_dtype(xp, api_version, dtype)
 
     def check_valid_minmax(prefix, val, info_obj):
         name = f"{prefix}_value"
@@ -218,7 +245,7 @@ def _from_dtype(
         check_valid_minmax("max", max_value, iinfo)
         check_valid_interval(min_value, max_value, "min_value", "max_value")
         return st.integers(min_value=min_value, max_value=max_value)
-    else:
+    elif builtin is float:
         finfo = xp.finfo(dtype)
         kw = {}
 
@@ -269,10 +296,37 @@ def _from_dtype(
             kw["exclude_max"] = exclude_max
 
         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,
+            allow_nan=allow_nan,
+            allow_infinity=allow_infinity,
+            allow_subnormal=allow_subnormal,
+        )
+
+        return st.builds(complex, floats, floats)  # type: ignore[arg-type]
 
 
 class ArrayStrategy(st.SearchStrategy):
-    def __init__(self, xp, elements_strategy, dtype, shape, fill, unique):
+    def __init__(
+        self, *, xp, api_version, elements_strategy, dtype, shape, fill, unique
+    ):
         self.xp = xp
         self.elements_strategy = elements_strategy
         self.dtype = dtype
@@ -280,12 +334,11 @@ class ArrayStrategy(st.SearchStrategy):
         self.fill = fill
         self.unique = unique
         self.array_size = math.prod(shape)
-        self.builtin = find_castable_builtin_for_dtype(xp, dtype)
+        self.builtin = find_castable_builtin_for_dtype(xp, api_version, dtype)
         self.finfo = None if self.builtin is not float else xp.finfo(self.dtype)
 
     def check_set_value(self, val, val_0d, strategy):
-        finite = self.builtin is bool or self.xp.isfinite(val_0d)
-        if finite and self.builtin(val_0d) != val:
+        if val == val and self.builtin(val_0d) != val:
             if self.builtin is float:
                 assert self.finfo is not None  # for mypy
                 try:
@@ -418,6 +471,7 @@ class ArrayStrategy(st.SearchStrategy):
 
 def _arrays(
     xp: Any,
+    api_version: NominalVersion,
     dtype: Union[DataType, str, st.SearchStrategy[DataType], st.SearchStrategy[str]],
     shape: Union[int, Shape, st.SearchStrategy[Shape]],
     *,
@@ -500,14 +554,18 @@ def _arrays(
 
     if isinstance(dtype, st.SearchStrategy):
         return dtype.flatmap(
-            lambda d: _arrays(xp, d, shape, elements=elements, fill=fill, unique=unique)
+            lambda d: _arrays(
+                xp, api_version, d, shape, elements=elements, fill=fill, unique=unique
+            )
         )
     elif isinstance(dtype, str):
         dtype = dtype_from_name(xp, dtype)
 
     if isinstance(shape, st.SearchStrategy):
         return shape.flatmap(
-            lambda s: _arrays(xp, dtype, s, elements=elements, fill=fill, unique=unique)
+            lambda s: _arrays(
+                xp, api_version, dtype, s, elements=elements, fill=fill, unique=unique
+            )
         )
     elif isinstance(shape, int):
         shape = (shape,)
@@ -519,9 +577,9 @@ def _arrays(
     )
 
     if elements is None:
-        elements = _from_dtype(xp, dtype)
+        elements = _from_dtype(xp, api_version, dtype)
     elif isinstance(elements, Mapping):
-        elements = _from_dtype(xp, dtype, **elements)
+        elements = _from_dtype(xp, api_version, dtype, **elements)
     check_strategy(elements, "elements")
 
     if fill is None:
@@ -532,7 +590,15 @@ def _arrays(
             fill = elements
     check_strategy(fill, "fill")
 
-    return ArrayStrategy(xp, elements, dtype, shape, fill, unique)
+    return ArrayStrategy(
+        xp=xp,
+        api_version=api_version,
+        elements_strategy=elements,
+        dtype=dtype,
+        shape=shape,
+        fill=fill,
+        unique=unique,
+    )
 
 
 @check_function
@@ -548,9 +614,9 @@ def check_dtypes(xp: Any, dtypes: List[DataType], stubs: List[str]) -> None:
         warn_on_missing_dtypes(xp, stubs)
 
 
-def _scalar_dtypes(xp: Any) -> st.SearchStrategy[DataType]:
+def _scalar_dtypes(xp: Any, api_version: NominalVersion) -> st.SearchStrategy[DataType]:
     """Return a strategy for all :xp-ref:`valid dtype <data_types.html>` objects."""
-    return st.one_of(_boolean_dtypes(xp), _numeric_dtypes(xp))
+    return st.one_of(_boolean_dtypes(xp), _numeric_dtypes(xp, api_version))
 
 
 def _boolean_dtypes(xp: Any) -> st.SearchStrategy[DataType]:
@@ -563,8 +629,8 @@ def _boolean_dtypes(xp: Any) -> st.SearchStrategy[DataType]:
         ) from None
 
 
-def _numeric_dtypes(xp: Any) -> st.SearchStrategy[DataType]:
-    """Return a strategy for all numeric dtype objects."""
+def _real_dtypes(xp: Any) -> st.SearchStrategy[DataType]:
+    """Return a strategy for all real-valued dtype objects."""
     return st.one_of(
         _integer_dtypes(xp),
         _unsigned_integer_dtypes(xp),
@@ -572,6 +638,16 @@ def _numeric_dtypes(xp: Any) -> st.SearchStrategy[DataType]:
     )
 
 
+def _numeric_dtypes(
+    xp: Any, api_version: NominalVersion
+) -> st.SearchStrategy[DataType]:
+    """Return a strategy for all numeric dtype objects."""
+    strat: st.SearchStrategy[DataType] = _real_dtypes(xp)
+    if api_version > "2021.12":
+        strat |= _complex_dtypes(xp)
+    return strat
+
+
 @check_function
 def check_valid_sizes(
     category: str, sizes: Sequence[int], valid_sizes: Sequence[int]
@@ -634,7 +710,7 @@ def _unsigned_integer_dtypes(
 def _floating_dtypes(
     xp: Any, *, sizes: Union[int, Sequence[int]] = (32, 64)
 ) -> st.SearchStrategy[DataType]:
-    """Return a strategy for floating-point dtype objects.
+    """Return a strategy for real-valued floating-point dtype objects.
 
     ``sizes`` contains the floating-point sizes in bits, defaulting to
     ``(32, 64)`` which covers all valid sizes.
@@ -649,6 +725,24 @@ def _floating_dtypes(
     return st.sampled_from(dtypes)
 
 
+def _complex_dtypes(
+    xp: Any, *, sizes: Union[int, Sequence[int]] = (64, 128)
+) -> st.SearchStrategy[DataType]:
+    """Return a strategy for complex dtype objects.
+
+    ``sizes`` contains the complex sizes in bits, defaulting to ``(64, 128)``
+    which covers all valid sizes.
+    """
+    if isinstance(sizes, int):
+        sizes = (sizes,)
+    check_valid_sizes("complex", sizes, (64, 128))
+    dtypes, stubs = partition_attributes_and_stubs(
+        xp, numeric_dtype_names("complex", sizes)
+    )
+    check_dtypes(xp, dtypes, stubs)
+    return st.sampled_from(dtypes)
+
+
 @proxies(_valid_tuple_axes)
 def valid_tuple_axes(*args, **kwargs):
     return _valid_tuple_axes(*args, **kwargs)
@@ -760,10 +854,21 @@ def indices(
     )
 
 
-def make_strategies_namespace(xp: Any) -> SimpleNamespace:
+# Cache for make_strategies_namespace()
+_args_to_xps: WeakValueDictionary = WeakValueDictionary()
+
+
+def make_strategies_namespace(
+    xp: Any, *, api_version: Optional[NominalVersion] = None
+) -> SimpleNamespace:
     """Creates a strategies namespace for the given array module.
 
     * ``xp`` is the Array API library to automatically pass to the namespaced methods.
+    * ``api_version`` is the version of the Array API which the returned
+      strategies namespace should conform to. If ``None``, the latest API
+      version which ``xp`` supports will be inferred from ``xp.__array_api_version__``.
+      If a version string in the ``YYYY.MM`` format, the strategies namespace
+      will conform to that version if supported.
 
     A :obj:`python:types.SimpleNamespace` is returned which contains all the
     strategy methods in this module but without requiring the ``xp`` argument.
@@ -772,8 +877,11 @@ def make_strategies_namespace(xp: Any) -> SimpleNamespace:
 
     .. code-block:: pycon
 
-      >>> from numpy import array_api as xp
+      >>> xp.__array_api_version__  # xp is your desired array library
+      '2021.12'
       >>> xps = make_strategies_namespace(xp)
+      >>> xps.api_version
+      '2021.12'
       >>> x = xps.arrays(xp.int8, (2, 3)).example()
       >>> x
       Array([[-8,  6,  3],
@@ -782,6 +890,34 @@ def make_strategies_namespace(xp: Any) -> SimpleNamespace:
       True
 
     """
+    not_available_msg = (
+        "If the standard version you want is not available, please ensure "
+        "you're using the latest version of Hypothesis, then open an issue if "
+        "one doesn't already exist."
+    )
+    if api_version is None:
+        check_argument(
+            hasattr(xp, "__array_api_version__"),
+            f"Array module {xp.__name__} has no attribute __array_api_version__, "
+            "which is required when inferring api_version. If you believe "
+            f"{xp.__name__} is indeed an Array API module, try explicitly "
+            "passing an api_version.",
+        )
+        check_argument(
+            isinstance(xp.__array_api_version__, str)
+            and xp.__array_api_version__ in RELEASED_VERSIONS,
+            f"xp.__array_api_version__={xp.__array_api_version__!r}, but it must "
+            f"be a valid version string {RELEASED_VERSIONS}. {not_available_msg}",
+        )
+        api_version = xp.__array_api_version__
+        inferred_version = True
+    else:
+        check_argument(
+            isinstance(api_version, str) and api_version in NOMINAL_VERSIONS,
+            f"api_version={api_version!r}, but it must be None, or a valid version "
+            f"string in {RELEASED_VERSIONS}. {not_available_msg}",
+        )
+        inferred_version = False
     try:
         array = xp.zeros(1)
         array.__array_namespace__()
@@ -791,6 +927,13 @@ def make_strategies_namespace(xp: Any) -> SimpleNamespace:
             HypothesisWarning,
         )
 
+    try:
+        namespace = _args_to_xps[(xp, api_version)]
+    except (KeyError, TypeError):
+        pass
+    else:
+        return namespace
+
     @defines_strategy(force_reusable_values=True)
     def from_dtype(
         dtype: Union[DataType, str],
@@ -805,6 +948,7 @@ def make_strategies_namespace(xp: Any) -> SimpleNamespace:
     ) -> st.SearchStrategy[Union[bool, int, float]]:
         return _from_dtype(
             xp,
+            api_version,  # type: ignore[arg-type]
             dtype,
             min_value=min_value,
             max_value=max_value,
@@ -828,6 +972,7 @@ def make_strategies_namespace(xp: Any) -> SimpleNamespace:
     ) -> st.SearchStrategy:
         return _arrays(
             xp,
+            api_version,  # type: ignore[arg-type]
             dtype,
             shape,
             elements=elements,
@@ -837,15 +982,19 @@ def make_strategies_namespace(xp: Any) -> SimpleNamespace:
 
     @defines_strategy()
     def scalar_dtypes() -> st.SearchStrategy[DataType]:
-        return _scalar_dtypes(xp)
+        return _scalar_dtypes(xp, api_version)  # type: ignore[arg-type]
 
     @defines_strategy()
     def boolean_dtypes() -> st.SearchStrategy[DataType]:
         return _boolean_dtypes(xp)
 
+    @defines_strategy()
+    def real_dtypes() -> st.SearchStrategy[DataType]:
+        return _real_dtypes(xp)
+
     @defines_strategy()
     def numeric_dtypes() -> st.SearchStrategy[DataType]:
-        return _numeric_dtypes(xp)
+        return _numeric_dtypes(xp, api_version)  # type: ignore[arg-type]
 
     @defines_strategy()
     def integer_dtypes(
@@ -869,21 +1018,45 @@ def make_strategies_namespace(xp: Any) -> SimpleNamespace:
     arrays.__doc__ = _arrays.__doc__
     scalar_dtypes.__doc__ = _scalar_dtypes.__doc__
     boolean_dtypes.__doc__ = _boolean_dtypes.__doc__
+    real_dtypes.__doc__ = _real_dtypes.__doc__
     numeric_dtypes.__doc__ = _numeric_dtypes.__doc__
     integer_dtypes.__doc__ = _integer_dtypes.__doc__
     unsigned_integer_dtypes.__doc__ = _unsigned_integer_dtypes.__doc__
     floating_dtypes.__doc__ = _floating_dtypes.__doc__
 
-    class PrettySimpleNamespace(SimpleNamespace):
-        def __repr__(self):
-            return f"make_strategies_namespace({xp.__name__})"
+    class StrategiesNamespace(SimpleNamespace):
+        def __init__(self, **kwargs):
+            for attr in ["name", "api_version"]:
+                if attr not in kwargs.keys():
+                    raise ValueError(f"'{attr}' kwarg required")
+            super().__init__(**kwargs)
+
+        @property
+        def complex_dtypes(self):
+            try:
+                return self.__dict__["complex_dtypes"]
+            except KeyError as e:
+                raise AttributeError(
+                    "You attempted to access 'complex_dtypes', but it is not "
+                    f"available for api_version='{self.api_version}' of "
+                    f"xp={self.name}."
+                ) from e
 
-    return PrettySimpleNamespace(
+        def __repr__(self):
+            f_args = self.name
+            if not inferred_version:
+                f_args += f", api_version='{self.api_version}'"
+            return f"make_strategies_namespace({f_args})"
+
+    kwargs = dict(
+        name=xp.__name__,
+        api_version=api_version,
         from_dtype=from_dtype,
         arrays=arrays,
         array_shapes=array_shapes,
         scalar_dtypes=scalar_dtypes,
         boolean_dtypes=boolean_dtypes,
+        real_dtypes=real_dtypes,
         numeric_dtypes=numeric_dtypes,
         integer_dtypes=integer_dtypes,
         unsigned_integer_dtypes=unsigned_integer_dtypes,
@@ -894,6 +1067,25 @@ def make_strategies_namespace(xp: Any) -> SimpleNamespace:
         indices=indices,
     )
 
+    if api_version > "2021.12":
+
+        @defines_strategy()
+        def complex_dtypes(
+            *, sizes: Union[int, Sequence[int]] = (64, 128)
+        ) -> st.SearchStrategy[DataType]:
+            return _complex_dtypes(xp, sizes=sizes)
+
+        complex_dtypes.__doc__ = _complex_dtypes.__doc__
+        kwargs["complex_dtypes"] = complex_dtypes
+
+    namespace = StrategiesNamespace(**kwargs)
+    try:
+        _args_to_xps[(xp, api_version)] = namespace
+    except TypeError:
+        pass
+
+    return namespace
+
 
 try:
     import numpy as np
@@ -935,7 +1127,8 @@ if np is not None:
         )
 
     mock_xp = SimpleNamespace(
-        __name__="mockpy",
+        __name__="mock",
+        __array_api_version__="2021.12",
         # Data types
         int8=np.int8,
         int16=np.int16,
@@ -947,6 +1140,8 @@ if np is not None:
         uint64=np.uint64,
         float32=np.float32,
         float64=np.float64,
+        complex64=np.complex64,
+        complex128=np.complex128,
         bool=np.bool_,
         # Constants
         nan=np.nan,
diff --git a/hypothesis-python/src/hypothesis/extra/codemods.py b/hypothesis-python/src/hypothesis/extra/codemods.py
index a254fb462..6bced42c8 100644
--- a/hypothesis-python/src/hypothesis/extra/codemods.py
+++ b/hypothesis-python/src/hypothesis/extra/codemods.py
@@ -197,7 +197,10 @@ class HypothesisFixPositionalKeywonlyArgs(VisitorBasedCodemodCommand):
         # Get the actual function object so that we can inspect the signature.
         # This does e.g. incur a dependency on Numpy to fix Numpy-dependent code,
         # but having a single source of truth about the signatures is worth it.
-        params = signature(get_fn(*qualnames)).parameters.values()
+        try:
+            params = signature(get_fn(*qualnames)).parameters.values()
+        except ModuleNotFoundError:
+            return updated_node
 
         # st.floats() has a new allow_subnormal kwonly argument not at the end,
         # so we do a bit more of a dance here.
diff --git a/hypothesis-python/src/hypothesis/extra/django/_impl.py b/hypothesis-python/src/hypothesis/extra/django/_impl.py
index f5f7e7c8a..397898cdd 100644
--- a/hypothesis-python/src/hypothesis/extra/django/_impl.py
+++ b/hypothesis-python/src/hypothesis/extra/django/_impl.py
@@ -24,14 +24,13 @@ from hypothesis.errors import InvalidArgument
 from hypothesis.extra.django._fields import from_field
 from hypothesis.internal.reflection import define_function_signature
 from hypothesis.strategies._internal.utils import defines_strategy
-from hypothesis.utils.conventions import infer
 
 if sys.version_info >= (3, 10):  # pragma: no cover
-    from types import EllipsisType as InferType
+    from types import EllipsisType as EllipsisType
 elif TYPE_CHECKING:
-    from builtins import ellipsis as InferType
+    from builtins import ellipsis as EllipsisType
 else:
-    InferType = type(Ellipsis)
+    EllipsisType = type(Ellipsis)
 
 
 class HypothesisTestCase:
@@ -67,7 +66,7 @@ class StaticLiveServerTestCase(HypothesisTestCase, dst.StaticLiveServerTestCase)
 
 @defines_strategy()
 def from_model(
-    *model: Type[dm.Model], **field_strategies: Union[st.SearchStrategy, InferType]
+    *model: Type[dm.Model], **field_strategies: Union[st.SearchStrategy, EllipsisType]
 ) -> st.SearchStrategy:
     """Return a strategy for examples of ``model``.
 
@@ -106,7 +105,7 @@ def from_model(
 
     fields_by_name = {f.name: f for f in m_type._meta.concrete_fields}
     for name, value in sorted(field_strategies.items()):
-        if value is infer:
+        if value is ...:
             field_strategies[name] = from_field(fields_by_name[name])
     for name, field in sorted(fields_by_name.items()):
         if (
@@ -157,7 +156,7 @@ def _models_impl(draw, strat):
 def from_form(
     form: Type[df.Form],
     form_kwargs: Optional[dict] = None,
-    **field_strategies: Union[st.SearchStrategy, InferType],
+    **field_strategies: Union[st.SearchStrategy, EllipsisType],
 ) -> st.SearchStrategy[df.Form]:
     """Return a strategy for examples of ``form``.
 
@@ -214,7 +213,7 @@ def from_form(
         else:
             fields_by_name[name] = field
     for name, value in sorted(field_strategies.items()):
-        if value is infer:
+        if value is ...:
             field_strategies[name] = from_field(fields_by_name[name])
 
     for name, field in sorted(fields_by_name.items()):
diff --git a/hypothesis-python/src/hypothesis/extra/ghostwriter.py b/hypothesis-python/src/hypothesis/extra/ghostwriter.py
index fbcab111f..6b24549a4 100644
--- a/hypothesis-python/src/hypothesis/extra/ghostwriter.py
+++ b/hypothesis-python/src/hypothesis/extra/ghostwriter.py
@@ -116,14 +116,13 @@ from hypothesis.strategies._internal.strategies import (
     SampledFromStrategy,
 )
 from hypothesis.strategies._internal.types import _global_type_lookup, is_generic_type
-from hypothesis.utils.conventions import infer
 
 if sys.version_info >= (3, 10):  # pragma: no cover
-    from types import EllipsisType as InferType
+    from types import EllipsisType as EllipsisType
 elif TYPE_CHECKING:
-    from builtins import ellipsis as InferType
+    from builtins import ellipsis as EllipsisType
 else:
-    InferType = type(Ellipsis)
+    EllipsisType = type(Ellipsis)
 
 
 IMPORT_SECTION = """
@@ -252,10 +251,7 @@ def _type_from_doc_fragment(token: str) -> Optional[type]:
     return getattr(sys.modules.get(mod, None), name, None)
 
 
-def _strategy_for(
-    param: inspect.Parameter,
-    docstring: str,
-) -> Union[st.SearchStrategy, InferType]:
+def _strategy_for(param: inspect.Parameter, docstring: str) -> st.SearchStrategy:
     # Example types in docstrings:
     # - `:type a: sequence of integers`
     # - `b (list, tuple, or None): ...`
@@ -532,7 +528,7 @@ def _get_strategies(
         hints = get_type_hints(f)
         docstring = getattr(f, "__doc__", None) or ""
         builder_args = {
-            k: infer if k in hints else _strategy_for(v, docstring)
+            k: ... if k in hints else _strategy_for(v, docstring)
             for k, v in params.items()
         }
         with _with_any_registered():
@@ -673,11 +669,25 @@ def _valid_syntax_repr(strategy):
 KNOWN_FUNCTION_LOCATIONS: Dict[object, str] = {}
 
 
+def _get_module_helper(obj):
+    # Get the __module__ attribute of the object, and return the first ancestor module
+    # which contains the object; falling back to the literal __module__ if none do.
+    # The goal is to show location from which obj should usually be accessed, rather
+    # than what we assume is an internal submodule which defined it.
+    module_name = obj.__module__
+    dots = [i for i, c in enumerate(module_name) if c == "."] + [None]
+    for idx in dots:
+        if getattr(sys.modules.get(module_name[:idx]), obj.__name__, None) is obj:
+            KNOWN_FUNCTION_LOCATIONS[obj] = module_name[:idx]
+            return module_name[:idx]
+    return module_name
+
+
 def _get_module(obj):
     if obj in KNOWN_FUNCTION_LOCATIONS:
         return KNOWN_FUNCTION_LOCATIONS[obj]
     try:
-        return obj.__module__
+        return _get_module_helper(obj)
     except AttributeError:
         if not _is_probably_ufunc(obj):
             raise
@@ -936,7 +946,7 @@ def magic(
                     functions.add(f)
                     if getattr(thing, "__name__", None):
                         if inspect.isclass(thing):
-                            KNOWN_FUNCTION_LOCATIONS[f] = thing.__module__
+                            KNOWN_FUNCTION_LOCATIONS[f] = _get_module_helper(thing)
                         else:
                             KNOWN_FUNCTION_LOCATIONS[f] = thing.__name__
             except (TypeError, ValueError):
@@ -1309,7 +1319,7 @@ def binary_operation(
     *,
     associative: bool = True,
     commutative: bool = True,
-    identity: Union[X, InferType, None] = infer,
+    identity: Union[X, EllipsisType, None] = ...,
     distributes_over: Optional[Callable[[X, X], X]] = None,
     except_: Except = (),
     style: str = "pytest",
@@ -1370,7 +1380,7 @@ def _make_binop_body(
     *,
     associative: bool = True,
     commutative: bool = True,
-    identity: Union[X, InferType, None] = infer,
+    identity: Union[X, EllipsisType, None] = ...,
     distributes_over: Optional[Callable[[X, X], X]] = None,
     except_: Tuple[Type[Exception], ...],
     style: str,
@@ -1440,7 +1450,7 @@ def _make_binop_body(
         # Guess that the identity element is the minimal example from our operands
         # strategy.  This is correct often enough to be worthwhile, and close enough
         # that it's a good starting point to edit much of the rest.
-        if identity is infer:
+        if identity is ...:
             try:
                 identity = find(operands, lambda x: True, settings=_quietly_settings)
             except Exception:
@@ -1450,7 +1460,7 @@ def _make_binop_body(
         # happen.  E.g. type(None) -> <class 'NoneType'> -> quoted.
         try:
             # We don't actually execute this code object; we're just compiling
-            # to check that the repr is syntatically valid.  HOWEVER, we're
+            # to check that the repr is syntactically valid.  HOWEVER, we're
             # going to output that code string into test code which will be
             # executed; so you still shouldn't ghostwrite for hostile code.
             compile(repr(identity), "<string>", "exec")
diff --git a/hypothesis-python/src/hypothesis/extra/lark.py b/hypothesis-python/src/hypothesis/extra/lark.py
index 127604b8e..632011566 100644
--- a/hypothesis-python/src/hypothesis/extra/lark.py
+++ b/hypothesis-python/src/hypothesis/extra/lark.py
@@ -22,11 +22,6 @@ Lark already `supports loading grammars
 <https://lark-parser.readthedocs.io/en/latest/nearley.html>`_
 from `nearley.js <https://nearley.js.org/>`_, so you may not have to write
 your own at all.
-
-Note that as Lark is at version 0.x, this module *may* break API compatibility
-in minor releases if supporting the latest version of Lark would otherwise be
-infeasible.  We may also be quite aggressive in bumping the minimum version of
-Lark, unless someone volunteers to either fund or do the maintenance.
 """
 
 from inspect import signature
@@ -34,7 +29,7 @@ from typing import Dict, Optional
 
 import attr
 import lark
-from lark.grammar import NonTerminal, Terminal  # type: ignore
+from lark.grammar import NonTerminal, Terminal
 
 from hypothesis import strategies as st
 from hypothesis.errors import InvalidArgument
diff --git a/hypothesis-python/src/hypothesis/extra/pandas/impl.py b/hypothesis-python/src/hypothesis/extra/pandas/impl.py
index 7158d490f..a1220d6b3 100644
--- a/hypothesis-python/src/hypothesis/extra/pandas/impl.py
+++ b/hypothesis-python/src/hypothesis/extra/pandas/impl.py
@@ -88,6 +88,11 @@ def elements_and_dtype(elements, dtype, source=None):
             has_codemod=False,
         )
 
+    if isinstance(dtype, st.SearchStrategy):
+        raise InvalidArgument(
+            f"Passed dtype={dtype!r} is a strategy, but we require a concrete dtype "
+            "here.  See https://stackoverflow.com/q/74355937 for workaround patterns."
+        )
     dtype = try_convert(np.dtype, dtype, "dtype")
 
     if elements is None:
@@ -532,9 +537,8 @@ def data_frames(
                 hash(c.name)
             except TypeError:
                 raise InvalidArgument(
-                    "Column names must be hashable, but columns[%d].name was "
-                    "%r of type %s, which cannot be hashed."
-                    % (i, c.name, type(c.name).__name__)
+                    f"Column names must be hashable, but columns[{i}].name was "
+                    f"{c.name!r} of type {type(c.name).__name__}, which cannot be hashed."
                 ) from None
 
         if c.name in column_names:
diff --git a/hypothesis-python/src/hypothesis/internal/compat.py b/hypothesis-python/src/hypothesis/internal/compat.py
index 67297bcda..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
@@ -22,6 +53,19 @@ except NameError:
         BaseExceptionGroup as BaseExceptionGroup,
         ExceptionGroup as ExceptionGroup,
     )
+if typing.TYPE_CHECKING:  # pragma: no cover
+    from typing_extensions import Concatenate as Concatenate, ParamSpec as ParamSpec
+else:
+    try:
+        from typing import Concatenate as Concatenate, ParamSpec as ParamSpec
+    except ImportError:
+        try:
+            from typing_extensions import (
+                Concatenate as Concatenate,
+                ParamSpec as ParamSpec,
+            )
+        except ImportError:
+            Concatenate, ParamSpec = None, None
 
 PYPY = platform.python_implementation() == "PyPy"
 WINDOWS = platform.system() == "Windows"
@@ -58,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.
 
@@ -75,7 +123,7 @@ def get_type_hints(thing):
 
     try:
         hints = typing.get_type_hints(thing, **kwargs)
-    except (AttributeError, TypeError, NameError):
+    except (AttributeError, TypeError, NameError):  # pragma: no cover
         hints = {}
 
     if inspect.isclass(thing):
@@ -98,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/hypothesis-python/src/hypothesis/internal/conjecture/choicetree.py b/hypothesis-python/src/hypothesis/internal/conjecture/choicetree.py
index 0f1932e8e..38bcc3a57 100644
--- a/hypothesis-python/src/hypothesis/internal/conjecture/choicetree.py
+++ b/hypothesis-python/src/hypothesis/internal/conjecture/choicetree.py
@@ -142,7 +142,7 @@ class ChoiceTree:
 
 
 class TreeNode:
-    def __init__(self):
+    def __init__(self) -> None:
         self.children: Dict[int, TreeNode] = defaultdict(TreeNode)
         self.live_child_count: "Optional[int]" = None
         self.n: "Optional[int]" = None
diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/data.py b/hypothesis-python/src/hypothesis/internal/conjecture/data.py
index 6f40fc0a6..4cb0b463f 100644
--- a/hypothesis-python/src/hypothesis/internal/conjecture/data.py
+++ b/hypothesis-python/src/hypothesis/internal/conjecture/data.py
@@ -400,10 +400,10 @@ class Examples:
         return self.starts_and_ends[1]
 
     class _discarded(ExampleProperty):
-        def begin(self):
-            self.result: "Set[int]" = set()
+        def begin(self) -> None:
+            self.result: "Set[int]" = set()  # type: ignore  # IntList in parent class
 
-        def finish(self):
+        def finish(self) -> FrozenSet[int]:
             return frozenset(self.result)
 
         def stop_example(self, i: int, discarded: bool) -> None:
@@ -413,9 +413,9 @@ class Examples:
     discarded: FrozenSet[int] = calculated_example_property(_discarded)
 
     class _trivial(ExampleProperty):
-        def begin(self):
+        def begin(self) -> None:
             self.nontrivial = IntList.of_length(len(self.examples))
-            self.result: "Set[int]" = set()
+            self.result: "Set[int]" = set()  # type: ignore  # IntList in parent class
 
         def block(self, i: int) -> None:
             if not self.examples.blocks.trivial(i):
@@ -428,7 +428,7 @@ class Examples:
             else:
                 self.result.add(i)
 
-        def finish(self):
+        def finish(self) -> FrozenSet[int]:
             return frozenset(self.result)
 
     trivial: FrozenSet[int] = calculated_example_property(_trivial)
@@ -456,7 +456,7 @@ class Examples:
     label_indices: IntList = calculated_example_property(_label_indices)
 
     class _mutator_groups(ExampleProperty):
-        def begin(self):
+        def begin(self) -> None:
             self.groups: "Dict[Tuple[int, int], List[int]]" = defaultdict(list)
 
         def start_example(self, i: int, label_index: int) -> None:
diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py b/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py
index 9080d07f6..2ee5d5f4c 100644
--- a/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py
+++ b/hypothesis-python/src/hypothesis/internal/conjecture/shrinker.py
@@ -278,8 +278,8 @@ class Shrinker:
 
         # We keep track of the current best example on the shrink_target
         # attribute.
-        self.shrink_target = None
-        self.update_shrink_target(initial)
+        self.shrink_target = initial
+        self.clear_change_tracking()
         self.shrinks = 0
 
         # We terminate shrinks that seem to have reached their logical
@@ -447,23 +447,15 @@ class Shrinker:
                     return "s" if n != 1 else ""
 
                 total_deleted = self.initial_size - len(self.shrink_target.buffer)
-
-                self.debug("---------------------")
-                self.debug("Shrink pass profiling")
-                self.debug("---------------------")
-                self.debug("")
                 calls = self.engine.call_count - self.initial_calls
+
                 self.debug(
-                    "Shrinking made a total of %d call%s "
-                    "of which %d shrank. This deleted %d byte%s out of %d."
-                    % (
-                        calls,
-                        s(calls),
-                        self.shrinks,
-                        total_deleted,
-                        s(total_deleted),
-                        self.initial_size,
-                    )
+                    "---------------------\n"
+                    "Shrink pass profiling\n"
+                    "---------------------\n\n"
+                    f"Shrinking made a total of {calls} call{s(calls)} of which "
+                    f"{self.shrinks} shrank. This deleted {total_deleted} bytes out "
+                    f"of {self.initial_size}."
                 )
                 for useful in [True, False]:
                     self.debug("")
@@ -828,22 +820,17 @@ class Shrinker:
 
     def update_shrink_target(self, new_target):
         assert isinstance(new_target, ConjectureResult)
-        if self.shrink_target is not None:
-            self.shrinks += 1
-            # If we are just taking a long time to shrink we don't want to
-            # trigger this heuristic, so whenever we shrink successfully
-            # we give ourselves a bit of breathing room to make sure we
-            # would find a shrink that took that long to find the next time.
-            # The case where we're taking a long time but making steady
-            # progress is handled by `finish_shrinking_deadline` in engine.py
-            self.max_stall = max(
-                self.max_stall, (self.calls - self.calls_at_last_shrink) * 2
-            )
-            self.calls_at_last_shrink = self.calls
-        else:
-            self.__all_changed_blocks = set()
-            self.__last_checked_changed_at = new_target
-
+        self.shrinks += 1
+        # If we are just taking a long time to shrink we don't want to
+        # trigger this heuristic, so whenever we shrink successfully
+        # we give ourselves a bit of breathing room to make sure we
+        # would find a shrink that took that long to find the next time.
+        # The case where we're taking a long time but making steady
+        # progress is handled by `finish_shrinking_deadline` in engine.py
+        self.max_stall = max(
+            self.max_stall, (self.calls - self.calls_at_last_shrink) * 2
+        )
+        self.calls_at_last_shrink = self.calls
         self.shrink_target = new_target
         self.__derived_values = {}
 
diff --git a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py
index 2a4d73eab..0133ceaaf 100644
--- a/hypothesis-python/src/hypothesis/internal/conjecture/utils.py
+++ b/hypothesis-python/src/hypothesis/internal/conjecture/utils.py
@@ -320,8 +320,8 @@ class Sampler:
 
         num_type = type(total)
 
-        zero = num_type(0)
-        one = num_type(1)
+        zero = num_type(0)  # type: ignore
+        one = num_type(1)  # type: ignore
 
         small: "List[int]" = []
         large: "List[int]" = []
diff --git a/hypothesis-python/src/hypothesis/internal/entropy.py b/hypothesis-python/src/hypothesis/internal/entropy.py
index cea430b18..d47747792 100644
--- a/hypothesis-python/src/hypothesis/internal/entropy.py
+++ b/hypothesis-python/src/hypothesis/internal/entropy.py
@@ -9,13 +9,33 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 import contextlib
+import gc
 import random
 import sys
+import warnings
 from itertools import count
+from typing import TYPE_CHECKING, Any, Callable, Hashable, Tuple
 from weakref import WeakValueDictionary
 
 import hypothesis.core
-from hypothesis.errors import InvalidArgument
+from hypothesis.errors import HypothesisWarning, InvalidArgument
+from hypothesis.internal.compat import PYPY
+
+if TYPE_CHECKING:
+    if sys.version_info >= (3, 8):  # pragma: no cover
+        from typing import Protocol
+    else:
+        from typing_extensions import Protocol
+
+    # we can't use this at runtime until from_type supports
+    # protocols -- breaks ghostwriter tests
+    class RandomLike(Protocol):
+        seed: Callable[..., Any]
+        getstate: Callable[[], Any]
+        setstate: Callable[..., Any]
+
+else:  # pragma: no cover
+    RandomLike = random.Random
 
 # This is effectively a WeakSet, which allows us to associate the saved states
 # with their respective Random instances even as new ones are registered and old
@@ -39,26 +59,94 @@ class NumpyRandomWrapper:
 NP_RANDOM = None
 
 
-def register_random(r: random.Random) -> None:
-    """Register the given Random instance for management by Hypothesis.
+if not PYPY:
+
+    def _get_platform_base_refcount(r: Any) -> int:
+        return sys.getrefcount(r)
+
+    # Determine the number of refcounts created by function scope for
+    # the given platform / version of Python.
+    _PLATFORM_REF_COUNT = _get_platform_base_refcount(object())
+else:  # pragma: no cover
+    # PYPY doesn't have `sys.getrefcount`
+    _PLATFORM_REF_COUNT = -1
 
-    You can pass ``random.Random`` instances (or other objects with seed,
-    getstate, and setstate methods) to ``register_random(r)`` to have their
-    states seeded and restored in the same way as the global PRNGs from the
-    ``random`` and ``numpy.random`` modules.
+
+def register_random(r: RandomLike) -> None:
+    """Register (a weakref to) the given Random-like instance for management by
+    Hypothesis.
+
+    You can pass instances of structural subtypes of ``random.Random``
+    (i.e., objects with seed, getstate, and setstate methods) to
+    ``register_random(r)`` to have their states seeded and restored in the same
+    way as the global PRNGs from the ``random`` and ``numpy.random`` modules.
 
     All global PRNGs, from e.g. simulation or scheduling frameworks, should
-    be registered to prevent flaky tests.  Hypothesis will ensure that the
-    PRNG state is consistent for all test runs, or reproducibly varied if you
+    be registered to prevent flaky tests. Hypothesis will ensure that the
+    PRNG state is consistent for all test runs, always seeding them to zero and
+    restoring the previous state after the test, or, reproducibly varied if you
     choose to use the :func:`~hypothesis.strategies.random_module` strategy.
+
+    ``register_random`` only makes `weakrefs
+    <https://docs.python.org/3/library/weakref.html#module-weakref>`_ to ``r``,
+    thus ``r`` will only be managed by Hypothesis as long as it has active
+    references elsewhere at runtime. The pattern ``register_random(MyRandom())``
+    will raise a ``ReferenceError`` to help protect users from this issue.
+    This check does not occur for the PyPy interpreter. See the following example for
+    an illustration of this issue
+
+    .. code-block:: python
+
+
+       def my_BROKEN_hook():
+           r = MyRandomLike()
+
+           # `r` will be garbage collected after the hook resolved
+           # and Hypothesis will 'forget' that it was registered
+           register_random(r)  # Hypothesis will emit a warning
+
+
+       rng = MyRandomLike()
+
+
+       def my_WORKING_hook():
+           register_random(rng)
     """
     if not (hasattr(r, "seed") and hasattr(r, "getstate") and hasattr(r, "setstate")):
         raise InvalidArgument(f"r={r!r} does not have all the required methods")
-    if r not in RANDOMS_TO_MANAGE.values():
-        RANDOMS_TO_MANAGE[next(_RKEY)] = r
-
 
-def get_seeder_and_restorer(seed=0):
+    if r in RANDOMS_TO_MANAGE.values():
+        return
+
+    if not PYPY:  # pragma: no branch
+        # PYPY does not have `sys.getrefcount`
+        gc.collect()
+        if not gc.get_referrers(r):
+            if sys.getrefcount(r) <= _PLATFORM_REF_COUNT:
+                raise ReferenceError(
+                    f"`register_random` was passed `r={r}` which will be "
+                    "garbage collected immediately after `register_random` creates a "
+                    "weakref to it. This will prevent Hypothesis from managing this "
+                    "source of RNG. See the docs for `register_random` for more "
+                    "details."
+                )
+            else:
+                warnings.warn(
+                    HypothesisWarning(
+                        "It looks like `register_random` was passed an object "
+                        "that could be garbage collected immediately after "
+                        "`register_random` creates a weakref to it. This will "
+                        "prevent Hypothesis from managing this source of RNG. "
+                        "See the docs for `register_random` for more details."
+                    )
+                )
+
+    RANDOMS_TO_MANAGE[next(_RKEY)] = r
+
+
+def get_seeder_and_restorer(
+    seed: Hashable = 0,
+) -> Tuple[Callable[[], None], Callable[[], None]]:
     """Return a pair of functions which respectively seed all and restore
     the state of all registered PRNGs.
 
diff --git a/hypothesis-python/src/hypothesis/internal/escalation.py b/hypothesis-python/src/hypothesis/internal/escalation.py
index 4eb1fd421..64f214a0c 100644
--- a/hypothesis-python/src/hypothesis/internal/escalation.py
+++ b/hypothesis-python/src/hypothesis/internal/escalation.py
@@ -67,12 +67,12 @@ def escalate_hypothesis_internal_error():
     if PREVENT_ESCALATION:
         return
 
-    error_type, e, tb = sys.exc_info()
+    _, e, tb = sys.exc_info()
 
     if getattr(e, "hypothesis_internal_never_escalate", False):
         return
 
-    filepath = traceback.extract_tb(tb)[-1][0]
+    filepath = None if tb is None else traceback.extract_tb(tb)[-1][0]
     if is_hypothesis_file(filepath) and not isinstance(
         e, (HypothesisException,) + HYPOTHESIS_CONTROL_EXCEPTIONS
     ):
@@ -87,8 +87,10 @@ def get_trimmed_traceback(exception=None):
         tb = exception.__traceback__
     # Avoid trimming the traceback if we're in verbose mode, or the error
     # was raised inside Hypothesis
-    if hypothesis.settings.default.verbosity >= hypothesis.Verbosity.debug or (
-        is_hypothesis_file(traceback.extract_tb(tb)[-1][0])
+    if (
+        tb is None
+        or hypothesis.settings.default.verbosity >= hypothesis.Verbosity.debug
+        or is_hypothesis_file(traceback.extract_tb(tb)[-1][0])
         and not isinstance(exception, _Trimmable)
     ):
         return tb
@@ -111,7 +113,10 @@ def get_interesting_origin(exception):
     # blocks and understand the __cause__ (`raise x from y`) or __context__ that
     # first raised an exception as well as PEP-654 exception groups.
     tb = get_trimmed_traceback(exception)
-    filename, lineno, *_ = traceback.extract_tb(tb)[-1]
+    if tb is None:
+        filename, lineno = None, None
+    else:
+        filename, lineno, *_ = traceback.extract_tb(tb)[-1]
     return (
         type(exception),
         filename,
diff --git a/hypothesis-python/src/hypothesis/internal/filtering.py b/hypothesis-python/src/hypothesis/internal/filtering.py
index e0d576e7e..8f15ba9ec 100644
--- a/hypothesis-python/src/hypothesis/internal/filtering.py
+++ b/hypothesis-python/src/hypothesis/internal/filtering.py
@@ -12,7 +12,7 @@
 
 For example::
 
-    integers().filter(lamda x: x >= 0) -> integers(min_value=0)
+    integers().filter(lambda x: x >= 0) -> integers(min_value=0)
 
 This is intractable in general, but reasonably easy for simple cases involving
 numeric bounds, strings with length or regex constraints, and collection lengths -
diff --git a/hypothesis-python/src/hypothesis/internal/reflection.py b/hypothesis-python/src/hypothesis/internal/reflection.py
index ed4975972..1c1419c60 100644
--- a/hypothesis-python/src/hypothesis/internal/reflection.py
+++ b/hypothesis-python/src/hypothesis/internal/reflection.py
@@ -64,10 +64,6 @@ def function_digest(function):
         hasher.update(function.__name__.encode())
     except AttributeError:
         pass
-    try:
-        hasher.update(function.__module__.__name__.encode())
-    except AttributeError:
-        pass
     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
@@ -124,7 +120,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):
+    if sys.version_info[:2] <= (3, 8) and inspect.isclass(target):  # pragma: no cover
         # Workaround for subclasses of typing.Generic on Python <= 3.8
         from hypothesis.strategies._internal.types import is_generic_type
 
@@ -276,6 +272,8 @@ def extract_lambda_source(f):
     source = LINE_CONTINUATION.sub(" ", source)
     source = WHITESPACE.sub(" ", source)
     source = source.strip()
+    if "lambda" not in source and sys.platform == "emscripten":  # pragma: no cover
+        return if_confused  # work around Pyodide bug in inspect.getsource()
     assert "lambda" in source
 
     tree = None
diff --git a/hypothesis-python/src/hypothesis/internal/scrutineer.py b/hypothesis-python/src/hypothesis/internal/scrutineer.py
index 0a6350869..6c5075114 100644
--- a/hypothesis-python/src/hypothesis/internal/scrutineer.py
+++ b/hypothesis-python/src/hypothesis/internal/scrutineer.py
@@ -11,7 +11,7 @@
 import sys
 from collections import defaultdict
 from functools import lru_cache, reduce
-from itertools import groupby
+from os import sep
 from pathlib import Path
 
 from hypothesis._settings import Phase, Verbosity
@@ -45,6 +45,20 @@ class Tracer:
                 self._previous_location = current_location
 
 
+UNHELPFUL_LOCATIONS = (
+    # There's a branch which is only taken when an exception is active while exiting
+    # a contextmanager; this is probably after the fault has been triggered.
+    # Similar reasoning applies to a few other standard-library modules: even
+    # if the fault was later, these still aren't useful locations to report!
+    f"{sep}contextlib.py",
+    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}_pytest{sep}assertion{sep}rewrite.py",
+)
+
+
 def get_explaining_locations(traces):
     # Traces is a dict[interesting_origin | None, set[frozenset[tuple[str, int]]]]
     # Each trace in the set might later become a Counter instead of frozenset.
@@ -84,7 +98,13 @@ def get_explaining_locations(traces):
             else:
                 queue.update(cf_graphs[origin][src] - seen)
 
-    return explanations
+    # The last step is to filter out explanations that we know would be uninformative.
+    # When this is the first AFNP location, we conclude that Scrutineer missed the
+    # real divergence (earlier in the trace) and drop that unhelpful explanation.
+    return {
+        origin: {loc for loc in afnp_locs if not loc[0].endswith(UNHELPFUL_LOCATIONS)}
+        for origin, afnp_locs in explanations.items()
+    }
 
 
 LIB_DIR = str(Path(sys.executable).parent / "lib")
@@ -92,31 +112,25 @@ EXPLANATION_STUB = (
     "Explanation:",
     "    These lines were always and only run by failing examples:",
 )
-HAD_TRACE = "    We didn't try to explain this, because sys.gettrace()="
 
 
 def make_report(explanations, cap_lines_at=5):
     report = defaultdict(list)
     for origin, locations in explanations.items():
-        assert locations  # or else we wouldn't have stored the key, above.
-        report_lines = [
-            "        {}:{}".format(k, ", ".join(map(str, sorted(l for _, l in v))))
-            for k, v in groupby(locations, lambda kv: kv[0])
-        ]
+        report_lines = [f"        {fname}:{lineno}" for fname, lineno in locations]
         report_lines.sort(key=lambda line: (line.startswith(LIB_DIR), line))
         if len(report_lines) > cap_lines_at + 1:
             msg = "        (and {} more with settings.verbosity >= verbose)"
             report_lines[cap_lines_at:] = [msg.format(len(report_lines[cap_lines_at:]))]
-        report[origin] = list(EXPLANATION_STUB) + report_lines
+        if report_lines:  # We might have filtered out every location as uninformative.
+            report[origin] = list(EXPLANATION_STUB) + report_lines
     return report
 
 
 def explanatory_lines(traces, settings):
     if Phase.explain in settings.phases and sys.gettrace() and not traces:
-        return defaultdict(
-            lambda: [EXPLANATION_STUB[0], HAD_TRACE + repr(sys.gettrace())]
-        )
+        return defaultdict(list)
     # Return human-readable report lines summarising the traces
     explanations = get_explaining_locations(traces)
-    max_lines = 5 if settings.verbosity <= Verbosity.normal else 100
+    max_lines = 5 if settings.verbosity <= Verbosity.normal else float("inf")
     return make_report(explanations, cap_lines_at=max_lines)
diff --git a/hypothesis-python/src/hypothesis/provisional.py b/hypothesis-python/src/hypothesis/provisional.py
index 914391ae4..d3a699530 100644
--- a/hypothesis-python/src/hypothesis/provisional.py
+++ b/hypothesis-python/src/hypothesis/provisional.py
@@ -35,7 +35,7 @@ FRAGMENT_SAFE_CHARACTERS = URL_SAFE_CHARACTERS | {"?", "/"}
 try:  # pragma: no cover
     traversable = resources.files("hypothesis.vendor") / "tlds-alpha-by-domain.txt"
     _tlds = traversable.read_text().splitlines()
-except (AttributeError, ValueError):  # .files() was added in Python 3.9
+except (AttributeError, ValueError):  # pragma: no cover  # .files() was added in 3.9
     _tlds = resources.read_text(
         "hypothesis.vendor", "tlds-alpha-by-domain.txt"
     ).splitlines()
diff --git a/hypothesis-python/src/hypothesis/stateful.py b/hypothesis-python/src/hypothesis/stateful.py
index c81b66a90..1c66d88ee 100644
--- a/hypothesis-python/src/hypothesis/stateful.py
+++ b/hypothesis-python/src/hypothesis/stateful.py
@@ -249,7 +249,7 @@ class RuleBasedStateMachine(metaclass=StateMachineMeta):
     _invariants_per_class: Dict[type, List[classmethod]] = {}
     _initializers_per_class: Dict[type, List[classmethod]] = {}
 
-    def __init__(self):
+    def __init__(self) -> None:
         if not self.rules():
             raise InvalidDefinition(f"Type {type(self).__name__} defines no rules")
         self.bundles: Dict[str, list] = {}
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/core.py b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
index 3c0eadc7b..575c1c70a 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
@@ -18,10 +18,11 @@ import sys
 import typing
 from decimal import Context, Decimal, localcontext
 from fractions import Fraction
-from functools import reduce
+from functools import lru_cache, reduce
 from inspect import Parameter, Signature, isabstract, isclass, signature
 from types import FunctionType
 from typing import (
+    TYPE_CHECKING,
     Any,
     AnyStr,
     Callable,
@@ -49,7 +50,14 @@ from hypothesis.control import cleanup, note
 from hypothesis.errors import InvalidArgument, ResolutionFailed
 from hypothesis.internal.cathetus import cathetus
 from hypothesis.internal.charmap import as_general_categories
-from hypothesis.internal.compat import ceil, floor, get_type_hints, is_typed_named_tuple
+from hypothesis.internal.compat import (
+    Concatenate,
+    ParamSpec,
+    ceil,
+    floor,
+    get_type_hints,
+    is_typed_named_tuple,
+)
 from hypothesis.internal.conjecture.utils import (
     calc_label_from_cls,
     check_sample,
@@ -85,7 +93,7 @@ from hypothesis.strategies._internal.collections import (
 )
 from hypothesis.strategies._internal.deferred import DeferredStrategy
 from hypothesis.strategies._internal.functions import FunctionStrategy
-from hypothesis.strategies._internal.lazy import LazyStrategy
+from hypothesis.strategies._internal.lazy import LazyStrategy, unwrap_strategies
 from hypothesis.strategies._internal.misc import just, none, nothing
 from hypothesis.strategies._internal.numbers import (
     IntegersStrategy,
@@ -107,28 +115,22 @@ from hypothesis.strategies._internal.strings import (
     TextStrategy,
 )
 from hypothesis.strategies._internal.utils import cacheable, defines_strategy
-from hypothesis.utils.conventions import infer, not_set
+from hypothesis.utils.conventions import not_set
 
-if sys.version_info >= (3, 10):  # pragma: no cover
-    from types import EllipsisType as InferType
+if sys.version_info >= (3, 10):
+    from types import EllipsisType as EllipsisType
 elif typing.TYPE_CHECKING:  # pragma: no cover
-    from builtins import ellipsis as InferType
+    from builtins import ellipsis as EllipsisType
 else:
-    InferType = type(Ellipsis)
+    EllipsisType = type(Ellipsis)  # pragma: no cover
 
 
-try:
+if sys.version_info >= (3, 8):  # pragma: no cover
     from typing import Protocol
-except ImportError:  # < py3.8
-    Protocol = object  # type: ignore[assignment]
-
-try:
-    from typing import Concatenate, ParamSpec
-except ImportError:
-    try:
-        from typing_extensions import Concatenate, ParamSpec
-    except ImportError:
-        ParamSpec = None  # type: ignore
+elif TYPE_CHECKING:
+    from typing_extensions import Protocol
+else:  # pragma: no cover
+    Protocol = object
 
 
 @cacheable
@@ -616,6 +618,19 @@ def characters(
     )
 
 
+# Cache size is limited by sys.maxunicode, but passing None makes it slightly faster.
+@lru_cache(maxsize=None)
+def _check_is_single_character(c):
+    # In order to mitigate the performance cost of this check, we use a shared cache,
+    # even at the cost of showing the culprit strategy in the error message.
+    if not isinstance(c, str):
+        type_ = get_pretty_function_description(type(c))
+        raise InvalidArgument(f"Got non-string {c!r} (type {type_})")
+    if len(c) != 1:
+        raise InvalidArgument(f"Got {c!r} (length {len(c)} != 1)")
+    return c
+
+
 @cacheable
 @defines_strategy(force_reusable_values=True)
 def text(
@@ -646,7 +661,13 @@ def text(
     """
     check_valid_sizes(min_size, max_size)
     if isinstance(alphabet, SearchStrategy):
-        char_strategy = alphabet
+        char_strategy = unwrap_strategies(alphabet)
+        if isinstance(char_strategy, SampledFromStrategy):
+            # Check this via the up-front validation logic below, and incidentally
+            # convert into a `characters()` strategy for standard text shrinking.
+            return text(char_strategy.elements, min_size=min_size, max_size=max_size)
+        elif not isinstance(char_strategy, OneCharStringStrategy):
+            char_strategy = char_strategy.map(_check_is_single_character)
     else:
         non_string = [c for c in alphabet if not isinstance(c, str)]
         if non_string:
@@ -864,7 +885,7 @@ class BuildsStrategy(SearchStrategy):
 @defines_strategy()
 def builds(
     *callable_and_args: Union[Callable[..., Ex], SearchStrategy[Any]],
-    **kwargs: Union[SearchStrategy[Any], InferType],
+    **kwargs: Union[SearchStrategy[Any], EllipsisType],
 ) -> SearchStrategy[Ex]:
     """Generates values by drawing from ``args`` and ``kwargs`` and passing
     them to the callable (provided as the first positional argument) in the
@@ -899,14 +920,14 @@ def builds(
             "target to construct."
         )
 
-    if infer in args:  # type: ignore  # we only annotated the allowed types
+    if ... in args:  # type: ignore  # we only annotated the allowed types
         # Avoid an implementation nightmare juggling tuples and worse things
         raise InvalidArgument(
             "... was passed as a positional argument to "
             "builds(), but is only allowed as a keyword arg"
         )
     required = required_args(target, args, kwargs)
-    to_infer = {k for k, v in kwargs.items() if v is infer}
+    to_infer = {k for k, v in kwargs.items() if v is ...}
     if required or to_infer:
         if isinstance(target, type) and attr.has(target):
             # Use our custom introspection for attrs classes
@@ -1095,7 +1116,7 @@ def _from_type(thing: Type[Ex]) -> SearchStrategy[Ex]:
         pass
     if (
         hasattr(typing, "_TypedDictMeta")
-        and type(thing) is typing._TypedDictMeta  # type: ignore
+        and type(thing) is typing._TypedDictMeta
         or hasattr(types.typing_extensions, "_TypedDictMeta")  # type: ignore
         and type(thing) is types.typing_extensions._TypedDictMeta  # type: ignore
     ):  # pragma: no cover
@@ -1498,6 +1519,7 @@ class DrawFn(Protocol):
         def list_and_index(draw: DrawFn) -> Tuple[int, str]:
             i = draw(integers())  # type inferred as 'int'
             s = draw(text())  # type inferred as 'str'
+            return i, s
 
     """
 
@@ -1994,7 +2016,7 @@ def _functions(*, like, returns, pure):
             "The first argument to functions() must be a callable to imitate, "
             f"but got non-callable like={nicerepr(like)!r}"
         )
-    if returns is None or returns is infer:
+    if returns in (None, ...):
         # Passing `None` has never been *documented* as working, but it still
         # did from May 2020 to Jan 2022 so we'll avoid breaking it without cause.
         hints = get_type_hints(like)
@@ -2037,7 +2059,7 @@ if typing.TYPE_CHECKING or ParamSpec is not None:
         ...
 
     @defines_strategy()
-    def functions(*, like=lambda: None, returns=infer, pure=False):
+    def functions(*, like=lambda: None, returns=..., pure=False):
         # We shouldn't need overloads here, but mypy disallows default args for
         # generics: https://github.com/python/mypy/issues/3737
         """functions(*, like=lambda: None, returns=..., pure=False)
@@ -2068,7 +2090,7 @@ else:  # pragma: no cover
     def functions(
         *,
         like: Callable[..., Any] = lambda: None,
-        returns: Union[SearchStrategy[Any], InferType] = infer,
+        returns: Union[SearchStrategy[Any], EllipsisType] = ...,
         pure: bool = False,
     ) -> SearchStrategy[Callable[..., Any]]:
         """functions(*, like=lambda: None, returns=..., pure=False)
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py b/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
index 224a3cc88..380c8732c 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/numbers.py
@@ -450,13 +450,31 @@ def floats(
         # Erroring out here ensures that the database contents are interpreted
         # consistently - which matters for such a foundational strategy, even if it's
         # not always true for all user-composed strategies further up the stack.
+        from _hypothesis_ftz_detector import identify_ftz_culprits
+
+        try:
+            ftz_pkg = identify_ftz_culprits()
+        except Exception:
+            ftz_pkg = None
+        if ftz_pkg:
+            ftz_msg = (
+                f"This seems to be because the `{ftz_pkg}` package was compiled with "
+                f"-ffast-math or a similar option, which sets global processor state "
+                f"- see https://simonbyrne.github.io/notes/fastmath/ for details.  "
+                f"If you don't know why {ftz_pkg} is installed, `pipdeptree -rp "
+                f"{ftz_pkg}` will show which packages depend on it."
+            )
+        else:
+            ftz_msg = (
+                "This is usually because something was compiled with -ffast-math "
+                "or a similar option, which sets global processor state.  See "
+                "https://simonbyrne.github.io/notes/fastmath/ for a more detailed "
+                "writeup - and good luck!"
+            )
         raise FloatingPointError(
             f"Got allow_subnormal={allow_subnormal!r}, but we can't represent "
             f"subnormal floats right now, in violation of the IEEE-754 floating-point "
-            f"specification.  This is usually because something was compiled with "
-            f"-ffast-math or a similar option, which sets global processor state.  "
-            f"See https://simonbyrne.github.io/notes/fastmath/ for a more detailed "
-            f"writeup - and good luck!"
+            f"specification.  {ftz_msg}"
         )
 
     min_arg, max_arg = min_value, max_value
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py
index faae2b084..edc719113 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/strategies.py
@@ -29,6 +29,7 @@ from hypothesis._settings import HealthCheck, Phase, Verbosity, settings
 from hypothesis.control import _current_build_context, assume
 from hypothesis.errors import (
     HypothesisException,
+    HypothesisWarning,
     InvalidArgument,
     NonInteractiveExampleWarning,
     UnsatisfiedAssumption,
@@ -396,6 +397,14 @@ class SearchStrategy(Generic[Ex]):
             raise ValueError(f"Cannot | a SearchStrategy with {other!r}")
         return OneOfStrategy((self, other))
 
+    def __bool__(self) -> bool:
+        warnings.warn(
+            f"bool({self!r}) is always True, did you mean to draw a value?",
+            HypothesisWarning,
+            stacklevel=2,
+        )
+        return True
+
     def validate(self) -> None:
         """Throw an exception if the strategy is not valid.
 
@@ -589,7 +598,7 @@ class SampledFromStrategy(SearchStrategy):
         return filter_not_satisfied
 
 
-class OneOfStrategy(SearchStrategy):
+class OneOfStrategy(SearchStrategy[Ex]):
     """Implements a union of strategies. Given a number of strategies this
     generates values which could have come from any of them.
 
@@ -781,7 +790,7 @@ def one_of(
     return OneOfStrategy(args)
 
 
-class MappedSearchStrategy(SearchStrategy):
+class MappedSearchStrategy(SearchStrategy[Ex]):
     """A strategy which is defined purely by conversion to and from another
     strategy.
 
@@ -816,7 +825,7 @@ class MappedSearchStrategy(SearchStrategy):
         into a value suitable for outputting from this strategy."""
         raise NotImplementedError(f"{self.__class__.__name__}.pack()")
 
-    def do_draw(self, data: ConjectureData) -> Ex:
+    def do_draw(self, data: ConjectureData) -> Any:
         with warnings.catch_warnings():
             if isinstance(self.pack, type) and issubclass(
                 self.pack, (abc.Mapping, abc.Set)
@@ -826,7 +835,7 @@ class MappedSearchStrategy(SearchStrategy):
                 i = data.index
                 try:
                     data.start_example(MAPPED_SEARCH_STRATEGY_DO_DRAW_LABEL)
-                    result = self.pack(data.draw(self.mapped_strategy))
+                    result = self.pack(data.draw(self.mapped_strategy))  # type: ignore
                     data.stop_example()
                     return result
                 except UnsatisfiedAssumption:
@@ -846,7 +855,7 @@ class MappedSearchStrategy(SearchStrategy):
 filter_not_satisfied = UniqueIdentifier("filter not satisfied")
 
 
-class FilteredStrategy(SearchStrategy):
+class FilteredStrategy(SearchStrategy[Ex]):
     def __init__(self, strategy, conditions):
         super().__init__()
         if isinstance(strategy, FilteredStrategy):
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/strings.py b/hypothesis-python/src/hypothesis/strategies/_internal/strings.py
index 2e72a9c1e..e1dc1f4da 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/strings.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/strings.py
@@ -121,11 +121,14 @@ class TextStrategy(ListStrategy):
     # See https://docs.python.org/3/library/stdtypes.html#string-methods
     # These methods always return Truthy values for any nonempty string.
     _nonempty_filters = ListStrategy._nonempty_filters + (
+        str,
         str.capitalize,
         str.casefold,
+        str.encode,
         str.expandtabs,
         str.join,
         str.lower,
+        str.rsplit,
         str.split,
         str.splitlines,
         str.swapcase,
@@ -144,6 +147,9 @@ class TextStrategy(ListStrategy):
         str.isnumeric,
         str.isspace,
         str.istitle,
+        str.lstrip,
+        str.rstrip,
+        str.strip,
     )
 
     def filter(self, condition):
@@ -156,9 +162,6 @@ class TextStrategy(ListStrategy):
         # We use ListStrategy filter logic for the conditions that *only* imply
         # the string is nonempty.  Here, we increment the min_size but still apply
         # the filter for conditions that imply nonempty *and specific contents*.
-        #
-        # TODO: we may eventually rewrite the elements_strategy for some of these,
-        #       avoiding rejection sampling and making them much more efficient.
         if condition in self._nonempty_and_content_filters:
             assert self.max_size >= 1, "Always-empty is special cased in st.text()"
             self = copy.copy(self)
diff --git a/hypothesis-python/src/hypothesis/strategies/_internal/types.py b/hypothesis-python/src/hypothesis/strategies/_internal/types.py
index 4e410bcf5..ddd73dd31 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/types.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/types.py
@@ -64,7 +64,7 @@ except ImportError:
 TypeAliasTypes: tuple = ()
 try:
     TypeAliasTypes += (typing.TypeAlias,)
-except AttributeError:
+except AttributeError:  # pragma: no cover
     pass  # Is missing for `python<3.10`
 try:
     TypeAliasTypes += (typing_extensions.TypeAlias,)
@@ -223,7 +223,7 @@ def try_issubclass(thing, superclass):
             # and Hypothesis issue #2951 closely first, and good luck.  The tests
             # will help you, I hope - good luck.
             if getattr(thing, "__args__", None) is not None:
-                return True
+                return True  # pragma: no cover  # only possible on Python <= 3.9
             for orig_base in getattr(thing, "__orig_bases__", None) or [None]:
                 args = getattr(orig_base, "__args__", None)
                 if _compatible_args(args, superclass_args):
@@ -238,7 +238,7 @@ def is_a_new_type(thing):
     if not isinstance(typing.NewType, type):
         # At runtime, `typing.NewType` returns an identity function rather
         # than an actual type, but we can check whether that thing matches.
-        return (
+        return (  # pragma: no cover  # Python <= 3.9 only
             hasattr(thing, "__supertype__")
             and getattr(thing, "__module__", None) in ("typing", "typing_extensions")
             and inspect.isfunction(thing)
@@ -361,7 +361,8 @@ def from_typing_type(thing):
         literals = []
         while args_dfs_stack:
             arg = args_dfs_stack.pop()
-            if is_typing_literal(arg):
+            if is_typing_literal(arg):  # pragma: no cover
+                # Python 3.10+ flattens for us when constructing Literal objects
                 args_dfs_stack.extend(reversed(arg.__args__))
             else:
                 literals.append(arg)
@@ -813,7 +814,9 @@ def resolve_OrderedDict(thing):
 
 @register(typing.Pattern, st.builds(re.compile, st.sampled_from(["", b""])))
 def resolve_Pattern(thing):
-    if isinstance(thing.__args__[0], typing.TypeVar):
+    if isinstance(thing.__args__[0], typing.TypeVar):  # pragma: no cover
+        # TODO: this was covered on Python 3.8, but isn't on 3.10 - we should
+        # work out why not and write some extra tests to help avoid regressions.
         return st.builds(re.compile, st.sampled_from(["", b""]))
     return st.just(re.compile(thing.__args__[0]()))
 
@@ -868,7 +871,7 @@ def resolve_Callable(thing):
     # Note that a list can only appear in __args__ under Python 3.9 with the
     # collections.abc version; see https://bugs.python.org/issue42195
     if len(args_types) == 1 and isinstance(args_types[0], list):
-        args_types = tuple(args_types[0])
+        args_types = tuple(args_types[0])  # pragma: no cover
 
     pep612 = ConcatenateTypes + ParamSpecTypes
     for arg in args_types:
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 6c6ae4f8c..cbff00706 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 2022040900, Last Updated Sat Apr  9 07:07:02 2022 UTC
+# Version 2022112700, Last Updated Sun Nov 27 07:07:01 2022 UTC
 AAA
 AARP
 ABARTH
@@ -17,7 +17,6 @@ ACCOUNTANTS
 ACO
 ACTOR
 AD
-ADAC
 ADS
 ADULT
 AE
@@ -176,7 +175,6 @@ BROTHER
 BRUSSELS
 BS
 BT
-BUGATTI
 BUILD
 BUILDERS
 BUSINESS
@@ -196,7 +194,6 @@ CALVINKLEIN
 CAM
 CAMERA
 CAMP
-CANCERRESEARCH
 CANON
 CAPETOWN
 CAPITAL
diff --git a/hypothesis-python/src/hypothesis/version.py b/hypothesis-python/src/hypothesis/version.py
index 90f70c324..a48a54199 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, 54, 0)
+__version_info__ = (6, 59, 0)
 __version__ = ".".join(map(str, __version_info__))
diff --git a/hypothesis-python/tox.ini b/hypothesis-python/tox.ini
index c68badc32..fc86194b3 100644
--- a/hypothesis-python/tox.ini
+++ b/hypothesis-python/tox.ini
@@ -1,5 +1,5 @@
 [tox]
-envlist = py{37,py37,38,py38,39,py39,310,311,312}-{brief,prettyquick,full,custom}
+envlist = py{37,py37,38,py38,39,py39,310,311,312}-{brief,full,cover,nocover,niche,custom}
 toxworkdir={env:TOX_WORK_DIR:.tox}
 
 [testenv]
@@ -15,12 +15,13 @@ passenv=
     # Allow CI builds (or user builds) to force coloured terminal output.
     PY_COLORS
 setenv=
-    PYTHONDEVMODE=1
     brief: HYPOTHESIS_PROFILE=speedy
 commands =
     full: bash scripts/basic-test.sh
     brief: python -bb -X dev -m pytest tests/cover/test_testdecorators.py {posargs}
-    prettyquick: python -bb -X dev -m pytest tests/cover/
+    cover: python -bb -X dev -m pytest tests/cover/ tests/pytest/ {posargs}
+    nocover: python -bb -X dev -m pytest tests/nocover/ {posargs}
+    niche: bash scripts/other-tests.sh
     custom: python -bb -X dev -m pytest {posargs}
 
 [testenv:py310-pyjion]
@@ -43,8 +44,8 @@ commands=
 
 # Note: when adding or removing tested Pandas versions, make sure to update the
 # docs in numpy.rst too.  To see current download rates of each minor version:
-# https://pepy.tech/project/pandas?versions=0.25.*&versions=1.0.*&versions=1.1.*&versions=1.2.*&versions=1.3.*&versions=1.4.*
-[testenv:pandas10]
+# https://pepy.tech/project/pandas?versions=1.0.*&versions=1.1.*&versions=1.2.*&versions=1.3.*&versions=1.4.*&versions=1.5.*
+[testenv:py39-pandas10]
 deps =
     -r../requirements/test.txt
     pandas~=1.0.5
@@ -78,39 +79,45 @@ deps =
     pandas~=1.4.3
 commands =
     python -bb -X dev -m pytest tests/pandas -n auto
-# Adding a new pandas?  See comment above!
 
-[testenv:django22]
+[testenv:pandas15]
+deps =
+    -r../requirements/test.txt
+    pandas~=1.5.0
 commands =
-    pip install .[pytz]
-    pip install django~=2.2.25
-    python -bb -X dev -m tests.django.manage test tests.django
+    python -bb -X dev -m pytest tests/pandas -n auto
+# Adding a new pandas?  See comment above!
 
 [testenv:django32]
 commands =
     pip install .[pytz]
-    pip install django~=3.2.10
+    pip install django~=3.2.15
     python -bb -X dev -m tests.django.manage test tests.django
 
 [testenv:django40]
 commands =
-    pip install django~=4.0.0
+    pip install django~=4.0.7
+    python -bb -X dev -m tests.django.manage test tests.django
+
+[testenv:django41]
+commands =
+    pip install django~=4.1.0
     python -bb -X dev -m tests.django.manage test tests.django
 
-[testenv:nose]
+[testenv:py{37,38,39}-nose]
 deps =
     nose
 commands=
     nosetests tests/cover/test_testdecorators.py
 
-[testenv:pytest46]
+[testenv:py{37,38,39}-pytest46]
 deps =
     -r../requirements/test.txt
 commands=
     pip install pytest==4.6.11 pytest-xdist==1.34
-    python -bb -X dev -m pytest tests/pytest tests/cover/test_testdecorators.py
+    python -bb -X dev -W'ignore:the imp module:DeprecationWarning' -m pytest tests/pytest tests/cover/test_testdecorators.py
 
-[testenv:pytest54]
+[testenv:py{37,38,39}-pytest54]
 deps =
     -r../requirements/test.txt
 commands=
@@ -150,7 +157,7 @@ commands_pre =
 # (The tox task will still count as failed.)
 ignore_errors = true
 commands =
-    python -bb -X dev -m coverage run --rcfile=.coveragerc --source=hypothesis -m pytest -n0 --strict-markers --ff {posargs} \
+    python -bb -X dev -m coverage run --rcfile=.coveragerc --source=hypothesis -m pytest -n0 --ff {posargs} \
         tests/cover tests/conjecture tests/datetime tests/numpy tests/pandas tests/lark tests/redis tests/dpcontracts tests/codemods tests/typing_extensions
     python -m coverage report -m --fail-under=100 --show-missing --skip-covered
     python scripts/validate_branch_check.py
diff --git a/pytest.ini b/pytest.ini
index eaf26d582..25fbfae21 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,7 +1,13 @@
 [pytest]
 
 # -rfEX :: Print a summary of failures, errors, and xpasses (xfails that pass).
-addopts=--strict-markers --tb=native -p pytester --runpytest=subprocess --durations=20 -rfEX
+addopts =
+    -rfEX
+    --strict-markers
+    --tb=native
+    -p pytester
+    --runpytest=subprocess
+    --durations=20
 xfail_strict = True
 filterwarnings =
     error
@@ -11,9 +17,6 @@ filterwarnings =
     default:distutils Version classes are deprecated\. Use packaging\.version instead:DeprecationWarning
     # https://github.com/pandas-dev/pandas/issues/32056 (?)
     default:numpy\.ufunc size changed, may indicate binary incompatibility\. Expected 216 from C header, got 232 from PyObject:RuntimeWarning
-    # https://github.com/lark-parser/lark/pull/1140
-    default:module 'sre_constants' is deprecated:DeprecationWarning
-    default:module 'sre_parse' is deprecated:DeprecationWarning
     # https://github.com/pandas-dev/pandas/issues/34848
     default:`np\.bool` is a deprecated alias for the builtin `bool`:DeprecationWarning
     default:`np\.complex` is a deprecated alias for the builtin `complex`:DeprecationWarning
diff --git a/requirements/coverage.in b/requirements/coverage.in
index c196eba12..353822329 100644
--- a/requirements/coverage.in
+++ b/requirements/coverage.in
@@ -1,10 +1,9 @@
-backports.zoneinfo
 black
 click
 coverage
 dpcontracts
 fakeredis
-lark-parser
+lark
 libcst
 numpy
 pandas
diff --git a/requirements/coverage.txt b/requirements/coverage.txt
index b5937b3ad..97bd5958a 100644
--- a/requirements/coverage.txt
+++ b/requirements/coverage.txt
@@ -1,8 +1,8 @@
 #
-# This file is autogenerated by pip-compile with python 3.8
+# This file is autogenerated by pip-compile with python 3.10
 # To update, run:
 #
-#    pip-compile --output-file=requirements/coverage.txt hypothesis-python/setup.py requirements/coverage.in
+#    ./build.sh upgrade-requirements
 #
 async-timeout==4.0.2
     # via redis
@@ -10,37 +10,35 @@ attrs==22.1.0
     # via
     #   hypothesis (hypothesis-python/setup.py)
     #   pytest
-backports-zoneinfo==0.2.1
-    # via -r requirements/coverage.in
-black==22.6.0
+black==22.10.0
     # via -r requirements/coverage.in
 click==8.1.3
     # via
     #   -r requirements/coverage.in
     #   black
-coverage==6.4.2
+coverage==6.5.0
     # via -r requirements/coverage.in
-deprecated==1.2.13
-    # via redis
 dpcontracts==0.6.0
     # via -r requirements/coverage.in
-exceptiongroup==1.0.0rc8 ; python_version < "3.11"
-    # via hypothesis (hypothesis-python/setup.py)
+exceptiongroup==1.0.4 ; python_version < "3.11"
+    # via
+    #   hypothesis (hypothesis-python/setup.py)
+    #   pytest
 execnet==1.9.0
     # via pytest-xdist
-fakeredis==1.8.2
+fakeredis==2.1.0
     # via -r requirements/coverage.in
 iniconfig==1.1.1
     # via pytest
-lark-parser==0.12.0
+lark==1.1.4
     # via -r requirements/coverage.in
-libcst==0.4.7
+libcst==0.4.9
     # via -r requirements/coverage.in
 mypy-extensions==0.4.3
     # via
     #   black
     #   typing-inspect
-numpy==1.23.1
+numpy==1.23.5
     # via
     #   -r requirements/coverage.in
     #   pandas
@@ -48,49 +46,40 @@ packaging==21.3
     # via
     #   pytest
     #   redis
-pandas==1.4.3
+pandas==1.5.2
     # via -r requirements/coverage.in
-pathspec==0.9.0
+pathspec==0.10.2
     # via black
 pexpect==4.8.0
     # via -r requirements/test.in
-platformdirs==2.5.2
+platformdirs==2.5.4
     # via black
 pluggy==1.0.0
     # via pytest
 ptyprocess==0.7.0
     # via pexpect
-py==1.11.0
-    # via
-    #   pytest
-    #   pytest-forked
 pyparsing==3.0.9
     # via packaging
-pytest==7.1.2
+pytest==7.2.0
     # via
     #   -r requirements/test.in
-    #   pytest-forked
     #   pytest-xdist
-pytest-forked==1.4.0
-    # via pytest-xdist
-pytest-xdist==2.5.0
+pytest-xdist==3.1.0
     # via -r requirements/test.in
 python-dateutil==2.8.2
     # via
     #   -r requirements/coverage.in
     #   pandas
-pytz==2022.1
+pytz==2022.6
     # via
     #   -r requirements/coverage.in
     #   pandas
 pyyaml==6.0
     # via libcst
-redis==4.3.4
+redis==4.3.5
     # via fakeredis
 six==1.16.0
-    # via
-    #   fakeredis
-    #   python-dateutil
+    # via python-dateutil
 sortedcontainers==2.4.0
     # via
     #   fakeredis
@@ -99,13 +88,10 @@ tomli==2.0.1
     # via
     #   black
     #   pytest
-typing-extensions==4.3.0
+typing-extensions==4.4.0
     # via
     #   -r requirements/coverage.in
-    #   black
     #   libcst
     #   typing-inspect
-typing-inspect==0.7.1
+typing-inspect==0.8.0
     # via libcst
-wrapt==1.14.1
-    # via deprecated
diff --git a/requirements/test.txt b/requirements/test.txt
index 64cb7c331..38e3e5f60 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -1,15 +1,17 @@
 #
-# This file is autogenerated by pip-compile with python 3.8
+# This file is autogenerated by pip-compile with python 3.10
 # To update, run:
 #
-#    pip-compile --output-file=requirements/test.txt hypothesis-python/setup.py requirements/test.in
+#    ./build.sh upgrade-requirements
 #
 attrs==22.1.0
     # via
     #   hypothesis (hypothesis-python/setup.py)
     #   pytest
-exceptiongroup==1.0.0rc8 ; python_version < "3.11"
-    # via hypothesis (hypothesis-python/setup.py)
+exceptiongroup==1.0.4 ; python_version < "3.11"
+    # via
+    #   hypothesis (hypothesis-python/setup.py)
+    #   pytest
 execnet==1.9.0
     # via pytest-xdist
 iniconfig==1.1.1
@@ -22,20 +24,13 @@ pluggy==1.0.0
     # via pytest
 ptyprocess==0.7.0
     # via pexpect
-py==1.11.0
-    # via
-    #   pytest
-    #   pytest-forked
 pyparsing==3.0.9
     # via packaging
-pytest==7.1.2
+pytest==7.2.0
     # via
     #   -r requirements/test.in
-    #   pytest-forked
     #   pytest-xdist
-pytest-forked==1.4.0
-    # via pytest-xdist
-pytest-xdist==2.5.0
+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.in b/requirements/tools.in
index 83d9fd2ee..7dc9d8370 100644
--- a/requirements/tools.in
+++ b/requirements/tools.in
@@ -16,9 +16,9 @@ flake8-pie
 flake8-pytest-style
 flake8-return
 flake8-simplify
-flake8-strftime
+# flake8-strftime  # See https://github.com/python-formate/flake8_strftime/issues/47
 ipython
-lark-parser
+lark
 libcst
 mypy
 pip-tools
@@ -33,7 +33,6 @@ sphinx-codeautolink
 sphinx-hoverxref
 sphinx-rtd-theme
 sphinx-selective-exclude
-toml
 tox
 twine
 types-click
diff --git a/requirements/tools.txt b/requirements/tools.txt
index 2d9d6f146..f7d8554e9 100644
--- a/requirements/tools.txt
+++ b/requirements/tools.txt
@@ -1,8 +1,8 @@
 #
-# This file is autogenerated by pip-compile with python 3.8
+# This file is autogenerated by pip-compile with python 3.10
 # To update, run:
 #
-#    pip-compile --output-file=requirements/tools.txt hypothesis-python/setup.py requirements/tools.in
+#    ./build.sh upgrade-requirements
 #
 alabaster==0.7.12
     # via sphinx
@@ -10,56 +10,54 @@ asgiref==3.5.2
     # via django
 astor==0.8.1
     # via flake8-simplify
-asttokens==2.0.5
+asttokens==2.2.0
     # via stack-data
 attrs==22.1.0
     # via
     #   flake8-bugbear
     #   hypothesis (hypothesis-python/setup.py)
     #   pytest
-autoflake==1.4
+autoflake==2.0.0
     # via shed
-babel==2.10.3
+babel==2.11.0
     # via sphinx
 backcall==0.2.0
     # via ipython
-backports-zoneinfo==0.2.1
-    # via django
 bandit==1.7.4
     # via flake8-bandit
 beautifulsoup4==4.11.1
     # via sphinx-codeautolink
-black==22.6.0
+black==22.10.0
     # via shed
 bleach==5.0.1
     # via readme-renderer
-build==0.8.0
+build==0.9.0
     # via pip-tools
-certifi==2022.6.15
+certifi==2022.9.24
     # via requests
 cffi==1.15.1
     # via cryptography
-charset-normalizer==2.1.0
+charset-normalizer==2.1.1
     # via requests
 click==8.1.3
     # via
     #   black
     #   pip-tools
-codespell==2.1.0
+codespell==2.2.2
     # via -r requirements/tools.in
 com2ann==0.3.0
     # via shed
 commonmark==0.9.1
     # via rich
-coverage==6.4.2
+coverage==6.5.0
     # via -r requirements/tools.in
-cryptography==37.0.4
+cryptography==38.0.4
     # via secretstorage
 decorator==5.1.1
     # via ipython
-distlib==0.3.5
+distlib==0.3.6
     # via virtualenv
-django==4.0.6
+django==4.1.3
     # via -r requirements/tools.in
 docutils==0.17.1
     # via
@@ -69,15 +67,17 @@ docutils==0.17.1
     #   sphinx-rtd-theme
 dpcontracts==0.6.0
     # via -r requirements/tools.in
-exceptiongroup==1.0.0rc8 ; python_version < "3.11"
-    # via hypothesis (hypothesis-python/setup.py)
-executing==0.9.1
+exceptiongroup==1.0.4 ; python_version < "3.11"
+    # via
+    #   hypothesis (hypothesis-python/setup.py)
+    #   pytest
+executing==1.2.0
     # via stack-data
-filelock==3.7.1
+filelock==3.8.0
     # via
     #   tox
     #   virtualenv
-flake8==4.0.1
+flake8==6.0.0
     # via
     #   -r requirements/tools.in
     #   flake8-2020
@@ -87,68 +87,60 @@ flake8==4.0.1
     #   flake8-comprehensions
     #   flake8-datetimez
     #   flake8-docstrings
-    #   flake8-helper
     #   flake8-mutable
     #   flake8-noqa
-    #   flake8-polyfill
     #   flake8-simplify
-    #   flake8-strftime
-flake8-2020==1.6.1
+flake8-2020==1.7.0
     # via -r requirements/tools.in
-flake8-bandit==3.0.0
+flake8-bandit==4.1.1
     # via -r requirements/tools.in
-flake8-bugbear==22.7.1
+flake8-bugbear==22.10.27
     # via -r requirements/tools.in
-flake8-builtins==1.5.3
+flake8-builtins==2.0.1
     # via -r requirements/tools.in
-flake8-comprehensions==3.10.0
+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
     # via -r requirements/tools.in
-flake8-helper==0.2.1
-    # via flake8-strftime
 flake8-mutable==1.2.0
     # via -r requirements/tools.in
-flake8-noqa==1.2.7
+flake8-noqa==1.3.0
     # via -r requirements/tools.in
-flake8-pie==0.15.0
+flake8-pie==0.16.0
     # via -r requirements/tools.in
 flake8-plugin-utils==1.3.2
     # via
     #   flake8-pytest-style
     #   flake8-return
-flake8-polyfill==1.0.2
-    # via flake8-bandit
 flake8-pytest-style==1.6.0
     # via -r requirements/tools.in
-flake8-return==1.1.3
+flake8-return==1.2.0
     # via -r requirements/tools.in
 flake8-simplify==0.19.3
     # via -r requirements/tools.in
-flake8-strftime==0.3.2
-    # via -r requirements/tools.in
-gitdb==4.0.9
+gitdb==4.0.10
     # via gitpython
-gitpython==3.1.27
+gitpython==3.1.29
     # via bandit
-idna==3.3
+idna==3.4
     # via requests
 imagesize==1.4.1
     # via sphinx
-importlib-metadata==4.12.0
+importlib-metadata==5.1.0
     # via
     #   keyring
-    #   sphinx
     #   twine
 iniconfig==1.1.1
     # via pytest
-ipython==8.4.0
+ipython==8.7.0
     # via -r requirements/tools.in
 isort==5.10.1
     # via shed
-jedi==0.18.1
+jaraco-classes==3.2.3
+    # via keyring
+jedi==0.18.2
     # via ipython
 jeepney==0.8.0
     # via
@@ -156,21 +148,23 @@ jeepney==0.8.0
     #   secretstorage
 jinja2==3.1.2
     # via sphinx
-keyring==23.7.0
+keyring==23.11.0
     # via twine
-lark-parser==0.12.0
+lark==1.1.4
     # via -r requirements/tools.in
-libcst==0.4.7
+libcst==0.4.9
     # via
     #   -r requirements/tools.in
     #   shed
 markupsafe==2.1.1
     # via jinja2
-matplotlib-inline==0.1.3
+matplotlib-inline==0.1.6
     # via ipython
-mccabe==0.6.1
+mccabe==0.7.0
     # via flake8
-mypy==0.971
+more-itertools==9.0.0
+    # via jaraco-classes
+mypy==0.991
     # via -r requirements/tools.in
 mypy-extensions==0.4.3
     # via
@@ -187,9 +181,9 @@ packaging==21.3
     #   tox
 parso==0.8.3
     # via jedi
-pathspec==0.9.0
+pathspec==0.10.2
     # via black
-pbr==5.9.0
+pbr==5.11.0
     # via stevedore
 pep517==0.13.0
     # via build
@@ -197,11 +191,11 @@ pexpect==4.8.0
     # via ipython
 pickleshare==0.7.5
     # via ipython
-pip-tools==6.8.0
+pip-tools==6.11.0
     # via -r requirements/tools.in
-pkginfo==1.8.3
+pkginfo==1.9.2
     # via twine
-platformdirs==2.5.2
+platformdirs==2.5.4
     # via
     #   black
     #   virtualenv
@@ -209,29 +203,25 @@ pluggy==1.0.0
     # via
     #   pytest
     #   tox
-prompt-toolkit==3.0.30
+prompt-toolkit==3.0.33
     # via ipython
 ptyprocess==0.7.0
     # via pexpect
 pure-eval==0.2.2
     # via stack-data
 py==1.11.0
-    # via
-    #   pytest
-    #   tox
-pycodestyle==2.8.0
-    # via
-    #   flake8
-    #   flake8-bandit
+    # via tox
+pycodestyle==2.10.0
+    # via flake8
 pycparser==2.21
     # via cffi
 pydocstyle==6.1.1
     # via flake8-docstrings
-pyflakes==2.4.0
+pyflakes==3.0.1
     # via
     #   autoflake
     #   flake8
-pygments==2.12.0
+pygments==2.13.0
     # via
     #   ipython
     #   readme-renderer
@@ -239,21 +229,21 @@ pygments==2.12.0
     #   sphinx
 pyparsing==3.0.9
     # via packaging
-pyright==1.1.264
+pyright==1.1.282
     # via -r requirements/tools.in
-pytest==7.1.2
+pytest==7.2.0
     # via -r requirements/tools.in
 python-dateutil==2.8.2
     # via -r requirements/tools.in
-pytz==2022.1
+pytz==2022.6
     # via babel
-pyupgrade==2.37.3
+pyupgrade==3.3.0
     # via shed
 pyyaml==6.0
     # via
     #   bandit
     #   libcst
-readme-renderer==35.0
+readme-renderer==37.3
     # via twine
 requests==2.28.1
     # via
@@ -261,17 +251,17 @@ requests==2.28.1
     #   requests-toolbelt
     #   sphinx
     #   twine
-requests-toolbelt==0.9.1
+requests-toolbelt==0.10.1
     # via twine
 restructuredtext-lint==1.4.0
     # via -r requirements/tools.in
 rfc3986==2.0.0
     # via twine
-rich==12.5.1
+rich==12.6.0
     # via twine
-secretstorage==3.3.2
+secretstorage==3.3.3
     # via keyring
-shed==0.10.1
+shed==0.10.7
     # via -r requirements/tools.in
 six==1.16.0
     # via
@@ -289,17 +279,17 @@ sortedcontainers==2.4.0
     # via hypothesis (hypothesis-python/setup.py)
 soupsieve==2.3.2.post1
     # via beautifulsoup4
-sphinx==5.1.1
+sphinx==5.3.0
     # via
     #   -r requirements/tools.in
     #   sphinx-codeautolink
     #   sphinx-hoverxref
     #   sphinx-rtd-theme
-sphinx-codeautolink==0.11.0
+sphinx-codeautolink==0.12.1
     # via -r requirements/tools.in
-sphinx-hoverxref==1.1.3
+sphinx-hoverxref==1.3.0
     # via -r requirements/tools.in
-sphinx-rtd-theme==1.0.0
+sphinx-rtd-theme==1.1.1
     # via -r requirements/tools.in
 sphinx-selective-exclude==1.0.3
     # via -r requirements/tools.in
@@ -309,74 +299,77 @@ sphinxcontrib-devhelp==1.0.2
     # via sphinx
 sphinxcontrib-htmlhelp==2.0.0
     # via sphinx
+sphinxcontrib-jquery==3.0.0
+    # via sphinx-hoverxref
 sphinxcontrib-jsmath==1.0.1
     # via sphinx
 sphinxcontrib-qthelp==1.0.3
     # via sphinx
 sphinxcontrib-serializinghtml==1.1.5
     # via sphinx
-sqlparse==0.4.2
+sqlparse==0.4.3
     # via django
-stack-data==0.3.0
+stack-data==0.6.2
     # via ipython
-stevedore==4.0.0
+stevedore==4.1.1
     # via bandit
-tokenize-rt==4.2.1
+tokenize-rt==5.0.0
     # via pyupgrade
-toml==0.10.2
-    # via
-    #   -r requirements/tools.in
-    #   tox
 tomli==2.0.1
     # via
+    #   autoflake
     #   black
     #   build
     #   mypy
     #   pep517
     #   pytest
-tox==3.25.1
+    #   tox
+tox==3.27.1
     # via -r requirements/tools.in
-traitlets==5.3.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
 types-pkg-resources==0.1.3
     # via -r requirements/tools.in
-types-pytz==2022.1.2
+types-pytz==2022.6.0.1
     # via -r requirements/tools.in
-types-redis==4.3.12
+types-redis==4.3.21.6
     # via -r requirements/tools.in
-typing-extensions==4.3.0
+typing-extensions==4.4.0
     # via
     #   -r requirements/tools.in
-    #   black
     #   flake8-noqa
     #   flake8-pie
     #   libcst
     #   mypy
-    #   rich
     #   typing-inspect
-typing-inspect==0.7.1
+typing-inspect==0.8.0
     # via libcst
-urllib3==1.26.11
+urllib3==1.26.13
     # via
     #   requests
     #   twine
-virtualenv==20.16.2
+virtualenv==20.17.0
     # via tox
 wcwidth==0.2.5
     # via prompt-toolkit
 webencodings==0.5.1
     # via bleach
-wheel==0.37.1
+wheel==0.38.4
     # via pip-tools
-zipp==3.8.1
+zipp==3.11.0
     # via importlib-metadata
 
 # The following packages are considered to be unsafe in a requirements file:
-# pip
-# setuptools
+pip==22.3.1
+    # via pip-tools
+setuptools==65.6.3
+    # via
+    #   nodeenv
+    #   pip-tools
+    #   sphinxcontrib-jquery
diff --git a/tooling/ignore-list.txt b/tooling/ignore-list.txt
index ede56a0e4..203487fd6 100644
--- a/tooling/ignore-list.txt
+++ b/tooling/ignore-list.txt
@@ -1,4 +1,5 @@
 crate
+damon
 nd
 ned
 nin
diff --git a/tooling/src/hypothesistooling/__main__.py b/tooling/src/hypothesistooling/__main__.py
index cc1ea7a1f..9909260f0 100644
--- a/tooling/src/hypothesistooling/__main__.py
+++ b/tooling/src/hypothesistooling/__main__.py
@@ -243,7 +243,7 @@ def check_not_changed():
 @task()
 def compile_requirements(upgrade=False):
     if upgrade:
-        extra = ["--upgrade"]
+        extra = ["--upgrade", "--rebuild"]
     else:
         extra = []
 
@@ -251,12 +251,18 @@ def compile_requirements(upgrade=False):
         base, _ = os.path.splitext(f)
         pip_tool(
             "pip-compile",
+            "--allow-unsafe",  # future default, not actually unsafe
+            "--resolver=backtracking",  # new pip resolver, default in pip-compile 7+
             *extra,
             f,
             "hypothesis-python/setup.py",
             "--output-file",
             base + ".txt",
             cwd=tools.ROOT,
+            env={
+                "CUSTOM_COMPILE_COMMAND": "./build.sh upgrade-requirements",
+                **os.environ,
+            },
         )
 
 
@@ -380,17 +386,17 @@ 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.13",
-    "3.8": "3.8.13",
-    "3.9": "3.9.13",
-    "3.10": "3.10.5",
-    "3.11": "3.11-dev",
+    "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.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",
 }
-ci_version = "3.8"  # Keep this in sync with GH Actions main.yml
+ci_version = "3.10"  # Keep this in sync with GH Actions main.yml and .readthedocs.yml
 
 python_tests = task(
     if_changed=(
@@ -412,13 +418,17 @@ for key, version in PYTHONS.items():
         ALIASES[version] = f"python{key}"
         name = f"py3{key[2:]}"
     TASKS[f"check-{name}"] = python_tests(
-        lambda n=f"{name}-full", v=version: run_tox(n, v)
+        lambda n=f"{name}-full", v=version, *args: run_tox(n, v, *args)
     )
+    for subtask in ("brief", "full", "cover", "nocover", "niche", "custom"):
+        TASKS[f"check-{name}-{subtask}"] = python_tests(
+            lambda n=f"{name}-{subtask}", v=version, *args: run_tox(n, v, *args)
+        )
 
 
 @python_tests
-def check_py310_pyjion():
-    run_tox("py310-pyjion", PYTHONS["3.10"])
+def check_py310_pyjion(*args):
+    run_tox("py310-pyjion", PYTHONS["3.10"], *args)
 
 
 @task()
@@ -426,21 +436,25 @@ def tox(*args):
     if len(args) < 2:
         print("Usage: ./build.sh tox TOX_ENV PY_VERSION [tox args]")
         sys.exit(1)
-    run_tox(args[0], args[1], *args[2:])
+    run_tox(*args)
 
 
-def standard_tox_task(name):
-    TASKS["check-" + name] = python_tests(lambda: run_tox(name, PYTHONS[ci_version]))
+def standard_tox_task(name, *args, py=ci_version):
+    TASKS["check-" + name] = python_tests(
+        lambda: run_tox(name, PYTHONS.get(py, py), *args)
+    )
 
 
-standard_tox_task("nose")
-standard_tox_task("pytest46")
-standard_tox_task("pytest54")
+standard_tox_task("py39-nose", py="3.9")
+standard_tox_task("py39-pytest46", py="3.9")
+standard_tox_task("py39-pytest54", py="3.9")
 standard_tox_task("pytest62")
 
-for n in [22, 32, 40]:
+for n in [32, 40, 41]:
     standard_tox_task(f"django{n}")
-for n in [10, 11, 12, 13, 14]:
+
+standard_tox_task("py39-pandas10", py="3.9")
+for n in [11, 12, 13, 14, 15]:
     standard_tox_task(f"pandas{n}")
 
 standard_tox_task("coverage")
@@ -448,13 +462,13 @@ standard_tox_task("conjecture-coverage")
 
 
 @task()
-def check_quality():
-    run_tox("quality", PYTHONS[ci_version])
+def check_quality(*args):
+    run_tox("quality", PYTHONS[ci_version], *args)
 
 
 @task(if_changed=(hp.PYTHON_SRC, os.path.join(hp.HYPOTHESIS_PYTHON, "examples")))
-def check_examples3():
-    run_tox("examples3", PYTHONS[ci_version])
+def check_examples3(*args):
+    run_tox("examples3", PYTHONS[ci_version], *args)
 
 
 @task()
diff --git a/whole-repo-tests/test_mypy.py b/whole-repo-tests/test_mypy.py
index d8956bccf..7cc2d2eec 100644
--- a/whole-repo-tests/test_mypy.py
+++ b/whole-repo-tests/test_mypy.py
@@ -84,6 +84,8 @@ def assert_mypy_errors(fname, expected, python_version=None):
             # Intentional print so we can check mypy's output if a test fails
             print(error_line)
             error_code = error_line.split("[")[-1].rstrip("]")
+            if error_code == "empty-body":
+                continue
             yield (int(col), error_code)
 
     assert sorted(convert_lines()) == sorted(expected)
@@ -538,3 +540,26 @@ def test_raises_for_mixed_pos_kwargs_in_given(tmpdir, python_version):
     assert_mypy_errors(
         str(f.realpath()), [(5, "call-overload")], python_version=python_version
     )
+
+
+def test_register_random_interface(tmpdir):
+    f = tmpdir.join("check_mypy_on_pos_arg_only_strats.py")
+    f.write(
+        textwrap.dedent(
+            """
+            from random import Random
+            from hypothesis import register_random
+
+            class MyRandom:
+                def __init__(self) -> None:
+                    r = Random()
+                    self.seed = r.seed
+                    self.setstate = r.setstate
+                    self.getstate = r.getstate
+
+            register_random(MyRandom())
+            register_random(None)  # type: ignore[arg-type]
+            """
+        )
+    )
+    assert_mypy_errors(str(f.realpath()), [])
diff --git a/whole-repo-tests/test_pyright.py b/whole-repo-tests/test_pyright.py
index 68ec016a6..89f79f70d 100644
--- a/whole-repo-tests/test_pyright.py
+++ b/whole-repo-tests/test_pyright.py
@@ -195,6 +195,30 @@ def test_pyright_one_of_pos_args_only(tmp_path: Path):
     )
 
 
+def test_register_random_protocol(tmp_path: Path):
+    file = tmp_path / "test.py"
+    file.write_text(
+        textwrap.dedent(
+            """
+            from random import Random
+            from hypothesis import register_random
+
+            class MyRandom:
+                def __init__(self) -> None:
+                    r = Random()
+                    self.seed = r.seed
+                    self.setstate = r.setstate
+                    self.getstate = r.getstate
+
+            register_random(MyRandom())
+            register_random(None)  # type: ignore
+            """
+        )
+    )
+    _write_config(tmp_path, {"reportUnnecessaryTypeIgnoreComment": True})
+    assert _get_pyright_errors(file) == []
+
+
 # ---------- Helpers for running pyright ---------- #
 
 

test_patch: diff --git a/hypothesis-python/tests/README.rst b/hypothesis-python/tests/README.rst
index 500f042eb..496a98bb7 100644
--- a/hypothesis-python/tests/README.rst
+++ b/hypothesis-python/tests/README.rst
@@ -89,7 +89,7 @@ Still here?  Here's a note on what to expect in each directory.
 
 ``lark/``
     Tests for the Lark extra for context-free grammars, which depend on the
-    ``lark-parser`` package.
+    ``lark`` package.
 
 ``nocover/``
     More expensive and longer-running tests, typically used to test trickier
diff --git a/hypothesis-python/tests/array_api/README.md b/hypothesis-python/tests/array_api/README.md
index e3dcefa03..bc2f4d1aa 100644
--- a/hypothesis-python/tests/array_api/README.md
+++ b/hypothesis-python/tests/array_api/README.md
@@ -1,16 +1,19 @@
 This folder contains tests for `hypothesis.extra.array_api`.
 
-## Running against different array modules
+## Mocked array module
+
+A mock of the Array API namespace exists as `mock_xp` in `extra.array_api`. This
+wraps NumPy-proper to conform it to the *draft* spec, where `numpy.array_api`
+might not. This is not a fully compliant wrapper, but conforms enough for the
+purposes of testing.
 
-By default it will run against `numpy.array_api`. If that's not available
-(likely because an older NumPy version is installed), these tests will fallback
-to using the mock defined at the bottom of `src/hypothesis/extra/array_api.py`.
+## Running against different array modules
 
 You can test other array modules which adopt the Array API via the
 `HYPOTHESIS_TEST_ARRAY_API` environment variable. There are two recognized
 options:
 
-* `"default"`: only uses `numpy.array_api`, or if not available, fallbacks to the mock.
+* `"default"`: uses the mock.
 * `"all"`: uses all array modules found via entry points, _and_ the mock.
 
 If neither of these, the test suite will then try resolve the variable like so:
@@ -30,3 +33,18 @@ or use the import path (**2.**),
 The former method is more ergonomic, but as entry points are optional for
 adopting the Array API, you will need to use the latter method for libraries
 that opt-out.
+
+## Running against different API versions
+
+You can specify the `api_version` to use when testing array modules via the 
+`HYPOTHESIS_TEST_ARRAY_API_VERSION` environment variable. There is one
+recognized option:
+
+* `"default"`: infers the latest API version for each array module.
+
+Otherwise the test suite will use the variable as the `api_version` argument for
+`make_strategies_namespace()`.
+
+In the future we intend to support running tests against multiple API versioned
+namespaces, likely with an additional recognized option that infers all
+supported versions.
diff --git a/hypothesis-python/tests/array_api/common.py b/hypothesis-python/tests/array_api/common.py
index 72c674b43..cf706d970 100644
--- a/hypothesis-python/tests/array_api/common.py
+++ b/hypothesis-python/tests/array_api/common.py
@@ -11,14 +11,30 @@
 from importlib.metadata import EntryPoint, entry_points  # type: ignore
 from typing import Dict
 
+import pytest
+
+from hypothesis.extra.array_api import (
+    COMPLEX_NAMES,
+    REAL_NAMES,
+    RELEASED_VERSIONS,
+    NominalVersion,
+)
 from hypothesis.internal.floats import next_up
 
 __all__ = [
+    "MIN_VER_FOR_COMPLEX:",
     "installed_array_modules",
     "flushes_to_zero",
+    "dtype_name_params",
 ]
 
 
+# This should be updated to the next spec release, which should include complex numbers
+MIN_VER_FOR_COMPLEX: NominalVersion = "draft"
+if len(RELEASED_VERSIONS) > 1:
+    assert MIN_VER_FOR_COMPLEX == RELEASED_VERSIONS[1]
+
+
 def installed_array_modules() -> Dict[str, EntryPoint]:
     """Returns a dictionary of array module names paired to their entry points
 
@@ -48,3 +64,9 @@ def flushes_to_zero(xp, width: int) -> bool:
         raise ValueError(f"{width=}, but should be either 32 or 64")
     dtype = getattr(xp, f"float{width}")
     return bool(xp.asarray(next_up(0.0, width=width), dtype=dtype) == 0)
+
+
+dtype_name_params = ["bool"] + list(REAL_NAMES)
+for name in COMPLEX_NAMES:
+    param = pytest.param(name, marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX))
+    dtype_name_params.append(param)
diff --git a/hypothesis-python/tests/array_api/conftest.py b/hypothesis-python/tests/array_api/conftest.py
index afa59d40a..7ee6a1f4e 100644
--- a/hypothesis-python/tests/array_api/conftest.py
+++ b/hypothesis-python/tests/array_api/conftest.py
@@ -11,62 +11,109 @@
 import warnings
 from importlib import import_module
 from os import getenv
+from types import ModuleType, SimpleNamespace
+from typing import Tuple
 
 import pytest
 
-from hypothesis.errors import HypothesisWarning
-from hypothesis.extra.array_api import make_strategies_namespace, mock_xp
+from hypothesis.errors import HypothesisWarning, InvalidArgument
+from hypothesis.extra.array_api import (
+    NOMINAL_VERSIONS,
+    NominalVersion,
+    make_strategies_namespace,
+    mock_xp,
+)
 
 from tests.array_api.common import installed_array_modules
 
+# See README.md in regards to the env variables
+test_xp_option = getenv("HYPOTHESIS_TEST_ARRAY_API", "default")
+
+test_version_option = getenv("HYPOTHESIS_TEST_ARRAY_API_VERSION", "default")
+if test_version_option != "default" and test_version_option not in NOMINAL_VERSIONS:
+    raise ValueError(
+        f"HYPOTHESIS_TEST_ARRAY_API_VERSION='{test_version_option}' is not "
+        f"'default' or a valid api_version {NOMINAL_VERSIONS}."
+    )
 with pytest.warns(HypothesisWarning):
-    mock_xps = make_strategies_namespace(mock_xp)
+    mock_version = "draft" if test_version_option == "default" else test_version_option
+    mock_xps = make_strategies_namespace(mock_xp, api_version=mock_version)
+api_version = None if test_version_option == "default" else test_version_option
+
+
+class InvalidArgumentWarning(UserWarning):
+    """Custom warning so we can bypass our global capturing"""
+
 
-# See README.md in regards to the HYPOTHESIS_TEST_ARRAY_API env variable
-test_xp_option = getenv("HYPOTHESIS_TEST_ARRAY_API", "default")
 name_to_entry_point = installed_array_modules()
+xp_and_xps_pairs: Tuple[ModuleType, SimpleNamespace] = []
 with warnings.catch_warnings():
-    # We ignore all warnings here as many array modules warn on import
+    # We ignore all warnings here as many array modules warn on import. Ideally
+    # we would just ignore ImportWarning, but no one seems to use it!
     warnings.simplefilter("ignore")
-    # We go through the steps described in README.md to define `params`, which
-    # contains the array module(s) to be ran against the test suite.
-    # Specifically `params` is a list of pytest parameters, with each parameter
-    # containing the array module and its respective strategies namespace.
+    warnings.simplefilter("default", category=InvalidArgumentWarning)
+    # We go through the steps described in README.md to define `xp_xps_pairs`,
+    # which contains the array module(s) to be run against the test suite, along
+    # with their respective strategy namespaces.
     if test_xp_option == "default":
-        try:
-            xp = name_to_entry_point["numpy"].load()
-            xps = make_strategies_namespace(xp)
-            params = [pytest.param(xp, xps, id="numpy")]
-        except KeyError:
-            params = [pytest.param(mock_xp, mock_xps, id="mock")]
+        xp_and_xps_pairs = [(mock_xp, mock_xps)]
     elif test_xp_option == "all":
         if len(name_to_entry_point) == 0:
             raise ValueError(
                 "HYPOTHESIS_TEST_ARRAY_API='all', but no entry points where found"
             )
-        params = [pytest.param(mock_xp, mock_xps, id="mock")]
+        xp_and_xps_pairs = [(mock_xp, mock_xps)]
         for name, ep in name_to_entry_point.items():
             xp = ep.load()
-            xps = make_strategies_namespace(xp)
-            params.append(pytest.param(xp, xps, id=name))
+            try:
+                xps = make_strategies_namespace(xp, api_version=api_version)
+            except InvalidArgument as e:
+                warnings.warn(str(e), InvalidArgumentWarning)
+            else:
+                xp_and_xps_pairs.append((xp, xps))
     elif test_xp_option in name_to_entry_point.keys():
         ep = name_to_entry_point[test_xp_option]
         xp = ep.load()
-        xps = make_strategies_namespace(xp)
-        params = [pytest.param(xp, xps, id=test_xp_option)]
+        xps = make_strategies_namespace(xp, api_version=api_version)
+        xp_and_xps_pairs = [(xp, xps)]
     else:
         try:
             xp = import_module(test_xp_option)
-            xps = make_strategies_namespace(xp)
-            params = [pytest.param(xp, xps, id=test_xp_option)]
         except ImportError as e:
             raise ValueError(
                 f"HYPOTHESIS_TEST_ARRAY_API='{test_xp_option}' is not a valid "
                 "option ('default' or 'all'), name of an available entry point, "
                 "or a valid import path."
             ) from e
+        else:
+            xps = make_strategies_namespace(xp, api_version=api_version)
+            xp_and_xps_pairs = [(xp, xps)]
 
 
 def pytest_generate_tests(metafunc):
-    if "xp" in metafunc.fixturenames and "xps" in metafunc.fixturenames:
-        metafunc.parametrize("xp, xps", params)
+    xp_params = []
+    xp_and_xps_params = []
+    for xp, xps in xp_and_xps_pairs:
+        xp_params.append(pytest.param(xp, id=xp.__name__))
+        xp_and_xps_params.append(
+            pytest.param(xp, xps, id=f"{xp.__name__}-{xps.api_version}")
+        )
+    if "xp" in metafunc.fixturenames:
+        if "xps" in metafunc.fixturenames:
+            metafunc.parametrize("xp, xps", xp_and_xps_params)
+        else:
+            metafunc.parametrize("xp", xp_params)
+
+
+def pytest_collection_modifyitems(config, items):
+    for item in items:
+        if "xps" in item.fixturenames:
+            markers = [m for m in item.own_markers if m.name == "xp_min_version"]
+            if markers:
+                assert len(markers) == 1  # sanity check
+                min_version: NominalVersion = markers[0].args[0]
+                xps_version: NominalVersion = item.callspec.params["xps"].api_version
+                if xps_version < min_version:
+                    item.add_marker(
+                        pytest.mark.skip(reason=f"requires api_version=>{min_version}")
+                    )
diff --git a/hypothesis-python/tests/array_api/test_argument_validation.py b/hypothesis-python/tests/array_api/test_argument_validation.py
index 0ea07d20c..3a14ee998 100644
--- a/hypothesis-python/tests/array_api/test_argument_validation.py
+++ b/hypothesis-python/tests/array_api/test_argument_validation.py
@@ -8,14 +8,24 @@
 # 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 typing import Optional
+
 import pytest
 
 from hypothesis.errors import InvalidArgument
+from hypothesis.extra.array_api import NominalVersion, make_strategies_namespace
+
+from tests.array_api.common import MIN_VER_FOR_COMPLEX
 
 
-def e(name, **kwargs):
+def e(name, *, _min_version: Optional[NominalVersion] = None, **kwargs):
     kw = ", ".join(f"{k}={v!r}" for k, v in kwargs.items())
-    return pytest.param(name, kwargs, id=f"{name}({kw})")
+    id_ = f"{name}({kw})"
+    if _min_version is None:
+        marks = ()
+    else:
+        marks = pytest.mark.xp_min_version(_min_version)
+    return pytest.param(name, kwargs, id=id_, marks=marks)
 
 
 @pytest.mark.parametrize(
@@ -61,6 +71,8 @@ def e(name, **kwargs):
         e("unsigned_integer_dtypes", sizes=(3,)),
         e("floating_dtypes", sizes=()),
         e("floating_dtypes", sizes=(3,)),
+        e("complex_dtypes", _min_version=MIN_VER_FOR_COMPLEX, sizes=()),
+        e("complex_dtypes", _min_version=MIN_VER_FOR_COMPLEX, sizes=(3,)),
         e("valid_tuple_axes", ndim=-1),
         e("valid_tuple_axes", ndim=2, min_size=-1),
         e("valid_tuple_axes", ndim=2, min_size=3, max_size=10),
@@ -214,3 +226,10 @@ def test_raise_invalid_argument(xp, xps, strat_name, kwargs):
     strat = strat_func(**kwargs)
     with pytest.raises(InvalidArgument):
         strat.example()
+
+
+@pytest.mark.parametrize("api_version", [..., "latest", "1970.01", 42])
+def test_make_strategies_namespace_raise_invalid_argument(xp, api_version):
+    """Function raises helpful error with invalid arguments."""
+    with pytest.raises(InvalidArgument):
+        make_strategies_namespace(xp, api_version=api_version)
diff --git a/hypothesis-python/tests/array_api/test_arrays.py b/hypothesis-python/tests/array_api/test_arrays.py
index 6d9feff3d..b36520688 100644
--- a/hypothesis-python/tests/array_api/test_arrays.py
+++ b/hypothesis-python/tests/array_api/test_arrays.py
@@ -12,10 +12,14 @@ import pytest
 
 from hypothesis import given, strategies as st
 from hypothesis.errors import InvalidArgument
-from hypothesis.extra.array_api import DTYPE_NAMES, NUMERIC_NAMES
+from hypothesis.extra.array_api import COMPLEX_NAMES, REAL_NAMES
 from hypothesis.internal.floats import width_smallest_normals
 
-from tests.array_api.common import flushes_to_zero
+from tests.array_api.common import (
+    MIN_VER_FOR_COMPLEX,
+    dtype_name_params,
+    flushes_to_zero,
+)
 from tests.common.debug import assert_all_examples, find_any, minimal
 from tests.common.utils import flaky
 
@@ -38,14 +42,14 @@ def xfail_on_indistinct_nans(xp):
         pytest.xfail("NaNs not distinct")
 
 
-@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+@pytest.mark.parametrize("dtype_name", dtype_name_params)
 def test_draw_arrays_from_dtype(xp, xps, dtype_name):
     """Draw arrays from dtypes."""
     dtype = getattr(xp, dtype_name)
     assert_all_examples(xps.arrays(dtype, ()), lambda x: x.dtype == dtype)
 
 
-@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+@pytest.mark.parametrize("dtype_name", dtype_name_params)
 def test_draw_arrays_from_scalar_names(xp, xps, dtype_name):
     """Draw arrays from dtype names."""
     dtype = getattr(xp, dtype_name)
@@ -77,6 +81,10 @@ def test_draw_arrays_from_int_shapes(xp, xps, data):
         "integer_dtypes",
         "unsigned_integer_dtypes",
         "floating_dtypes",
+        "real_dtypes",
+        pytest.param(
+            "complex_dtypes", marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX)
+        ),
     ],
 )
 def test_draw_arrays_from_dtype_strategies(xp, xps, strat_name):
@@ -86,14 +94,16 @@ def test_draw_arrays_from_dtype_strategies(xp, xps, strat_name):
     find_any(xps.arrays(strat, ()))
 
 
-@given(
-    strat=st.lists(st.sampled_from(DTYPE_NAMES), min_size=1, unique=True).flatmap(
-        st.sampled_from
-    )
-)
-def test_draw_arrays_from_dtype_name_strategies(xp, xps, strat):
+@given(data=st.data())
+def test_draw_arrays_from_dtype_name_strategies(xp, xps, data):
     """Draw arrays from dtype name strategies."""
-    find_any(xps.arrays(strat, ()))
+    all_names = ("bool",) + REAL_NAMES
+    if xps.api_version > "2021.12":
+        all_names += COMPLEX_NAMES
+    sample_names = data.draw(
+        st.lists(st.sampled_from(all_names), min_size=1, unique=True)
+    )
+    find_any(xps.arrays(st.sampled_from(sample_names), ()))
 
 
 def test_generate_arrays_from_shapes_strategy(xp, xps):
@@ -156,7 +166,7 @@ def test_minimize_arrays_with_0d_shape_strategy(xp, xps):
     assert smallest.shape == ()
 
 
-@pytest.mark.parametrize("dtype", NUMERIC_NAMES)
+@pytest.mark.parametrize("dtype", dtype_name_params[1:])
 def test_minimizes_numeric_arrays(xp, xps, dtype):
     """Strategies with numeric dtypes minimize to zero-filled arrays."""
     smallest = minimal(xps.arrays(dtype, (2, 2)))
@@ -296,6 +306,11 @@ def test_may_not_use_overflowing_integers(xp, xps, kwargs):
     [
         ("float32", st.floats(min_value=10**40, allow_infinity=False)),
         ("float64", st.floats(min_value=10**40, allow_infinity=False)),
+        pytest.param(
+            "complex64",
+            st.complex_numbers(min_magnitude=10**300, allow_infinity=False),
+            marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX),
+        ),
     ],
 )
 def test_may_not_use_unrepresentable_elements(xp, xps, fill, dtype, strat):
diff --git a/hypothesis-python/tests/array_api/test_from_dtype.py b/hypothesis-python/tests/array_api/test_from_dtype.py
index 1ea61a5e2..18f6d081f 100644
--- a/hypothesis-python/tests/array_api/test_from_dtype.py
+++ b/hypothesis-python/tests/array_api/test_from_dtype.py
@@ -12,10 +12,10 @@ import math
 
 import pytest
 
-from hypothesis.extra.array_api import DTYPE_NAMES, find_castable_builtin_for_dtype
+from hypothesis.extra.array_api import find_castable_builtin_for_dtype
 from hypothesis.internal.floats import width_smallest_normals
 
-from tests.array_api.common import flushes_to_zero
+from tests.array_api.common import dtype_name_params, flushes_to_zero
 from tests.common.debug import (
     assert_all_examples,
     assert_no_examples,
@@ -24,32 +24,32 @@ from tests.common.debug import (
 )
 
 
-@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+@pytest.mark.parametrize("dtype_name", dtype_name_params)
 def test_strategies_have_reusable_values(xp, xps, dtype_name):
     """Inferred strategies have reusable values."""
     strat = xps.from_dtype(dtype_name)
     assert strat.has_reusable_values
 
 
-@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+@pytest.mark.parametrize("dtype_name", dtype_name_params)
 def test_produces_castable_instances_from_dtype(xp, xps, dtype_name):
     """Strategies inferred by dtype generate values of a builtin type castable
     to the dtype."""
     dtype = getattr(xp, dtype_name)
-    builtin = find_castable_builtin_for_dtype(xp, dtype)
+    builtin = find_castable_builtin_for_dtype(xp, xps.api_version, dtype)
     assert_all_examples(xps.from_dtype(dtype), lambda v: isinstance(v, builtin))
 
 
-@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+@pytest.mark.parametrize("dtype_name", dtype_name_params)
 def test_produces_castable_instances_from_name(xp, xps, dtype_name):
     """Strategies inferred by dtype name generate values of a builtin type
     castable to the dtype."""
     dtype = getattr(xp, dtype_name)
-    builtin = find_castable_builtin_for_dtype(xp, dtype)
+    builtin = find_castable_builtin_for_dtype(xp, xps.api_version, dtype)
     assert_all_examples(xps.from_dtype(dtype_name), lambda v: isinstance(v, builtin))
 
 
-@pytest.mark.parametrize("dtype_name", DTYPE_NAMES)
+@pytest.mark.parametrize("dtype_name", dtype_name_params)
 def test_passing_inferred_strategies_in_arrays(xp, xps, dtype_name):
     """Inferred strategies usable in arrays strategy."""
     elements = xps.from_dtype(dtype_name)
diff --git a/hypothesis-python/tests/array_api/test_partial_adoptors.py b/hypothesis-python/tests/array_api/test_partial_adoptors.py
index 6ae47a220..2f0f41062 100644
--- a/hypothesis-python/tests/array_api/test_partial_adoptors.py
+++ b/hypothesis-python/tests/array_api/test_partial_adoptors.py
@@ -18,6 +18,7 @@ import pytest
 from hypothesis import given, strategies as st
 from hypothesis.errors import HypothesisWarning, InvalidArgument
 from hypothesis.extra.array_api import (
+    COMPLEX_NAMES,
     DTYPE_NAMES,
     FLOAT_NAMES,
     INT_NAMES,
@@ -30,8 +31,9 @@ MOCK_WARN_MSG = f"determine.*{mock_xp.__name__}.*Array API"
 
 
 @lru_cache()
-def make_mock_xp(exclude: Tuple[str, ...] = ()) -> SimpleNamespace:
+def make_mock_xp(*, exclude: Tuple[str, ...] = ()) -> SimpleNamespace:
     xp = copy(mock_xp)
+    assert isinstance(exclude, tuple)  # sanity check
     for attr in exclude:
         delattr(xp, attr)
     return xp
@@ -41,7 +43,7 @@ def test_warning_on_noncompliant_xp():
     """Using non-compliant array modules raises helpful warning"""
     xp = make_mock_xp()
     with pytest.warns(HypothesisWarning, match=MOCK_WARN_MSG):
-        make_strategies_namespace(xp)
+        make_strategies_namespace(xp, api_version="draft")
 
 
 @pytest.mark.filterwarnings(f"ignore:.*{MOCK_WARN_MSG}.*")
@@ -53,7 +55,7 @@ def test_error_on_missing_attr(stratname, args, attr):
     """Strategies raise helpful error when using array modules that lack
     required attributes."""
     xp = make_mock_xp(exclude=(attr,))
-    xps = make_strategies_namespace(xp)
+    xps = make_strategies_namespace(xp, api_version="draft")
     func = getattr(xps, stratname)
     with pytest.raises(InvalidArgument, match=f"{mock_xp.__name__}.*required.*{attr}"):
         func(*args).example()
@@ -61,7 +63,7 @@ def test_error_on_missing_attr(stratname, args, attr):
 
 dtypeless_xp = make_mock_xp(exclude=tuple(DTYPE_NAMES))
 with pytest.warns(HypothesisWarning):
-    dtypeless_xps = make_strategies_namespace(dtypeless_xp)
+    dtypeless_xps = make_strategies_namespace(dtypeless_xp, api_version="draft")
 
 
 @pytest.mark.parametrize(
@@ -73,6 +75,8 @@ with pytest.warns(HypothesisWarning):
         "integer_dtypes",
         "unsigned_integer_dtypes",
         "floating_dtypes",
+        "real_dtypes",
+        "complex_dtypes",
     ],
 )
 def test_error_on_missing_dtypes(stratname):
@@ -88,10 +92,12 @@ def test_error_on_missing_dtypes(stratname):
     "stratname, keep_anys",
     [
         ("scalar_dtypes", [INT_NAMES, UINT_NAMES, FLOAT_NAMES]),
-        ("numeric_dtypes", [INT_NAMES, UINT_NAMES, FLOAT_NAMES]),
+        ("numeric_dtypes", [INT_NAMES, UINT_NAMES, FLOAT_NAMES, COMPLEX_NAMES]),
         ("integer_dtypes", [INT_NAMES]),
         ("unsigned_integer_dtypes", [UINT_NAMES]),
         ("floating_dtypes", [FLOAT_NAMES]),
+        ("real_dtypes", [INT_NAMES, UINT_NAMES, FLOAT_NAMES]),
+        ("complex_dtypes", [COMPLEX_NAMES]),
     ],
 )
 @given(st.data())
@@ -111,7 +117,24 @@ def test_warning_on_partial_dtypes(stratname, keep_anys, data):
             )
         )
     xp = make_mock_xp(exclude=tuple(exclude))
-    xps = make_strategies_namespace(xp)
+    xps = make_strategies_namespace(xp, api_version="draft")
     func = getattr(xps, stratname)
     with pytest.warns(HypothesisWarning, match=f"{mock_xp.__name__}.*dtype.*namespace"):
         data.draw(func())
+
+
+def test_raises_on_inferring_with_no_dunder_version():
+    """When xp has no __array_api_version__, inferring api_version raises
+    helpful error."""
+    xp = make_mock_xp(exclude=("__array_api_version__",))
+    with pytest.raises(InvalidArgument, match="has no attribute"):
+        make_strategies_namespace(xp)
+
+
+def test_raises_on_invalid_dunder_version():
+    """When xp has invalid __array_api_version__, inferring api_version raises
+    helpful error."""
+    xp = make_mock_xp()
+    xp.__array_api_version__ = None
+    with pytest.raises(InvalidArgument):
+        make_strategies_namespace(xp)
diff --git a/hypothesis-python/tests/array_api/test_pretty.py b/hypothesis-python/tests/array_api/test_pretty.py
index 79a7c6bb6..bb38f6427 100644
--- a/hypothesis-python/tests/array_api/test_pretty.py
+++ b/hypothesis-python/tests/array_api/test_pretty.py
@@ -12,6 +12,11 @@ from inspect import signature
 
 import pytest
 
+from hypothesis.errors import InvalidArgument
+from hypothesis.extra.array_api import make_strategies_namespace
+
+from tests.array_api.common import MIN_VER_FOR_COMPLEX
+
 
 @pytest.mark.parametrize(
     "name",
@@ -25,6 +30,10 @@ import pytest
         "integer_dtypes",
         "unsigned_integer_dtypes",
         "floating_dtypes",
+        "real_dtypes",
+        pytest.param(
+            "complex_dtypes", marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX)
+        ),
         "valid_tuple_axes",
         "broadcastable_shapes",
         "mutually_broadcastable_shapes",
@@ -55,6 +64,10 @@ def test_namespaced_methods_meta(xp, xps, name):
         ("integer_dtypes", []),
         ("unsigned_integer_dtypes", []),
         ("floating_dtypes", []),
+        ("real_dtypes", []),
+        pytest.param(
+            "complex_dtypes", [], marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX)
+        ),
         ("valid_tuple_axes", [0]),
         ("broadcastable_shapes", [()]),
         ("mutually_broadcastable_shapes", [3]),
@@ -70,8 +83,22 @@ def test_namespaced_strategies_repr(xp, xps, name, valid_args):
     assert xp.__name__ not in repr(strat), f"{xp.__name__} in strat repr"
 
 
-def test_strategies_namespace_repr(xp, xps):
-    """Strategies namespace has good repr."""
+@pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning")
+def test_inferred_version_strategies_namespace_repr(xp):
+    """Strategies namespace has good repr when api_version=None."""
+    try:
+        xps = make_strategies_namespace(xp)
+    except InvalidArgument as e:
+        pytest.skip(str(e))
     expected = f"make_strategies_namespace({xp.__name__})"
     assert repr(xps) == expected
     assert str(xps) == expected
+
+
+@pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning")
+def test_specified_version_strategies_namespace_repr(xp):
+    """Strategies namespace has good repr when api_version is specified."""
+    xps = make_strategies_namespace(xp, api_version="2021.12")
+    expected = f"make_strategies_namespace({xp.__name__}, api_version='2021.12')"
+    assert repr(xps) == expected
+    assert str(xps) == expected
diff --git a/hypothesis-python/tests/array_api/test_scalar_dtypes.py b/hypothesis-python/tests/array_api/test_scalar_dtypes.py
index 4f733a179..09e294bd1 100644
--- a/hypothesis-python/tests/array_api/test_scalar_dtypes.py
+++ b/hypothesis-python/tests/array_api/test_scalar_dtypes.py
@@ -11,48 +11,86 @@
 import pytest
 
 from hypothesis.extra.array_api import (
+    COMPLEX_NAMES,
     DTYPE_NAMES,
     FLOAT_NAMES,
     INT_NAMES,
     NUMERIC_NAMES,
+    REAL_NAMES,
     UINT_NAMES,
 )
 
+from tests.array_api.common import MIN_VER_FOR_COMPLEX
 from tests.common.debug import assert_all_examples, find_any, minimal
 
 
-def test_can_generate_scalar_dtypes(xp, xps):
-    dtypes = [getattr(xp, name) for name in DTYPE_NAMES]
-    assert_all_examples(xps.scalar_dtypes(), lambda dtype: dtype in dtypes)
-
-
-def test_can_generate_boolean_dtypes(xp, xps):
-    assert_all_examples(xps.boolean_dtypes(), lambda dtype: dtype == xp.bool)
+@pytest.mark.parametrize(
+    ("strat_name", "dtype_names"),
+    [
+        ("integer_dtypes", INT_NAMES),
+        ("unsigned_integer_dtypes", UINT_NAMES),
+        ("floating_dtypes", FLOAT_NAMES),
+        ("real_dtypes", REAL_NAMES),
+        pytest.param(
+            "complex_dtypes",
+            COMPLEX_NAMES,
+            marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX),
+        ),
+    ],
+)
+def test_all_generated_dtypes_are_of_group(xp, xps, strat_name, dtype_names):
+    """Strategy only generates expected dtypes."""
+    strat_func = getattr(xps, strat_name)
+    dtypes = [getattr(xp, n) for n in dtype_names]
+    assert_all_examples(strat_func(), lambda dtype: dtype in dtypes)
 
 
-def test_can_generate_numeric_dtypes(xp, xps):
-    numeric_dtypes = [getattr(xp, name) for name in NUMERIC_NAMES]
-    assert_all_examples(xps.numeric_dtypes(), lambda dtype: dtype in numeric_dtypes)
+def test_all_generated_scalar_dtypes_are_scalar(xp, xps):
+    """Strategy only generates scalar dtypes."""
+    if xps.api_version > "2021.12":
+        dtypes = [getattr(xp, n) for n in DTYPE_NAMES]
+    else:
+        dtypes = [getattr(xp, n) for n in ("bool",) + REAL_NAMES]
+    assert_all_examples(xps.scalar_dtypes(), lambda dtype: dtype in dtypes)
 
 
-def test_can_generate_integer_dtypes(xp, xps):
-    int_dtypes = [getattr(xp, name) for name in INT_NAMES]
-    assert_all_examples(xps.integer_dtypes(), lambda dtype: dtype in int_dtypes)
+def test_all_generated_numeric_dtypes_are_numeric(xp, xps):
+    """Strategy only generates numeric dtypes."""
+    if xps.api_version > "2021.12":
+        dtypes = [getattr(xp, n) for n in NUMERIC_NAMES]
+    else:
+        dtypes = [getattr(xp, n) for n in REAL_NAMES]
+    assert_all_examples(xps.numeric_dtypes(), lambda dtype: dtype in dtypes)
 
 
-def test_can_generate_unsigned_integer_dtypes(xp, xps):
-    uint_dtypes = [getattr(xp, name) for name in UINT_NAMES]
-    assert_all_examples(
-        xps.unsigned_integer_dtypes(), lambda dtype: dtype in uint_dtypes
-    )
+def skipif_unsupported_complex(strat_name, dtype_name):
+    if not dtype_name.startswith("complex"):
+        return strat_name, dtype_name
+    mark = pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX)
+    return pytest.param(strat_name, dtype_name, marks=mark)
 
 
-def test_can_generate_floating_dtypes(xp, xps):
-    float_dtypes = [getattr(xp, name) for name in FLOAT_NAMES]
-    assert_all_examples(xps.floating_dtypes(), lambda dtype: dtype in float_dtypes)
+@pytest.mark.parametrize(
+    ("strat_name", "dtype_name"),
+    [
+        *[skipif_unsupported_complex("scalar_dtypes", n) for n in DTYPE_NAMES],
+        *[skipif_unsupported_complex("numeric_dtypes", n) for n in NUMERIC_NAMES],
+        *[("integer_dtypes", n) for n in INT_NAMES],
+        *[("unsigned_integer_dtypes", n) for n in UINT_NAMES],
+        *[("floating_dtypes", n) for n in FLOAT_NAMES],
+        *[("real_dtypes", n) for n in REAL_NAMES],
+        *[skipif_unsupported_complex("complex_dtypes", n) for n in COMPLEX_NAMES],
+    ],
+)
+def test_strategy_can_generate_every_dtype(xp, xps, strat_name, dtype_name):
+    """Strategy generates every expected dtype."""
+    strat_func = getattr(xps, strat_name)
+    dtype = getattr(xp, dtype_name)
+    find_any(strat_func(), lambda d: d == dtype)
 
 
 def test_minimise_scalar_dtypes(xp, xps):
+    """Strategy minimizes to bool dtype."""
     assert minimal(xps.scalar_dtypes()) == xp.bool
 
 
@@ -62,9 +100,13 @@ def test_minimise_scalar_dtypes(xp, xps):
         ("integer_dtypes", 8),
         ("unsigned_integer_dtypes", 8),
         ("floating_dtypes", 32),
+        pytest.param(
+            "complex_dtypes", 64, marks=pytest.mark.xp_min_version(MIN_VER_FOR_COMPLEX)
+        ),
     ],
 )
 def test_can_specify_sizes_as_an_int(xp, xps, strat_name, sizes):
+    """Strategy treats ints as a single size."""
     strat_func = getattr(xps, strat_name)
     strat = strat_func(sizes=sizes)
     find_any(strat)
diff --git a/hypothesis-python/tests/array_api/test_strategies_namespace.py b/hypothesis-python/tests/array_api/test_strategies_namespace.py
new file mode 100644
index 000000000..d1f18a6c2
--- /dev/null
+++ b/hypothesis-python/tests/array_api/test_strategies_namespace.py
@@ -0,0 +1,86 @@
+# 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 types import SimpleNamespace
+from weakref import WeakValueDictionary
+
+import pytest
+
+from hypothesis.extra import array_api
+from hypothesis.extra.array_api import (
+    NOMINAL_VERSIONS,
+    make_strategies_namespace,
+    mock_xp,
+)
+from hypothesis.strategies import SearchStrategy
+
+pytestmark = pytest.mark.filterwarnings("ignore::hypothesis.errors.HypothesisWarning")
+
+
+class HashableArrayModuleFactory:
+    """
+    mock_xp cannot be hashed and thus cannot be used in our cache. So just for
+    the purposes of testing the cache, we wrap it with an unsafe hash method.
+    """
+
+    def __getattr__(self, name):
+        return getattr(mock_xp, name)
+
+    def __hash__(self):
+        return hash(tuple(sorted(mock_xp.__dict__)))
+
+
+@pytest.mark.parametrize("api_version", ["2021.12", None])
+def test_caching(api_version, monkeypatch):
+    """Caches namespaces respective to arguments."""
+    xp = HashableArrayModuleFactory()
+    assert isinstance(array_api._args_to_xps, WeakValueDictionary)  # sanity check
+    monkeypatch.setattr(array_api, "_args_to_xps", WeakValueDictionary())
+    assert len(array_api._args_to_xps) == 0  # sanity check
+    xps1 = array_api.make_strategies_namespace(xp, api_version=api_version)
+    assert len(array_api._args_to_xps) == 1
+    xps2 = array_api.make_strategies_namespace(xp, api_version=api_version)
+    assert len(array_api._args_to_xps) == 1
+    assert isinstance(xps2, SimpleNamespace)
+    assert xps2 is xps1
+    del xps1
+    del xps2
+    assert len(array_api._args_to_xps) == 0
+
+
+@pytest.mark.parametrize(
+    "api_version1, api_version2", [(None, "2021.12"), ("2021.12", None)]
+)
+def test_inferred_namespace_shares_cache(api_version1, api_version2, monkeypatch):
+    """Results from inferred versions share the same cache key as results
+    from specified versions."""
+    xp = HashableArrayModuleFactory()
+    xp.__array_api_version__ = "2021.12"
+    assert isinstance(array_api._args_to_xps, WeakValueDictionary)  # sanity check
+    monkeypatch.setattr(array_api, "_args_to_xps", WeakValueDictionary())
+    assert len(array_api._args_to_xps) == 0  # sanity check
+    xps1 = array_api.make_strategies_namespace(xp, api_version=api_version1)
+    assert xps1.api_version == "2021.12"  # sanity check
+    assert len(array_api._args_to_xps) == 1
+    xps2 = array_api.make_strategies_namespace(xp, api_version=api_version2)
+    assert xps2.api_version == "2021.12"  # sanity check
+    assert len(array_api._args_to_xps) == 1
+    assert xps2 is xps1
+
+
+def test_complex_dtypes_raises_on_2021_12():
+    """Accessing complex_dtypes() for 2021.12 strategy namespace raises helpful
+    error, but accessing on future versions returns expected strategy."""
+    first_xps = make_strategies_namespace(mock_xp, api_version="2021.12")
+    with pytest.raises(AttributeError, match="attempted to access"):
+        first_xps.complex_dtypes()
+    for api_version in NOMINAL_VERSIONS[1:]:
+        xps = make_strategies_namespace(mock_xp, api_version=api_version)
+        assert isinstance(xps.complex_dtypes(), SearchStrategy)
diff --git a/hypothesis-python/tests/common/setup.py b/hypothesis-python/tests/common/setup.py
index 25c37ef25..d10927319 100644
--- a/hypothesis-python/tests/common/setup.py
+++ b/hypothesis-python/tests/common/setup.py
@@ -12,7 +12,7 @@ import os
 from tempfile import mkdtemp
 from warnings import filterwarnings
 
-from hypothesis import Verbosity, settings
+from hypothesis import Phase, Verbosity, settings
 from hypothesis._settings import not_set
 from hypothesis.configuration import set_hypothesis_home_dir
 from hypothesis.errors import NonInteractiveExampleWarning
@@ -25,10 +25,7 @@ def run():
     filterwarnings("ignore", category=ImportWarning)
     filterwarnings("ignore", category=FutureWarning, module="pandas._version")
 
-    # Fixed in recent versions but allowed by pytest=3.0.0; see #1630
-    filterwarnings("ignore", category=DeprecationWarning, module="pluggy")
-
-    # See https://github.com/numpy/numpy/pull/432
+    # See https://github.com/numpy/numpy/pull/432; still a thing as of 2022.
     filterwarnings("ignore", message="numpy.dtype size changed")
     filterwarnings("ignore", message="numpy.ufunc size changed")
 
@@ -42,14 +39,6 @@ def run():
         category=UserWarning,
     )
 
-    # Imported by Pandas in version 1.9, but fixed in later versions.
-    filterwarnings(
-        "ignore", message="Importing from numpy.testing.decorators is deprecated"
-    )
-    filterwarnings(
-        "ignore", message="Importing from numpy.testing.nosetester is deprecated"
-    )
-
     # User-facing warning which does not apply to our own tests
     filterwarnings("ignore", category=NonInteractiveExampleWarning)
 
@@ -77,7 +66,11 @@ def run():
             )
 
     settings.register_profile(
-        "default", settings(max_examples=20 if IN_COVERAGE_TESTS else not_set)
+        "default",
+        settings(
+            max_examples=20 if IN_COVERAGE_TESTS else not_set,
+            phases=list(Phase),  # Dogfooding the explain phase
+        ),
     )
 
     settings.register_profile("speedy", settings(max_examples=5))
diff --git a/hypothesis-python/tests/conftest.py b/hypothesis-python/tests/conftest.py
index bcbd02490..cb015988f 100644
--- a/hypothesis-python/tests/conftest.py
+++ b/hypothesis-python/tests/conftest.py
@@ -40,6 +40,10 @@ if sys.version_info >= (3, 11):
 
 def pytest_configure(config):
     config.addinivalue_line("markers", "slow: pandas expects this marker to exist.")
+    config.addinivalue_line(
+        "markers",
+        "xp_min_version(api_version): run when greater or equal to api_version",
+    )
 
 
 def pytest_addoption(parser):
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
diff --git a/hypothesis-python/tests/cover/test_direct_strategies.py b/hypothesis-python/tests/cover/test_direct_strategies.py
index 9845626b9..fec2c3768 100644
--- a/hypothesis-python/tests/cover/test_direct_strategies.py
+++ b/hypothesis-python/tests/cover/test_direct_strategies.py
@@ -122,6 +122,12 @@ def fn_ktest(*fnkwargs):
     (ds.text, {"min_size": 10, "max_size": 9}),
     (ds.text, {"alphabet": [1]}),
     (ds.text, {"alphabet": ["abc"]}),
+    (ds.text, {"alphabet": ds.just("abc")}),
+    (ds.text, {"alphabet": ds.sampled_from(["abc", "def"])}),
+    (ds.text, {"alphabet": ds.just(123)}),
+    (ds.text, {"alphabet": ds.sampled_from([123, 456])}),
+    (ds.text, {"alphabet": ds.builds(lambda: "abc")}),
+    (ds.text, {"alphabet": ds.builds(lambda: 123)}),
     (ds.binary, {"min_size": 10, "max_size": 9}),
     (ds.floats, {"min_value": math.nan}),
     (ds.floats, {"min_value": "0"}),
@@ -247,7 +253,9 @@ def test_validates_keyword_arguments(fn, kwargs):
     (ds.text, {"alphabet": "abc"}),
     (ds.text, {"alphabet": set("abc")}),
     (ds.text, {"alphabet": ""}),
+    (ds.text, {"alphabet": ds.just("a")}),
     (ds.text, {"alphabet": ds.sampled_from("abc")}),
+    (ds.text, {"alphabet": ds.builds(lambda: "a")}),
     (ds.characters, {"whitelist_categories": ["N"]}),
     (ds.characters, {"blacklist_categories": []}),
     (ds.ip_addresses, {}),
diff --git a/hypothesis-python/tests/cover/test_error_in_draw.py b/hypothesis-python/tests/cover/test_error_in_draw.py
index e00d0c789..08fb8fe98 100644
--- a/hypothesis-python/tests/cover/test_error_in_draw.py
+++ b/hypothesis-python/tests/cover/test_error_in_draw.py
@@ -11,6 +11,7 @@
 import pytest
 
 from hypothesis import given, strategies as st
+from hypothesis.errors import HypothesisWarning
 
 
 def test_error_is_in_finally():
@@ -25,3 +26,13 @@ def test_error_is_in_finally():
         test()
 
     assert "[0, 1, -1]" in "\n".join(err.value.__notes__)
+
+
+@given(st.data())
+def test_warns_on_bool_strategy(data):
+    with pytest.warns(
+        HypothesisWarning,
+        match=r"bool\(.+\) is always True, did you mean to draw a value\?",
+    ):
+        if st.booleans():  # 'forgot' to draw from the strategy
+            pass
diff --git a/hypothesis-python/tests/cover/test_escalation.py b/hypothesis-python/tests/cover/test_escalation.py
index 575508982..e8ad8623f 100644
--- a/hypothesis-python/tests/cover/test_escalation.py
+++ b/hypothesis-python/tests/cover/test_escalation.py
@@ -70,3 +70,7 @@ def test_multiplefailures_deprecation():
     with pytest.warns(errors.HypothesisDeprecationWarning):
         exc = errors.MultipleFailures
     assert exc is BaseExceptionGroup
+
+
+def test_handles_null_traceback():
+    esc.get_interesting_origin(Exception())
diff --git a/hypothesis-python/tests/cover/test_example.py b/hypothesis-python/tests/cover/test_example.py
index dc272ad9c..493aba92d 100644
--- a/hypothesis-python/tests/cover/test_example.py
+++ b/hypothesis-python/tests/cover/test_example.py
@@ -18,6 +18,7 @@ import pytest
 from hypothesis import example, find, given, strategies as st
 from hypothesis.errors import (
     HypothesisException,
+    InvalidArgument,
     NonInteractiveExampleWarning,
     Unsatisfiable,
 )
@@ -84,3 +85,22 @@ def test_interactive_example_does_not_emit_warning():
     child.sendline("from hypothesis.strategies import none")
     child.sendline("none().example()")
     child.sendline("quit(code=0)")
+
+
+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
+    return decorator
+
+
+@identity(example(False).via("Manually specified"))
+@given(st.booleans())
+def test_ok_example_via(x):
+    pass
+
+
+def test_invalid_example_via():
+    with pytest.raises(InvalidArgument):
+        example(x=False).via(100)  # not a string!
+    with pytest.raises(TypeError):
+        example(x=False).via("abc", "def")  # too many args
diff --git a/hypothesis-python/tests/cover/test_lookup_py38.py b/hypothesis-python/tests/cover/test_lookup_py38.py
index ac350c643..5b6538999 100644
--- a/hypothesis-python/tests/cover/test_lookup_py38.py
+++ b/hypothesis-python/tests/cover/test_lookup_py38.py
@@ -12,6 +12,7 @@ import dataclasses
 import re
 import sys
 import typing
+from types import SimpleNamespace
 
 import pytest
 
@@ -222,3 +223,29 @@ def test_example_argument_validation():
         ),
     ):
         example(x=None)(has_posonly_args)(1)
+
+
+class FooProtocol(typing.Protocol):
+    def frozzle(self, x):
+        pass
+
+
+class BarProtocol(typing.Protocol):
+    def bazzle(self, y):
+        pass
+
+
+@given(st.data())
+def test_can_resolve_registered_protocol(data):
+    with temp_registered(
+        FooProtocol,
+        st.builds(SimpleNamespace, frozzle=st.functions(like=lambda x: ...)),
+    ):
+        obj = data.draw(st.from_type(FooProtocol))
+    assert obj.frozzle(x=1) is None
+
+
+def test_cannot_resolve_un_registered_protocol():
+    msg = "Instance and class checks can only be used with @runtime_checkable protocols"
+    with pytest.raises(TypeError, match=msg):
+        st.from_type(BarProtocol).example()
diff --git a/hypothesis-python/tests/cover/test_phases.py b/hypothesis-python/tests/cover/test_phases.py
index ca1e9b2b9..8ccca601e 100644
--- a/hypothesis-python/tests/cover/test_phases.py
+++ b/hypothesis-python/tests/cover/test_phases.py
@@ -11,6 +11,7 @@
 import pytest
 
 from hypothesis import Phase, example, given, settings, strategies as st
+from hypothesis._settings import all_settings
 from hypothesis.database import ExampleDatabase, InMemoryExampleDatabase
 from hypothesis.errors import InvalidArgument
 
@@ -47,7 +48,7 @@ def test_sorts_and_dedupes_phases(arg, expected):
 
 
 def test_phases_default_to_all_except_explain():
-    assert settings().phases + (Phase.explain,) == tuple(Phase)
+    assert all_settings["phases"].default + (Phase.explain,) == tuple(Phase)
 
 
 def test_does_not_reuse_saved_examples_if_reuse_not_in_phases():
diff --git a/hypothesis-python/tests/cover/test_random_module.py b/hypothesis-python/tests/cover/test_random_module.py
index 83ed253a3..f8979db83 100644
--- a/hypothesis-python/tests/cover/test_random_module.py
+++ b/hypothesis-python/tests/cover/test_random_module.py
@@ -14,7 +14,7 @@ import random
 import pytest
 
 from hypothesis import core, find, given, register_random, strategies as st
-from hypothesis.errors import InvalidArgument
+from hypothesis.errors import HypothesisWarning, InvalidArgument
 from hypothesis.internal import entropy
 from hypothesis.internal.compat import PYPY
 from hypothesis.internal.entropy import deterministic_PRNG
@@ -54,6 +54,9 @@ def test_cannot_register_non_Random():
         register_random("not a Random instance")
 
 
+@pytest.mark.filterwarnings(
+    "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()
     n_registered = len(entropy.RANDOMS_TO_MANAGE)
@@ -144,6 +147,9 @@ def test_find_does_not_pollute_state():
         assert state_a2 != state_b2
 
 
+@pytest.mark.filterwarnings(
+    "ignore:It looks like `register_random` was passed an object that could be garbage collected"
+)
 def test_evil_prng_registration_nonsense():
     gc_on_pypy()
     n_registered = len(entropy.RANDOMS_TO_MANAGE)
@@ -172,3 +178,57 @@ def test_evil_prng_registration_nonsense():
     # Implicit check, no exception was raised in __exit__
     assert r2.getstate() == s2, "reset previously registered random state"
     assert r3.getstate() == s4, "retained state when registered within the context"
+
+
+@pytest.mark.skipif(
+    PYPY, reason="We can't guard against bad no-reference patterns in pypy."
+)
+def test_passing_unreferenced_instance_raises():
+    with pytest.raises(ReferenceError):
+        register_random(random.Random(0))
+
+
+@pytest.mark.skipif(
+    PYPY, reason="We can't guard against bad no-reference patterns in pypy."
+)
+def test_passing_unreferenced_instance_within_function_scope_raises():
+    def f():
+        register_random(random.Random(0))
+
+    with pytest.raises(ReferenceError):
+        f()
+
+
+@pytest.mark.skipif(
+    PYPY, reason="We can't guard against bad no-reference patterns in pypy."
+)
+def test_passing_referenced_instance_within_function_scope_warns():
+    def f():
+        r = random.Random(0)
+        register_random(r)
+
+    with pytest.warns(
+        HypothesisWarning,
+        match="It looks like `register_random` was passed an object that could be"
+        " garbage collected",
+    ):
+        f()
+
+
+@pytest.mark.filterwarnings(
+    "ignore:It looks like `register_random` was passed an object that could be garbage collected"
+)
+@pytest.mark.skipif(
+    PYPY, reason="We can't guard against bad no-reference patterns in pypy."
+)
+def test_register_random_within_nested_function_scope():
+    n_registered = len(entropy.RANDOMS_TO_MANAGE)
+
+    def f():
+        r = random.Random()
+        register_random(r)
+        assert len(entropy.RANDOMS_TO_MANAGE) == n_registered + 1
+
+    f()
+    gc_on_pypy()
+    assert len(entropy.RANDOMS_TO_MANAGE) == n_registered
diff --git a/hypothesis-python/tests/cover/test_scrutineer.py b/hypothesis-python/tests/cover/test_scrutineer.py
deleted file mode 100644
index f2cfe2bd0..000000000
--- a/hypothesis-python/tests/cover/test_scrutineer.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# 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/.
-
-import re
-import sys
-
-import pytest
-
-from hypothesis.internal.compat import PYPY
-from hypothesis.internal.scrutineer import HAD_TRACE
-
-CODE_SAMPLE = """
-from hypothesis import Phase, given, settings, strategies as st
-
-@settings(phases=tuple(Phase), derandomize=True)
-@given(st.integers())
-def test_reports_branch_in_test(x):
-    if x > 10:
-        raise AssertionError  # BUG
-"""
-
-
-@pytest.mark.skipif(PYPY, reason="Tracing is slow under PyPy")
-def test_cannot_explain_message(testdir):
-    # Most of the explanation-related code can't be run under coverage, but
-    # what we can cover is the code that prints a message when the explanation
-    # tracer couldn't be installed.
-
-    no_tracer = sys.gettrace() is None
-    try:
-        if no_tracer:
-            sys.settrace(lambda frame, event, arg: None)
-        test_file = testdir.makepyfile(CODE_SAMPLE)
-        testdir.runpytest_inprocess(test_file, "--tb=native").stdout.re_match_lines(
-            [r"Explanation:", re.escape(HAD_TRACE)], consecutive=True
-        )
-    finally:
-        if no_tracer:
-            sys.settrace(None)
diff --git a/hypothesis-python/tests/cover/test_settings.py b/hypothesis-python/tests/cover/test_settings.py
index 19914c42c..e3ca7376c 100644
--- a/hypothesis-python/tests/cover/test_settings.py
+++ b/hypothesis-python/tests/cover/test_settings.py
@@ -464,7 +464,7 @@ def test_invalid_parent():
 
 
 def test_show_changed():
-    s = settings(max_examples=999, database=None)
+    s = settings(max_examples=999, database=None, phases=tuple(Phase)[:-1])
     assert s.show_changed() == "database=None, max_examples=999"
 
 
diff --git a/hypothesis-python/tests/cover/test_stateful.py b/hypothesis-python/tests/cover/test_stateful.py
index dbbb911f9..ce6f60b55 100644
--- a/hypothesis-python/tests/cover/test_stateful.py
+++ b/hypothesis-python/tests/cover/test_stateful.py
@@ -9,6 +9,7 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 import base64
+import sys
 from collections import defaultdict
 
 import pytest
@@ -19,6 +20,7 @@ from hypothesis import __version__, reproduce_failure, seed, settings as Setting
 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,
@@ -676,10 +678,7 @@ def test_invariant_present_in_falsifying_example():
     with pytest.raises(ValueError) as err:
         run_state_machine_as_test(BadRuleWithGoodInvariants)
 
-    result = "\n".join(err.value.__notes__)
-    assert (
-        result
-        == """
+    expected = """\
 Falsifying example:
 state = BadRuleWithGoodInvariants()
 state.invariant_1()
@@ -691,9 +690,19 @@ state.invariant_1()
 state.invariant_2()
 state.invariant_3()
 state.rule_1()
-state.teardown()
-""".strip()
-    )
+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])
+
+    assert expected == result
 
 
 def test_always_runs_at_least_one_step():
diff --git a/hypothesis-python/tests/cover/test_unittest.py b/hypothesis-python/tests/cover/test_unittest.py
index d914f7d2d..af638045a 100644
--- a/hypothesis-python/tests/cover/test_unittest.py
+++ b/hypothesis-python/tests/cover/test_unittest.py
@@ -51,10 +51,11 @@ class test_given_on_setUp_fails_health_check(unittest.TestCase):
 
 SUBTEST_SUITE = """
 import unittest
-from hypothesis import given, strategies as st
+from hypothesis import given, settings, strategies as st
 
 class MyTest(unittest.TestCase):
     @given(s=st.text())
+    @settings(deadline=None)
     def test_subtest(self, s):
         with self.subTest(text=s):
             self.assertIsInstance(s, str)
diff --git a/hypothesis-python/tests/cover/test_validation.py b/hypothesis-python/tests/cover/test_validation.py
index c7c57573c..ffc52e467 100644
--- a/hypothesis-python/tests/cover/test_validation.py
+++ b/hypothesis-python/tests/cover/test_validation.py
@@ -9,6 +9,7 @@
 # obtain one at https://mozilla.org/MPL/2.0/.
 
 import functools
+import sys
 
 import pytest
 
@@ -274,3 +275,13 @@ def test_check_strategy_might_suggest_sampled_from():
     with pytest.raises(InvalidArgument, match="such as st.sampled_from"):
         check_strategy_((1, 2, 3))
     check_strategy_(integers(), "passes for our custom coverage check")
+
+
+@pytest.mark.skipif(sys.version_info[:2] >= (3, 8), reason="only on 3.7")
+def test_explicit_error_from_array_api_before_py38():
+    with pytest.raises(
+        RuntimeError, match=r"The Array API standard requires Python 3\.8 or later"
+    ):
+        from hypothesis.extra import array_api
+
+        assert array_api  # avoid unused-name lint error
diff --git a/hypothesis-python/tests/ghostwriter/test_ghostwriter.py b/hypothesis-python/tests/ghostwriter/test_ghostwriter.py
index 9886b704a..93b13c2e2 100644
--- a/hypothesis-python/tests/ghostwriter/test_ghostwriter.py
+++ b/hypothesis-python/tests/ghostwriter/test_ghostwriter.py
@@ -37,6 +37,7 @@ import attr
 import click
 import pytest
 
+from hypothesis import assume
 from hypothesis.errors import InvalidArgument, Unsatisfiable
 from hypothesis.extra import cli, ghostwriter
 from hypothesis.internal.compat import BaseExceptionGroup
@@ -501,3 +502,7 @@ def test_obj_name(temp_script_file, temp_script_file_with_py_function):
     assert isinstance(
         cli.obj_name(str(temp_script_file_with_py_function)), FunctionType
     )
+
+
+def test_gets_public_location_not_impl_location():
+    assert ghostwriter._get_module(assume) == "hypothesis"  # not "hypothesis.control"
diff --git a/hypothesis-python/tests/nocover/test_scrutineer.py b/hypothesis-python/tests/nocover/test_scrutineer.py
index b24033a17..b0050320e 100644
--- a/hypothesis-python/tests/nocover/test_scrutineer.py
+++ b/hypothesis-python/tests/nocover/test_scrutineer.py
@@ -79,3 +79,25 @@ def test_no_explanations_if_deadline_exceeded(code, testdir):
     code = code.replace("AssertionError", "DeadlineExceeded(timedelta(), timedelta())")
     pytest_stdout, _ = get_reports(DEADLINE_PRELUDE + PRELUDE + code, testdir=testdir)
     assert "Explanation:" not in pytest_stdout
+
+
+NO_SHOW_CONTEXTLIB = """
+from contextlib import contextmanager
+from hypothesis import given, strategies as st, Phase, settings
+
+@contextmanager
+def ctx():
+    yield
+
+@settings(phases=list(Phase))
+@given(st.integers())
+def test(x):
+    with ctx():
+        assert x < 100
+"""
+
+
+@pytest.mark.skipif(PYPY, reason="Tracing is slow under PyPy")
+def test_skips_uninformative_locations(testdir):
+    pytest_stdout, _ = get_reports(NO_SHOW_CONTEXTLIB, testdir=testdir)
+    assert "Explanation:" not in pytest_stdout
diff --git a/hypothesis-python/tests/nocover/test_targeting.py b/hypothesis-python/tests/nocover/test_targeting.py
index 0fad74eb5..f90dda098 100644
--- a/hypothesis-python/tests/nocover/test_targeting.py
+++ b/hypothesis-python/tests/nocover/test_targeting.py
@@ -29,7 +29,7 @@ def test_threshold_problem(x):
 @pytest.mark.parametrize("multiple", [False, True])
 def test_reports_target_results(testdir, multiple):
     script = testdir.makepyfile(TESTSUITE.format("" if multiple else "# "))
-    result = testdir.runpytest(script, "--tb=native")
+    result = testdir.runpytest(script, "--tb=native", "-rN")
     out = "\n".join(result.stdout.lines)
     assert "Falsifying example" in out
     assert "x=101" in out
diff --git a/hypothesis-python/tests/nocover/test_type_lookup.py b/hypothesis-python/tests/nocover/test_type_lookup.py
index 038d45487..b5c2bfbd0 100644
--- a/hypothesis-python/tests/nocover/test_type_lookup.py
+++ b/hypothesis-python/tests/nocover/test_type_lookup.py
@@ -14,14 +14,13 @@ import pytest
 
 from hypothesis import strategies as st
 from hypothesis.errors import InvalidArgument
+from hypothesis.internal.compat import Concatenate, ParamSpec
 from hypothesis.strategies._internal.types import NON_RUNTIME_TYPES
 
 try:
-    from typing import Concatenate, ParamSpec, TypeGuard  # new in 3.10
+    from typing import TypeGuard  # new in 3.10
 except ImportError:
-    Concatenate = ParamSpec = TypeGuard = None
-
-# See also typing_extensions/test_backported_types.py
+    TypeGuard = None
 
 
 @pytest.mark.parametrize("non_runtime_type", NON_RUNTIME_TYPES)
diff --git a/hypothesis-python/tests/pandas/test_argument_validation.py b/hypothesis-python/tests/pandas/test_argument_validation.py
index 4d2d5d1ba..7980cefc4 100644
--- a/hypothesis-python/tests/pandas/test_argument_validation.py
+++ b/hypothesis-python/tests/pandas/test_argument_validation.py
@@ -65,6 +65,7 @@ BAD_ARGS = [
     e(pdst.indexes, elements="not a strategy"),
     e(pdst.indexes, elements=st.text(), dtype=float),
     e(pdst.indexes, elements=st.none(), dtype=int),
+    e(pdst.indexes, elements=st.integers(0, 10), dtype=st.sampled_from([int, float])),
     e(pdst.indexes, dtype=int, max_size=0, min_size=1),
     e(pdst.indexes, dtype=int, unique="true"),
     e(pdst.indexes, dtype=int, min_size="0"),
diff --git a/hypothesis-python/tests/pytest/test_reporting.py b/hypothesis-python/tests/pytest/test_reporting.py
index 7f39fbf83..a7cb77875 100644
--- a/hypothesis-python/tests/pytest/test_reporting.py
+++ b/hypothesis-python/tests/pytest/test_reporting.py
@@ -8,6 +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 pytest
+
 pytest_plugins = "pytester"
 
 
@@ -33,3 +35,24 @@ def test_runs_reporting_hook(testdir):
     assert "Captured stdout call" not in out
     assert "Falsifying example" in out
     assert result.ret != 0
+
+
+TEST_EXCEPTIONGROUP = """
+from hypothesis import given, strategies as st
+
+@given(x=st.booleans())
+def test_fuzz_sorted(x):
+    raise ValueError if x else TypeError
+"""
+
+
+@pytest.mark.parametrize("tb", ["auto", "long", "short", "native"])
+def test_no_missing_reports(testdir, tb):
+    script = testdir.makepyfile(TEST_EXCEPTIONGROUP)
+    result = testdir.runpytest(script, f"--tb={tb}")
+    out = "\n".join(result.stdout.lines)
+    # If the False case is missing, that means we're not printing exception info.
+    # See https://github.com/HypothesisWorks/hypothesis/issues/3430  With --tb=native,
+    # we should show the full ExceptionGroup with *both* errors.
+    assert "x=False" in out
+    assert "x=True" in out or tb != "native"
diff --git a/hypothesis-python/tests/pytest/test_seeding.py b/hypothesis-python/tests/pytest/test_seeding.py
index f009653e2..a31170cae 100644
--- a/hypothesis-python/tests/pytest/test_seeding.py
+++ b/hypothesis-python/tests/pytest/test_seeding.py
@@ -43,7 +43,7 @@ def test_runs_repeatably_when_seed_is_set(seed, testdir):
 
     results = [
         testdir.runpytest(
-            script, "--verbose", "--strict-markers", "--hypothesis-seed", str(seed)
+            script, "--verbose", "--strict-markers", f"--hypothesis-seed={seed}", "-rN"
         )
         for _ in range(2)
     ]
diff --git a/hypothesis-python/tests/pytest/test_skipping.py b/hypothesis-python/tests/pytest/test_skipping.py
index 0433fe970..af92d3aae 100644
--- a/hypothesis-python/tests/pytest/test_skipping.py
+++ b/hypothesis-python/tests/pytest/test_skipping.py
@@ -39,3 +39,25 @@ def test_no_falsifying_example_if_pytest_skip(testdir):
     )
     out = "\n".join(result.stdout.lines)
     assert "Falsifying example" not in out
+
+
+def test_issue_3453_regression(testdir):
+    """If ``pytest.skip() is called during a test, Hypothesis should not
+    continue running the test and shrink process, nor should it print anything
+    about falsifying examples."""
+    script = testdir.makepyfile(
+        """
+from hypothesis import example, given, strategies as st
+import pytest
+
+@given(value=st.none())
+@example("hello")
+@example("goodbye")
+def test_skip_on_first_skipping_example(value):
+    assert value is not None
+    assert value != "hello"  # queue up a non-skip error which must be discarded
+    pytest.skip()
+"""
+    )
+    result = testdir.runpytest(script, "--tb=native")
+    result.assert_outcomes(skipped=1)
diff --git a/hypothesis-python/tests/typing_extensions/test_backported_types.py b/hypothesis-python/tests/typing_extensions/test_backported_types.py
index 61f59b056..a6f3512de 100644
--- a/hypothesis-python/tests/typing_extensions/test_backported_types.py
+++ b/hypothesis-python/tests/typing_extensions/test_backported_types.py
@@ -31,7 +31,7 @@ from hypothesis.strategies._internal.types import NON_RUNTIME_TYPES
 
 from tests.common.debug import assert_all_examples, find_any
 
-# See also nocover/test_type_lookp.py
+# See also nocover/test_type_lookup.py
 
 
 @pytest.mark.parametrize("value", ["dog", b"goldfish", 42, 63.4, -80.5, False])


----

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/core.py b/hypothesis-python/src/hypothesis/core.py
index befea5dcf..124c12ad6 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(
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/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..c0732a53a 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 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
@@ -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
 
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..c05f52c48 100644
--- a/hypothesis-python/src/hypothesis/strategies/_internal/core.py
+++ b/hypothesis-python/src/hypothesis/strategies/_internal/core.py
@@ -125,7 +125,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 +902,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 +960,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)
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/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..d3ab4be10 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, 1)
 __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_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