From aeb677f1c3336df07bce0e381c272d5e2d40770c Mon Sep 17 00:00:00 2001 From: Paul Ganssle Date: Mon, 19 Oct 2026 16:40:42 -0400 Subject: [PATCH] gh-107862: Add stateful, timezones and ip_addresses to the Hypothesis stubs The stubs in test.support._hypothesis_stubs only cover the strategies and decorators the existing property tests happen to use, so a test that wants hypothesis.stateful, or the timezones() and ip_addresses() strategies, fails to import when Hypothesis is not installed. This adds those names. The stateful API has no equivalent of @example, so there is nothing for the stub to replay: a state machine's TestCase is skipped and run_state_machine_as_test raises SkipTest. The decorators are accepted so that state machine classes can be defined at import time. Real Hypothesis only exposes hypothesis.stateful once the submodule has been imported, so hypothesis_helper now imports it, making the attribute available in both modes. --- .../support/_hypothesis_stubs/__init__.py | 3 +- .../support/_hypothesis_stubs/stateful.py | 97 +++++++++++++++++++ .../support/_hypothesis_stubs/strategies.py | 3 + Lib/test/support/hypothesis_helper.py | 4 + ...-10-19-17-05-00.gh-issue-107862.qH3sTa.rst | 2 + 5 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 Lib/test/support/_hypothesis_stubs/stateful.py create mode 100644 Misc/NEWS.d/next/Tests/2026-10-19-17-05-00.gh-issue-107862.qH3sTa.rst diff --git a/Lib/test/support/_hypothesis_stubs/__init__.py b/Lib/test/support/_hypothesis_stubs/__init__.py index 6fa013b55b2ac46..e3cb54aad736d29 100644 --- a/Lib/test/support/_hypothesis_stubs/__init__.py +++ b/Lib/test/support/_hypothesis_stubs/__init__.py @@ -8,13 +8,14 @@ "assume", "reject", "register_random", + "stateful", "strategies", "HealthCheck", "settings", "Verbosity", ] -from . import strategies +from . import stateful, strategies def given(*_args, **_kwargs): diff --git a/Lib/test/support/_hypothesis_stubs/stateful.py b/Lib/test/support/_hypothesis_stubs/stateful.py new file mode 100644 index 000000000000000..53a550c5ccbad3b --- /dev/null +++ b/Lib/test/support/_hypothesis_stubs/stateful.py @@ -0,0 +1,97 @@ +"""Stubs for the subset of ``hypothesis.stateful`` used by the test suite. + +The stateful API has no equivalent of ``@example``, so without Hypothesis +there is nothing to replay: the ``TestCase`` generated for a state machine +is skipped, and ``run_state_machine_as_test`` raises ``unittest.SkipTest``. +The decorators and helpers are accepted so that state machine classes can +be defined at import time. +""" +import unittest + +from ._helpers import stub_factory +from .strategies import StubStrategy + +__all__ = [ + "Bundle", + "MultipleResults", + "RuleBasedStateMachine", + "consumes", + "initialize", + "invariant", + "multiple", + "precondition", + "rule", + "run_state_machine_as_test", +] + +_SKIP_REASON = "Hypothesis required for stateful test" + +Bundle = stub_factory(StubStrategy, "hypothesis.stateful.Bundle") + + +def consumes(bundle): + return bundle + + +class MultipleResults: + def __init__(self, *values): + self.values = values + + +def multiple(*args): + return MultipleResults(*args) + + +def _identity_operator(f): + return f + + +def _identity_decorator(*_args, **_kwargs): + return _identity_operator + + +rule = _identity_decorator +initialize = _identity_decorator +invariant = _identity_decorator +precondition = _identity_decorator + + +class _TestCaseProperty: + """Build (and cache) a skipped ``unittest.TestCase`` per state machine.""" + + def __init__(self): + self._cases = {} + + def __get__(self, obj, owner): + if owner not in self._cases: + + class StateMachineTestCase(unittest.TestCase): + settings = None + + def runTest(self): + raise unittest.SkipTest(_SKIP_REASON) + + StateMachineTestCase.__name__ = f"{owner.__name__}.TestCase" + StateMachineTestCase.__qualname__ = f"{owner.__qualname__}.TestCase" + self._cases[owner] = StateMachineTestCase + return self._cases[owner] + + +class RuleBasedStateMachine: + TestCase = _TestCaseProperty() + + def bundle(self, name): + return Bundle(name) + + def check_invariants(self, settings, output, runtimes): + pass + + def setup_state(self): + pass + + def teardown(self): + pass + + +def run_state_machine_as_test(state_machine_factory, *, settings=None): + raise unittest.SkipTest(_SKIP_REASON) diff --git a/Lib/test/support/_hypothesis_stubs/strategies.py b/Lib/test/support/_hypothesis_stubs/strategies.py index d2b885d41e16d0e..b58e36b41dbaf75 100644 --- a/Lib/test/support/_hypothesis_stubs/strategies.py +++ b/Lib/test/support/_hypothesis_stubs/strategies.py @@ -44,6 +44,7 @@ def __or__(self, other): "frozensets", "functions", "integers", + "ip_addresses", "iterables", "just", "lists", @@ -62,6 +63,8 @@ def __or__(self, other): "slices", "timedeltas", "times", + "timezone_keys", + "timezones", "text", "tuples", "uuids", diff --git a/Lib/test/support/hypothesis_helper.py b/Lib/test/support/hypothesis_helper.py index a99a4963ffecc94..6249ba43f06716a 100644 --- a/Lib/test/support/hypothesis_helper.py +++ b/Lib/test/support/hypothesis_helper.py @@ -5,6 +5,10 @@ except ImportError: from . import _hypothesis_stubs as hypothesis else: + # Real Hypothesis does not import this submodule on its own; do it here + # so that ``hypothesis.stateful`` is available in both modes. + import hypothesis.stateful + # Regrtest changes to use a tempdir as the working directory, so we have # to tell Hypothesis to use the original in order to persist the database. from test.support import has_socket_support diff --git a/Misc/NEWS.d/next/Tests/2026-10-19-17-05-00.gh-issue-107862.qH3sTa.rst b/Misc/NEWS.d/next/Tests/2026-10-19-17-05-00.gh-issue-107862.qH3sTa.rst new file mode 100644 index 000000000000000..624fd82d50041c1 --- /dev/null +++ b/Misc/NEWS.d/next/Tests/2026-10-19-17-05-00.gh-issue-107862.qH3sTa.rst @@ -0,0 +1,2 @@ +Add ``hypothesis.stateful`` and the ``timezones()`` and ``ip_addresses()`` +strategies to the Hypothesis stubs used when Hypothesis is not installed.