Skip to content

Commit 615d54f

Browse files
committed
Add stubs for hypothesis tests
These are stubs to be used for adding hypothesis (https://hypothesis.readthedocs.io/en/latest/) tests to the standard library. When the tests are run in an environment where `hypothesis` and its various dependencies are not installed, the stubs will turn any tests with examples into simple parameterized tests and any tests without examples are skipped.
1 parent c8c3956 commit 615d54f

File tree

4 files changed

+225
-0
lines changed

4 files changed

+225
-0
lines changed
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from enum import Enum
2+
import functools
3+
import unittest
4+
5+
__all__ = [
6+
"given",
7+
"example",
8+
"assume",
9+
"reject",
10+
"register_random",
11+
"strategies",
12+
"HealthCheck",
13+
"settings",
14+
"Verbosity",
15+
]
16+
17+
from . import strategies
18+
19+
20+
def given(*_args, **_kwargs):
21+
def decorator(f):
22+
if examples := getattr(f, "_examples", []):
23+
24+
@functools.wraps(f)
25+
def test_function(self):
26+
for example_args, example_kwargs in examples:
27+
with self.subTest(*example_args, **example_kwargs):
28+
f(self, *example_args, **example_kwargs)
29+
30+
else:
31+
# If we have found no examples, we must skip the test. If @example
32+
# is applied after @given, it will re-wrap the test to remove the
33+
# skip decorator.
34+
test_function = unittest.skip(
35+
"Hypothesis required for property test with no " +
36+
"specified examples"
37+
)(f)
38+
39+
test_function._given = True
40+
return test_function
41+
42+
return decorator
43+
44+
45+
def example(*args, **kwargs):
46+
if bool(args) == bool(kwargs):
47+
raise ValueError("Must specify exactly one of *args or **kwargs")
48+
49+
def decorator(f):
50+
base_func = getattr(f, "__wrapped__", f)
51+
if not hasattr(base_func, "_examples"):
52+
base_func._examples = []
53+
54+
base_func._examples.append((args, kwargs))
55+
56+
if getattr(f, "_given", False):
57+
# If the given decorator is below all the example decorators,
58+
# it would be erroneously skipped, so we need to re-wrap the new
59+
# base function.
60+
f = given()(base_func)
61+
62+
return f
63+
64+
return decorator
65+
66+
67+
def assume(condition):
68+
if not condition:
69+
raise unittest.SkipTest("Unsatisfied assumption")
70+
return True
71+
72+
73+
def reject():
74+
assume(False)
75+
76+
77+
def register_random(*args, **kwargs):
78+
pass # pragma: no cover
79+
80+
81+
def settings(*args, **kwargs):
82+
pass # pragma: nocover
83+
84+
85+
class HealthCheck(Enum):
86+
data_too_large = 1
87+
filter_too_much = 2
88+
too_slow = 3
89+
return_value = 5
90+
large_base_example = 7
91+
not_a_test_method = 8
92+
93+
@classmethod
94+
def all(cls):
95+
return list(cls)
96+
97+
98+
class Verbosity(Enum):
99+
quiet = 0
100+
normal = 1
101+
verbose = 2
102+
debug = 3
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Stub out only the subset of the interface that we actually use in our tests.
2+
class StubClass:
3+
def __init__(self, *args, **kwargs):
4+
super().__init__()
5+
self.__stub_args = args
6+
self.__stub_kwargs = kwargs
7+
8+
def __repr__(self):
9+
argstr = ", ".join(self.__stub_args)
10+
kwargstr = ", ".join(
11+
f"{kw}={val}" for kw, val in self.__stub_kwargs.items()
12+
)
13+
14+
in_parens = argstr
15+
if kwargstr:
16+
in_parens += ", " + kwargstr
17+
18+
return f"{self.__qualname__}({in_parens})"
19+
20+
21+
def stub_factory(klass, name, _seen={}):
22+
if (klass, name) not in _seen:
23+
24+
class Stub(klass):
25+
def __init__(self, *args, **kwargs):
26+
super().__init__()
27+
self.__stub_args = args
28+
self.__stub_kwargs = kwargs
29+
30+
Stub.__name__ = name
31+
Stub.__qualname__ = name
32+
_seen.setdefault((klass, name), Stub)
33+
34+
return _seen[(klass, name)]
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import functools
2+
3+
from ._helpers import StubClass, stub_factory
4+
5+
6+
class StubStrategy(StubClass):
7+
def map(self, pack):
8+
return self
9+
10+
def flatmap(self, expand):
11+
return self
12+
13+
def filter(self, condition):
14+
return self
15+
16+
def __or__(self, other):
17+
return self
18+
19+
20+
_STRATEGIES = {
21+
"binary",
22+
"booleans",
23+
"builds",
24+
"characters",
25+
"complex_numbers",
26+
"composite",
27+
"data",
28+
"dates",
29+
"datetimes",
30+
"decimals",
31+
"deferred",
32+
"dictionaries",
33+
"emails",
34+
"fixed_dictionaries",
35+
"floats",
36+
"fractions",
37+
"from_regex",
38+
"from_type",
39+
"frozensets",
40+
"functions" "integers",
41+
"iterables",
42+
"just",
43+
"lists",
44+
"none",
45+
"nothing",
46+
"one_of",
47+
"permutations",
48+
"random_module",
49+
"randoms",
50+
"recursive",
51+
"register_type_strategy",
52+
"runner",
53+
"sampled_from",
54+
"sets",
55+
"shared",
56+
"slices",
57+
"timedeltas",
58+
"times",
59+
"text",
60+
"tuples",
61+
"uuids",
62+
}
63+
64+
__all__ = sorted(_STRATEGIES)
65+
66+
67+
def composite(f):
68+
strategy = stub_factory(StubStrategy, f.__name__)
69+
70+
@functools.wraps(f)
71+
def inner(*args, **kwargs):
72+
return strategy(*args, **kwargs)
73+
74+
return inner
75+
76+
77+
def __getattr__(name):
78+
if name not in _STRATEGIES:
79+
raise AttributeError(f"Unknown attribute {name}")
80+
81+
return stub_factory(StubStrategy, f"hypothesis.strategies.{name}")
82+
83+
84+
def __dir__():
85+
return __all__

Lib/test/support/hypothesis_helper.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
try:
2+
import hypothesis
3+
except ImportError:
4+
from . import _hypothesis_stubs as hypothesis

0 commit comments

Comments
 (0)