Skip to content

Commit dfd0fc3

Browse files
authored
Merge pull request #4259 from tybug/typing
Add type hints to `strategies.py`
2 parents b668e47 + 841833f commit dfd0fc3

File tree

13 files changed

+310
-227
lines changed

13 files changed

+310
-227
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
RELEASE_TYPE: patch
2+
3+
Fixes a bug since around :ref:`version 6.124.4 <v6.124.4>` where we might have generated ``-0.0`` for ``st.floats(min_value=0.0)``, which is unsound.

hypothesis-python/src/hypothesis/control.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
import math
1313
import random
1414
from collections import defaultdict
15+
from collections.abc import Sequence
1516
from contextlib import contextmanager
16-
from typing import Any, NoReturn, Optional, Union
17+
from typing import Any, Callable, NoReturn, Optional, Union
1718
from weakref import WeakKeyDictionary
1819

1920
from hypothesis import Verbosity, settings
@@ -126,10 +127,15 @@ def deprecate_random_in_strategy(fmt, *args):
126127

127128

128129
class BuildContext:
129-
def __init__(self, data, *, is_final=False, close_on_capture=True):
130-
assert isinstance(data, ConjectureData)
130+
def __init__(
131+
self,
132+
data: ConjectureData,
133+
*,
134+
is_final: bool = False,
135+
close_on_capture: bool = True,
136+
) -> None:
131137
self.data = data
132-
self.tasks = []
138+
self.tasks: list[Callable[[], Any]] = []
133139
self.is_final = is_final
134140
self.close_on_capture = close_on_capture
135141
self.close_on_del = False
@@ -140,9 +146,17 @@ def __init__(self, data, *, is_final=False, close_on_capture=True):
140146
defaultdict(list)
141147
)
142148

