Skip to content

Commit b88d3ad

Browse files
committed
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. The stubs had no tests of their own, so this also adds test_hypothesis_stubs, which checks the replay and skip behavior directly and runs the API-shape checks against the real package when it is importable.
1 parent baec764 commit b88d3ad

6 files changed

Lines changed: 270 additions & 1 deletion

File tree

‎Lib/test/support/_hypothesis_stubs/__init__.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,14 @@
88
"assume",
99
"reject",
1010
"register_random",
11+
"stateful",
1112
"strategies",
1213
"HealthCheck",
1314
"settings",
1415
"Verbosity",
1516
]
1617

17-
from . import strategies
18+
from . import stateful, strategies
1819

1920

2021
def given(*_args, **_kwargs):
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
"""Stubs for the subset of ``hypothesis.stateful`` used by the test suite.
2+
3+
The stateful API has no equivalent of ``@example``, so without Hypothesis
4+
there is nothing to replay: the ``TestCase`` generated for a state machine
5+
is skipped, and ``run_state_machine_as_test`` raises ``unittest.SkipTest``.
6+
The decorators and helpers are accepted so that state machine classes can
7+
be defined at import time.
8+
"""
9+
import unittest
10+
11+
from ._helpers import stub_factory
12+
from .strategies import StubStrategy
13+
14+
__all__ = [
15+
"Bundle",
16+
"MultipleResults",
17+
"RuleBasedStateMachine",
18+
"consumes",
19+
"initialize",
20+
"invariant",
21+
"multiple",
22+
"precondition",
23+
"rule",
24+
"run_state_machine_as_test",
25+
]
26+
27+
_SKIP_REASON = "Hypothesis required for stateful test"
28+
29+
Bundle = stub_factory(StubStrategy, "hypothesis.stateful.Bundle")
30+
31+
32+
def consumes(bundle):
33+
return bundle
34+
35+
36+
class MultipleResults:
37+
def __init__(self, *values):
38+
self.values = values
39+
40+
41+
def multiple(*args):
42+
return MultipleResults(*args)
43+
44+
45+
def _identity_decorator(*_args, **_kwargs):
46+
return lambda f: f
47+
48+
49+
rule = _identity_decorator
50+
initialize = _identity_decorator
51+
invariant = _identity_decorator
52+
53+
54+
def precondition(precond):
55+
return lambda f: f
56+
57+
58+
class _TestCaseProperty:
59+
"""Build (and cache) a skipped ``unittest.TestCase`` per state machine."""
60+
61+
def __init__(self):
62+
self._cases = {}
63+
64+
def __get__(self, obj, owner):
65+
if owner not in self._cases:
66+
67+
class StateMachineTestCase(unittest.TestCase):
68+
settings = None
69+
70+
def runTest(self):
71+
raise unittest.SkipTest(_SKIP_REASON)
72+
73+
StateMachineTestCase.__name__ = f"{owner.__name__}.TestCase"
74+
StateMachineTestCase.__qualname__ = f"{owner.__qualname__}.TestCase"
75+
self._cases[owner] = StateMachineTestCase
76+
return self._cases[owner]
77+
78+
79+
class RuleBasedStateMachine:
80+
TestCase = _TestCaseProperty()
81+
82+
def bundle(self, name):
83+
return Bundle(name)
84+
85+
def check_invariants(self, settings, output, runtimes):
86+
pass
87+
88+
def setup_state(self):
89+
pass
90+
91+
def teardown(self):
92+
pass
93+
94+
95+
def run_state_machine_as_test(state_machine_factory, *, settings=None):
96+
raise unittest.SkipTest(_SKIP_REASON)

‎Lib/test/support/_hypothesis_stubs/strategies.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def __or__(self, other):
4444
"frozensets",
4545
"functions",
4646
"integers",
47+
"ip_addresses",
4748
"iterables",
4849
"just",
4950
"lists",
@@ -62,6 +63,8 @@ def __or__(self, other):
6263
"slices",
6364
"timedeltas",
6465
"times",
66+
"timezone_keys",
67+
"timezones",
6568
"text",
6669
"tuples",
6770
"uuids",

‎Lib/test/support/hypothesis_helper.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
try:
44
import hypothesis
5+
# Real Hypothesis does not import this submodule on its own; do it here
6+
# so that ``hypothesis.stateful`` is available in both modes.
7+
import hypothesis.stateful
58
except ImportError:
69
from . import _hypothesis_stubs as hypothesis
710
else:

