Skip to content

Commit f19e57c

Browse files
ROpdebeeZac-HD
andauthored
Add reusable type for @composite draw argument (#3069)
* Add reusable type for `@composite` draw argument * Make DrawFn a protocol on >= 3.8 * Change DrawFn `label` parameter type to object * Add test case for DrawFn type hints * Prevent `DrawFn` instantiation on < 3.8 Co-authored-by: Zac Hatfield-Dodds <[email protected]> * Condense `DrawFn` docs Co-authored-by: Zac Hatfield-Dodds <[email protected]> * Fix formatting * Add test for `DrawFn` * Update stateful docs Fixes #3071. Co-authored-by: Zac Hatfield-Dodds <[email protected]>
1 parent bf2c4be commit f19e57c

File tree

6 files changed

+63
-3
lines changed

6 files changed

+63
-3
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
RELEASE_TYPE: minor
2+
3+
This release adds the :class:`~hypothesis.strategies.DrawFn` type as a reusable
4+
type hint for the ``draw`` argument of
5+
:func:`@composite <hypothesis.strategies.composite>` functions.
6+
7+
Thanks to Ruben Opdebeeck for this contribution!

hypothesis-python/src/hypothesis/stateful.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,8 +518,8 @@ def _convert_targets(targets, target):
518518

519519

520520
def rule(*, targets=(), target=None, **kwargs):
521-
"""Decorator for RuleBasedStateMachine. Any name present in target or
522-
targets will define where the end result of this function should go. If
521+
"""Decorator for RuleBasedStateMachine. Any Bundle present in ``target`` or
522+
``targets`` will define where the end result of this function should go. If
523523
both are empty then the end result will be discarded.
524524
525525
``target`` must be a Bundle, or if the result should go to multiple

hypothesis-python/src/hypothesis/strategies/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from hypothesis.strategies._internal.collections import tuples
1818
from hypothesis.strategies._internal.core import (
1919
DataObject,
20+
DrawFn,
2021
binary,
2122
booleans,
2223
builds,
@@ -82,6 +83,7 @@
8283
"decimals",
8384
"deferred",
8485
"dictionaries",
86+
"DrawFn",
8587
"emails",
8688
"fixed_dictionaries",
8789
"floats",
@@ -126,6 +128,7 @@ def _check_exports(_public):
126128
# @declares_strategy.
127129
exported_strategies = set(__all__) - {
128130
"DataObject",
131+
"DrawFn",
129132
"SearchStrategy",
130133
"composite",
131134
"register_type_strategy",

hypothesis-python/src/hypothesis/strategies/_internal/core.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from decimal import Context, Decimal, localcontext
2525
from fractions import Fraction
2626
from functools import reduce
27-
from inspect import Parameter, getfullargspec, isabstract, isclass, signature
27+
from inspect import Parameter, Signature, getfullargspec, isabstract, isclass, signature
2828
from types import FunctionType
2929
from typing import (
3030
Any,
@@ -111,6 +111,11 @@
111111
from hypothesis.strategies._internal.utils import cacheable, defines_strategy
112112
from hypothesis.utils.conventions import InferType, infer, not_set
113113

114+
try:
115+
from typing import Protocol
116+
except ImportError: # < py3.8
117+
Protocol = object # type: ignore[assignment]
118+
114119
UniqueBy = Union[Callable[[Ex], Hashable], Tuple[Callable[[Ex], Hashable], ...]]
115120

116121

@@ -1404,6 +1409,33 @@ def calc_label(self):
14041409
return calc_label_from_cls(self.definition)
14051410

14061411

1412+
class DrawFn(Protocol):
1413+
"""This type only exists so that you can write type hints for functions
1414+
decorated with :func:`@composite <hypothesis.strategies.composite>`.
1415+
1416+
.. code-block:: python
1417+
1418+
@composite
1419+
def list_and_index(draw: DrawFn) -> Tuple[int, str]:
1420+
i = draw(integers()) # type inferred as 'int'
1421+
s = draw(text()) # type inferred as 'str'
1422+
1423+
"""
1424+
1425+
def __init__(self):
1426+
raise TypeError("Protocols cannot be instantiated") # pragma: no cover
1427+
1428+
# On Python 3.8+, Protocol overrides our signature for __init__,
1429+
# so we override it right back to make the docs look nice.
1430+
__signature__: Signature = Signature(parameters=[])
1431+
1432+
# We define this as a callback protocol because a simple typing.Callable is
1433+
# insufficient to fully represent the interface, due to the optional `label`
1434+
# parameter.
1435+
def __call__(self, strategy: SearchStrategy[Ex], label: object = None) -> Ex:
1436+
raise NotImplementedError
1437+
1438+
14071439
@cacheable
14081440
def composite(f: Callable[..., Ex]) -> Callable[..., SearchStrategy[Ex]]:
14091441
"""Defines a strategy that is built out of potentially arbitrarily many

hypothesis-python/tests/cover/test_composite.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,8 @@ def test_applying_composite_decorator_to_methods(data):
176176
x = data.draw(strategy)
177177
assert isinstance(x, int)
178178
assert 0 <= x <= 10
179+
180+
181+
def test_drawfn_cannot_be_instantiated():
182+
with pytest.raises(TypeError):
183+
st.DrawFn()

whole-repo-tests/test_type_hints.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,19 @@ def test_data_object_type_tracing(tmpdir):
109109
assert got == "int"
110110

111111

112+
def test_drawfn_type_tracing(tmpdir):
113+
f = tmpdir.join("check_mypy_on_st_drawfn.py")
114+
f.write(
115+
"from hypothesis.strategies import DrawFn, text\n"
116+
"def comp(draw: DrawFn) -> str:\n"
117+
" s = draw(text(), 123)\n"
118+
" reveal_type(s)\n"
119+
" return s\n"
120+
)
121+
got = get_mypy_analysed_type(str(f.realpath()), ...)
122+
assert got == "str"
123+
124+
112125
def test_settings_preserves_type(tmpdir):
113126
f = tmpdir.join("check_mypy_on_settings.py")
114127
f.write(

0 commit comments

Comments
 (0)