143-
def record_call(self, obj, func, args, kwargs):
149+
def record_call(
150+
self,
151+
obj: object,
152+
func: object,
153+
args: Sequence[object],
154+
kwargs: dict[str, object],
155+
) -> None:
144156
self.known_object_printers[IDKey(obj)].append(
145-
lambda obj, p, cycle, *, _func=func: p.maybe_repr_known_object_as_call(
157+
# _func=func prevents mypy from inferring lambda type. Would need
158+
# paramspec I think - not worth it.
159+
lambda obj, p, cycle, *, _func=func: p.maybe_repr_known_object_as_call( # type: ignore
146160
obj, cycle, get_pretty_function_description(_func), args, kwargs
147161
)
148162
)

hypothesis-python/src/hypothesis/core.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -996,8 +996,9 @@ def run(data):
996996
except TypeError as e:
997997
# If we sampled from a sequence of strategies, AND failed with a
998998
# TypeError, *AND that exception mentions SearchStrategy*, add a note:
999-
if "SearchStrategy" in str(e) and hasattr(
1000-
data, "_sampled_from_all_strategies_elements_message"
999+
if (
1000+
"SearchStrategy" in str(e)
1001+
and data._sampled_from_all_strategies_elements_message is not None
10011002
):
10021003
msg, format_arg = data._sampled_from_all_strategies_elements_message
10031004
add_note(e, msg.format(format_arg))

hypothesis-python/src/hypothesis/internal/conjecture/data.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,9 @@ def __init__(
793793
self._observability_predicates: defaultdict = defaultdict(
794794
lambda: {"satisfied": 0, "unsatisfied": 0}
795795
)
796+
self._sampled_from_all_strategies_elements_message: Optional[
797+
tuple[str, object]
798+
] = None
796799

797800
self.expected_exception: Optional[BaseException] = None
798801
self.expected_traceback: Optional[str] = None
@@ -991,6 +994,8 @@ def draw_string(
991994
) -> str:
992995
assert forced is None or min_size <= len(forced) <= max_size
993996
assert min_size >= 0
997+
if len(intervals) == 0:
998+
assert min_size == 0
994999

9951000
kwargs: StringKWargs = self._pooled_kwargs(
9961001
"string",

hypothesis-python/src/hypothesis/internal/conjecture/providers.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,9 @@ def draw_float(
370370
clamped = result # pragma: no cover
371371
else:
372372
clamped = clamper(result)
373-
if clamped != result and not (math.isnan(result) and allow_nan):
373+
if float_to_int(clamped) != float_to_int(result) and not (
374+
math.isnan(result) and allow_nan
375+
):
374376
result = clamped
375377
else:
376378
result = nasty_floats[i - 1]
@@ -386,6 +388,9 @@ def draw_string(
386388
assert self._cd is not None
387389
assert self._cd._random is not None
388390

391+
if len(intervals) == 0:
392+
return ""
393+
389394
average_size = min(
390395
max(min_size * 2, min_size + 5),
391396
0.5 * (min_size + max_size),

hypothesis-python/src/hypothesis/internal/escalation.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,24 @@
1616
from functools import partial
1717
from inspect import getframeinfo
1818
from pathlib import Path
19-
from typing import NamedTuple, Optional
19+
from types import ModuleType
20+
from typing import Callable, NamedTuple, Optional
2021

2122
import hypothesis
2223
from hypothesis.errors import _Trimmable
2324
from hypothesis.internal.compat import BaseExceptionGroup
2425
from hypothesis.utils.dynamicvariables import DynamicVariable
2526

2627

27-
def belongs_to(package):
28-
if not hasattr(package, "__file__"): # pragma: no cover
28+
def belongs_to(package: ModuleType) -> Callable[[str], bool]:
29+
if getattr(package, "__file__", None) is None: # pragma: no cover
2930
return lambda filepath: False
3031

32+
assert package.__file__ is not None
3133
root = Path(package.__file__).resolve().parent
32-
cache = {str: {}, bytes: {}}
34+
cache: dict[type, dict[str, bool]] = {str: {}, bytes: {}}
3335

34-
def accept(filepath):
36+
def accept(filepath: str) -> bool:
3537
ftype = type(filepath)
3638
try:
3739
return cache[ftype][filepath]

hypothesis-python/src/hypothesis/stateful.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from functools import lru_cache
2323
from io import StringIO
2424
from time import perf_counter
25-
from typing import Any, Callable, ClassVar, Optional, Union, overload
25+
from typing import Any, Callable, ClassVar, Optional, TypeVar, Union, overload
2626
from unittest import TestCase
2727

2828
import attr
@@ -54,13 +54,13 @@
5454
from hypothesis.strategies._internal.featureflags import FeatureStrategy
5555
from hypothesis.strategies._internal.strategies import (
5656
Ex,
57-
Ex_Inv,
5857
OneOfStrategy,
5958
SearchStrategy,
6059
check_strategy,
6160
)
6261
from hypothesis.vendor.pretty import RepresentationPrinter
6362

63+
T = TypeVar("T")
6464
STATE_MACHINE_RUN_LABEL = cu.calc_label_from_name("another state machine step")
6565
SHOULD_CONTINUE_LABEL = cu.calc_label_from_name("should we continue drawing")
6666

@@ -591,9 +591,7 @@ def __iter__(self):
591591
return iter(self.values)
592592

593593

594-
# We need to use an invariant typevar here to avoid a mypy error, as covariant
595-
# typevars cannot be used as parameters.
596-
def multiple(*args: Ex_Inv) -> MultipleResults[Ex_Inv]:
594+
def multiple(*args: T) -> MultipleResults[T]:
597595
"""This function can be used to pass multiple results to the target(s) of
598596
a rule. Just use ``return multiple(result1, result2, ...)`` in your rule.
599597

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

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,6 @@
127127
from hypothesis.strategies._internal.shared import SharedStrategy
128128
from hypothesis.strategies._internal.strategies import (
129129
Ex,
130-
Ex_Inv,
131130
SampledFromStrategy,
132131
T,
133132
one_of,
@@ -360,16 +359,20 @@ def lists(
360359

361360
# UniqueSampledListStrategy offers a substantial performance improvement for
362361
# unique arrays with few possible elements, e.g. of eight-bit integer types.
362+
363+
# all of these type: ignores are for a mypy bug, which narrows `elements`
364+
# to Never. https://github.com/python/mypy/issues/16494
363365
if (
364366
isinstance(elements, IntegersStrategy)
365-
and None not in (elements.start, elements.end)
366-
and (elements.end - elements.start) <= 255
367+
and elements.start is not None # type: ignore
368+
and elements.end is not None # type: ignore
369+
and (elements.end - elements.start) <= 255 # type: ignore
367370
):
368371
elements = SampledFromStrategy(
369-
sorted(range(elements.start, elements.end + 1), key=abs)
370-
if elements.end < 0 or elements.start > 0
371-
else list(range(elements.end + 1))
372-
+ list(range(-1, elements.start - 1, -1))
372+
sorted(range(elements.start, elements.end + 1), key=abs) # type: ignore
373+
if elements.end < 0 or elements.start > 0 # type: ignore
374+
else list(range(elements.end + 1)) # type: ignore
375+
+ list(range(-1, elements.start - 1, -1)) # type: ignore
373376
)
374377

375378
if isinstance(elements, SampledFromStrategy):
@@ -1106,7 +1109,7 @@ def builds(
11061109

11071110
@cacheable
11081111
@defines_strategy(never_lazy=True)
1109-
def from_type(thing: type[Ex_Inv]) -> SearchStrategy[Ex_Inv]:
1112+
def from_type(thing: type[T]) -> SearchStrategy[T]:
11101113
"""Looks up the appropriate search strategy for the given type.
11111114
11121115
``from_type`` is used internally to fill in missing arguments to

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
Real = Union[int, float, Fraction, Decimal]
4848

4949

50-
class IntegersStrategy(SearchStrategy):
51-
def __init__(self, start, end):
50+
class IntegersStrategy(SearchStrategy[int]):
51+
def __init__(self, start: Optional[int], end: Optional[int]) -> None:
5252
assert isinstance(start, int) or start is None
5353
assert isinstance(end, int) or end is None
5454
assert start is None or end is None or start <= end

0 commit comments

Comments
 (0)