‎Lib/test/test_hypothesis_stubs.py‎

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""Tests for the Hypothesis stubs used when Hypothesis is not installed.
2+
3+
The stubs are tested directly, so these tests run regardless of whether
4+
Hypothesis is available. The API-shape tests also run against the real
5+
package when it is importable, to catch the stubs drifting from it.
6+
"""
7+
import unittest
8+
9+
from test.support import _hypothesis_stubs as stubs
10+
from test.support import hypothesis_helper
11+
12+
try:
13+
import hypothesis
14+
import hypothesis.stateful
15+
except ImportError:
16+
hypothesis = None
17+
18+
19+
def _run(case_class):
20+
"""Run every test in ``case_class`` and return the ``TestResult``."""
21+
result = unittest.TestResult()
22+
unittest.defaultTestLoader.loadTestsFromTestCase(case_class).run(result)
23+
return result
24+
25+
26+
class APIShapeMixin:
27+
"""Checks that hold for both the stubs and the real package."""
28+
29+
module = None
30+
31+
def test_new_strategies_exist(self):
32+
st = self.module.strategies
33+
for name in ("ip_addresses", "timezone_keys", "timezones"):
34+
with self.subTest(name=name):
35+
self.assertTrue(callable(getattr(st, name)))
36+
self.assertIn(name, dir(st))
37+
38+
def test_stateful_names_exist(self):
39+
stateful = self.module.stateful
40+
for name in (
41+
"Bundle",
42+
"RuleBasedStateMachine",
43+
"consumes",
44+
"initialize",
45+
"invariant",
46+
"multiple",
47+
"precondition",
48+
"rule",
49+
"run_state_machine_as_test",
50+
):
51+
with self.subTest(name=name):
52+
self.assertTrue(hasattr(stateful, name))
53+
54+
def test_state_machine_definition(self):
55+
stateful = self.module.stateful
56+
st = self.module.strategies
57+
58+
class Machine(stateful.RuleBasedStateMachine):
59+
values = stateful.Bundle("values")
60+
61+
@stateful.initialize(target=values)
62+
def start(self):
63+
return stateful.multiple(1, 2)
64+
65+
@stateful.rule(target=values, x=st.integers())
66+
def add(self, x):
67+
return x
68+
69+
@stateful.precondition(lambda self: True)
70+
@stateful.rule(x=stateful.consumes(values))
71+
def remove(self, x):
72+
pass
73+
74+
@stateful.invariant()
75+
def check(self):
76+
pass
77+
78+
self.assertTrue(issubclass(Machine.TestCase, unittest.TestCase))
79+
self.assertIs(Machine.TestCase, Machine.TestCase)
80+
self.assertTrue(hasattr(Machine.TestCase, "runTest"))
81+
self.assertEqual(stateful.multiple(1, 2).values, (1, 2))
82+
83+
84+
class StubAPIShapeTests(APIShapeMixin, unittest.TestCase):
85+
module = stubs
86+
87+
88+
@unittest.skipIf(hypothesis is None, "requires Hypothesis")
89+
class RealAPIShapeTests(APIShapeMixin, unittest.TestCase):
90+
module = hypothesis
91+
92+
93+
class HelperTests(unittest.TestCase):
94+
def test_helper_exposes_stateful(self):
95+
# Real Hypothesis only has this attribute once the submodule has
96+
# been imported, so the helper must do that in both modes.
97+
self.assertTrue(hasattr(hypothesis_helper.hypothesis, "stateful"))
98+
self.assertTrue(hasattr(hypothesis_helper.hypothesis, "strategies"))
99+
100+
101+
class StubBehaviorTests(unittest.TestCase):
102+
def test_given_replays_examples(self):
103+
seen = []
104+
105+
class Case(unittest.TestCase):
106+
@stubs.example(x=1)
107+
@stubs.example(x=2)
108+
@stubs.given(x=stubs.strategies.integers())
109+
def test_prop(self, x):
110+
seen.append(x)
111+
112+
result = _run(Case)
113+
self.assertEqual(result.testsRun, 1)
114+
self.assertEqual(result.failures, [])
115+
self.assertEqual(result.errors, [])
116+
self.assertEqual(sorted(seen), [1, 2])
117+
118+
def test_given_without_examples_skips(self):
119+
class Case(unittest.TestCase):
120+
@stubs.given(x=stubs.strategies.integers())
121+
def test_prop(self, x):
122+
self.fail("should not run")
123+
124+
result = _run(Case)
125+
self.assertEqual(result.testsRun, 1)
126+
self.assertEqual(len(result.skipped), 1)
127+
128+
def test_strategy_repr_names_the_strategy(self):
129+
self.assertEqual(
130+
repr(stubs.strategies.timezones()),
131+
"hypothesis.strategies.timezones()",
132+
)
133+
self.assertEqual(
134+
repr(stubs.strategies.ip_addresses(v=4).map(str)),
135+
"hypothesis.strategies.ip_addresses().map(str)",
136+
)
137+
138+
def test_unknown_strategy_raises(self):
139+
with self.assertRaises(AttributeError):
140+
stubs.strategies.no_such_strategy
141+
142+
def test_state_machine_test_case_is_skipped(self):
143+
class Machine(stubs.stateful.RuleBasedStateMachine):
144+
@stubs.stateful.rule()
145+
def step(self):
146+
raise AssertionError("rules must not run without Hypothesis")
147+
148+
result = _run(Machine.TestCase)
149+
self.assertEqual(result.testsRun, 1)
150+
self.assertEqual(len(result.skipped), 1)
151+
self.assertEqual(result.failures, [])
152+
self.assertEqual(result.errors, [])
153+
154+
def test_run_state_machine_as_test_skips(self):
155+
class Machine(stubs.stateful.RuleBasedStateMachine):
156+
pass
157+
158+
with self.assertRaises(unittest.SkipTest):
159+
stubs.stateful.run_state_machine_as_test(Machine)
160+
161+
162+
if __name__ == "__main__":
163+
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add ``hypothesis.stateful`` and the ``timezones()`` and ``ip_addresses()``
2+
strategies to the Hypothesis stubs used when Hypothesis is not installed, and
3+
add tests for the stubs.

0 commit comments

Comments
 (0)