Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type hints to strategies.py #4259

Merged
merged 6 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
RELEASE_TYPE: patch

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.
26 changes: 20 additions & 6 deletions hypothesis-python/src/hypothesis/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
import math
import random
from collections import defaultdict
from collections.abc import Sequence
from contextlib import contextmanager
from typing import Any, NoReturn, Optional, Union
from typing import Any, Callable, NoReturn, Optional, Union
from weakref import WeakKeyDictionary

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


class BuildContext:
def __init__(self, data, *, is_final=False, close_on_capture=True):
assert isinstance(data, ConjectureData)
def __init__(
self,
data: ConjectureData,
*,
is_final: bool = False,
close_on_capture: bool = True,
) -> None:
self.data = data
self.tasks = []
self.tasks: list[Callable[[], Any]] = []
self.is_final = is_final
self.close_on_capture = close_on_capture
self.close_on_del = False
Expand All @@ -140,9 +146,17 @@ def __init__(self, data, *, is_final=False, close_on_capture=True):
defaultdict(list)
)

def record_call(self, obj, func, args, kwargs):
def record_call(
self,
obj: object,
func: object,
args: Sequence[object],
kwargs: dict[str, object],
) -> None:
self.known_object_printers[IDKey(obj)].append(
lambda obj, p, cycle, *, _func=func: p.maybe_repr_known_object_as_call(
# _func=func prevents mypy from inferring lambda type. Would need
# paramspec I think - not worth it.
lambda obj, p, cycle, *, _func=func: p.maybe_repr_known_object_as_call( # type: ignore
obj, cycle, get_pretty_function_description(_func), args, kwargs
)
)
Expand Down
5 changes: 3 additions & 2 deletions hypothesis-python/src/hypothesis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -996,8 +996,9 @@ def run(data):
except TypeError as e:
# If we sampled from a sequence of strategies, AND failed with a
# TypeError, *AND that exception mentions SearchStrategy*, add a note:
if "SearchStrategy" in str(e) and hasattr(
data, "_sampled_from_all_strategies_elements_message"
if (
"SearchStrategy" in str(e)
and data._sampled_from_all_strategies_elements_message is not None
):
msg, format_arg = data._sampled_from_all_strategies_elements_message
add_note(e, msg.format(format_arg))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,9 @@ def __init__(
self._observability_predicates: defaultdict = defaultdict(
lambda: {"satisfied": 0, "unsatisfied": 0}
)
self._sampled_from_all_strategies_elements_message: Optional[
tuple[str, object]
] = None

self.expected_exception: Optional[BaseException] = None
self.expected_traceback: Optional[str] = None
Expand Down Expand Up @@ -991,6 +994,8 @@ def draw_string(
) -> str:
assert forced is None or min_size <= len(forced) <= max_size
assert min_size >= 0
if len(intervals) == 0:
assert min_size == 0

kwargs: StringKWargs = self._pooled_kwargs(
"string",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,9 @@ def draw_float(
clamped = result # pragma: no cover
else:
clamped = clamper(result)
if clamped != result and not (math.isnan(result) and allow_nan):
if float_to_int(clamped) != float_to_int(result) and not (
math.isnan(result) and allow_nan
):
result = clamped
else:
result = nasty_floats[i - 1]
Expand All @@ -386,6 +388,9 @@ def draw_string(
assert self._cd is not None
assert self._cd._random is not None

if len(intervals) == 0:
return ""

average_size = min(
max(min_size * 2, min_size + 5),
0.5 * (min_size + max_size),
Expand Down
12 changes: 7 additions & 5 deletions hypothesis-python/src/hypothesis/internal/escalation.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,24 @@
from functools import partial
from inspect import getframeinfo
from pathlib import Path
from typing import NamedTuple, Optional
from types import ModuleType
from typing import Callable, NamedTuple, Optional

import hypothesis
from hypothesis.errors import _Trimmable
from hypothesis.internal.compat import BaseExceptionGroup
from hypothesis.utils.dynamicvariables import DynamicVariable


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

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

def accept(filepath):
def accept(filepath: str) -> bool:
ftype = type(filepath)
try:
return cache[ftype][filepath]
Expand Down
8 changes: 3 additions & 5 deletions hypothesis-python/src/hypothesis/stateful.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from functools import lru_cache
from io import StringIO
from time import perf_counter
from typing import Any, Callable, ClassVar, Optional, Union, overload
from typing import Any, Callable, ClassVar, Optional, TypeVar, Union, overload
from unittest import TestCase

import attr
Expand Down Expand Up @@ -54,13 +54,13 @@
from hypothesis.strategies._internal.featureflags import FeatureStrategy
from hypothesis.strategies._internal.strategies import (
Ex,
Ex_Inv,
OneOfStrategy,
SearchStrategy,
check_strategy,
)
from hypothesis.vendor.pretty import RepresentationPrinter

T = TypeVar("T")
STATE_MACHINE_RUN_LABEL = cu.calc_label_from_name("another state machine step")
SHOULD_CONTINUE_LABEL = cu.calc_label_from_name("should we continue drawing")

Expand Down Expand Up @@ -591,9 +591,7 @@ def __iter__(self):
return iter(self.values)


# We need to use an invariant typevar here to avoid a mypy error, as covariant
# typevars cannot be used as parameters.
def multiple(*args: Ex_Inv) -> MultipleResults[Ex_Inv]:
Zac-HD marked this conversation as resolved.
Show resolved Hide resolved
def multiple(*args: T) -> MultipleResults[T]:
"""This function can be used to pass multiple results to the target(s) of
a rule. Just use ``return multiple(result1, result2, ...)`` in your rule.

Expand Down
19 changes: 11 additions & 8 deletions hypothesis-python/src/hypothesis/strategies/_internal/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,6 @@
from hypothesis.strategies._internal.shared import SharedStrategy
from hypothesis.strategies._internal.strategies import (
Ex,
Ex_Inv,
SampledFromStrategy,
T,
one_of,
Expand Down Expand Up @@ -360,16 +359,20 @@ def lists(

# UniqueSampledListStrategy offers a substantial performance improvement for
# unique arrays with few possible elements, e.g. of eight-bit integer types.

# all of these type: ignores are for a mypy bug, which narrows `elements`
# to Never. https://github.com/python/mypy/issues/16494
if (
isinstance(elements, IntegersStrategy)
and None not in (elements.start, elements.end)
and (elements.end - elements.start) <= 255
and elements.start is not None # type: ignore
and elements.end is not None # type: ignore
and (elements.end - elements.start) <= 255 # type: ignore
):
elements = SampledFromStrategy(
sorted(range(elements.start, elements.end + 1), key=abs)
if elements.end < 0 or elements.start > 0
else list(range(elements.end + 1))
+ list(range(-1, elements.start - 1, -1))
sorted(range(elements.start, elements.end + 1), key=abs) # type: ignore
if elements.end < 0 or elements.start > 0 # type: ignore
else list(range(elements.end + 1)) # type: ignore
+ list(range(-1, elements.start - 1, -1)) # type: ignore
)

if isinstance(elements, SampledFromStrategy):
Expand Down Expand Up @@ -1106,7 +1109,7 @@ def builds(

@cacheable
@defines_strategy(never_lazy=True)
def from_type(thing: type[Ex_Inv]) -> SearchStrategy[Ex_Inv]:
def from_type(thing: type[T]) -> SearchStrategy[T]:
"""Looks up the appropriate search strategy for the given type.

``from_type`` is used internally to fill in missing arguments to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@
Real = Union[int, float, Fraction, Decimal]


class IntegersStrategy(SearchStrategy):
def __init__(self, start, end):
class IntegersStrategy(SearchStrategy[int]):
def __init__(self, start: Optional[int], end: Optional[int]) -> None:
assert isinstance(start, int) or start is None
assert isinstance(end, int) or end is None
assert start is None or end is None or start <= end
Expand Down
Loading