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

Use ParamSpec to precisely annotate @st.composite and st.functions() #3396

Merged
merged 3 commits into from
Jul 4, 2022
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
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ their individual contributions.
* `Felix Sheldon <https://www.github.com/darkpaw>`_
* `Florian Bruhin <https://www.github.com/The-Compiler>`_
* `follower <https://www.github.com/follower>`_
* `Gabe Joseph <https://github.com/gjoseph92>`_
* `Gary Donovan <https://www.github.com/garyd203>`_
* `George Macon <https://www.github.com/gmacon>`_
* `Glenn Lehman <https://www.github.com/glnnlhmn>`_
Expand Down
7 changes: 7 additions & 0 deletions hypothesis-python/RELEASE.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
RELEASE_TYPE: minor

This release uses :pep:`612` :obj:`python:typing.ParamSpec` (or the
:pypi:`typing_extensions` backport) to express the first-argument-removing
behaviour of :func:`@st.composite <hypothesis.strategies.composite>`
and signature-preservation of :func:`~hypothesis.strategies.functions`
to IDEs, editor plugins, and static type checkers such as :pypi:`mypy`.
6 changes: 3 additions & 3 deletions hypothesis-python/src/hypothesis/_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,19 +148,19 @@ def __init__(
# The intended use is "like **kwargs, but more tractable for tooling".
max_examples: int = not_set, # type: ignore
derandomize: bool = not_set, # type: ignore
database: Union[None, "ExampleDatabase"] = not_set, # type: ignore
database: Optional["ExampleDatabase"] = not_set, # type: ignore
verbosity: "Verbosity" = not_set, # type: ignore
phases: Collection["Phase"] = not_set, # type: ignore
stateful_step_count: int = not_set, # type: ignore
report_multiple_bugs: bool = not_set, # type: ignore
suppress_health_check: Collection["HealthCheck"] = not_set, # type: ignore
deadline: Union[None, int, float, datetime.timedelta] = not_set, # type: ignore
deadline: Union[int, float, datetime.timedelta, None] = not_set, # type: ignore
print_blob: bool = not_set, # type: ignore
) -> None:
if parent is not None:
check_type(settings, parent, "parent")
if derandomize not in (not_set, False):
if database not in (not_set, None):
if database not in (not_set, None): # type: ignore
raise InvalidArgument(
"derandomize=True implies database=None, so passing "
f"database={database!r} too is invalid."
Expand Down
6 changes: 3 additions & 3 deletions hypothesis-python/src/hypothesis/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,7 @@ def as_param(name, kind, defaults):
def given(
*_given_arguments: Union[SearchStrategy[Any], InferType],
) -> Callable[
[Callable[..., Union[None, Coroutine[Any, Any, None]]]], Callable[..., None]
[Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it is safe to say that we cannot currently annotate this with ParamSpec, because of how complex its argument addition is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alas, yes, it's a partial-bind of only positional-or-keyword xor keyword-only parameters; and you can't pass strategies as positional args if there are any pos-only parameters; and positional args are bound from the rightmost pos-or-kw parameter (bizzare semantics, but we're stuck with it now).

I don't even see a more-precise overload we could extract, the closest would be "if you pass all the kwargs the wrapped function accepts, then the new function has no parameters", but even that breaks with **kwargs.

]: # pragma: no cover
...

Expand All @@ -1010,7 +1010,7 @@ def given(
def given(
**_given_kwargs: Union[SearchStrategy[Any], InferType],
) -> Callable[
[Callable[..., Union[None, Coroutine[Any, Any, None]]]], Callable[..., None]
[Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
]: # pragma: no cover
...

Expand All @@ -1019,7 +1019,7 @@ def given(
*_given_arguments: Union[SearchStrategy[Any], InferType],
**_given_kwargs: Union[SearchStrategy[Any], InferType],
) -> Callable[
[Callable[..., Union[None, Coroutine[Any, Any, None]]]], Callable[..., None]
[Callable[..., Optional[Coroutine[Any, Any, None]]]], Callable[..., None]
]:
"""A decorator for turning a test function that accepts arguments into a
randomized test.
Expand Down
8 changes: 4 additions & 4 deletions hypothesis-python/src/hypothesis/internal/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,19 @@
WINDOWS = platform.system() == "Windows"


def escape_unicode_characters(s):
def escape_unicode_characters(s: str) -> str:
return codecs.encode(s, "unicode_escape").decode("ascii")


def int_from_bytes(data):
def int_from_bytes(data: typing.Union[bytes, bytearray]) -> int:
return int.from_bytes(data, "big")


def int_to_bytes(i, size):
def int_to_bytes(i: int, size: int) -> bytes:
return i.to_bytes(size, "big")


def int_to_byte(i):
def int_to_byte(i: int) -> bytes:
return bytes([i])


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ def __init__(self):
self.n: "Optional[int]" = None

@property
def exhausted(self):
def exhausted(self) -> bool:
return self.live_child_count == 0


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from typing_extensions import dataclass_transform

from hypothesis.strategies import SearchStrategy
from hypothesis.strategies._internal.strategies import Ex
else:

def dataclass_transform():
Expand Down Expand Up @@ -816,7 +817,7 @@ def __init__(
self.max_length = max_length
self.is_find = False
self.overdraw = 0
self.__prefix = prefix
self.__prefix = bytes(prefix)
self.__random = random

assert random is not None or max_length <= len(prefix)
Expand Down Expand Up @@ -907,7 +908,7 @@ def note(self, value: Any) -> None:
value = repr(value)
self.output += value

def draw(self, strategy: "SearchStrategy[Any]", label: Optional[int] = None) -> Any:
def draw(self, strategy: "SearchStrategy[Ex]", label: Optional[int] = None) -> "Ex":
if self.is_find and not strategy.supports_find:
raise InvalidArgument(
f"Cannot use strategy {strategy!r} within a call to find "
Expand Down
2 changes: 1 addition & 1 deletion hypothesis-python/src/hypothesis/internal/filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class ConstructivePredicate(NamedTuple):
predicate: Optional[Predicate]

@classmethod
def unchanged(cls, predicate):
def unchanged(cls, predicate: Predicate) -> "ConstructivePredicate":
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If some class will subclass ConstructivePredicate without changing def unchanged, this annotation will become incorrect.

We can use TypeVar(..., bound="ConstructivePredicate") if this is the case.

Copy link
Member Author

@Zac-HD Zac-HD Jul 4, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a private/internal-only class and so should never be subclassed; I'd decorate it with @final if we'd dropped Py37 but it doesn't seem worth the compatibility shims in the meantime.

return cls({}, predicate)


Expand Down
10 changes: 3 additions & 7 deletions hypothesis-python/src/hypothesis/internal/floats.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import math
import struct
from sys import float_info
from typing import Callable, Optional
from typing import Callable, Optional, SupportsFloat

# Format codes for (int, float) sized types, used for byte-wise casts.
# See https://docs.python.org/3/library/struct.html#format-characters
Expand All @@ -36,19 +36,15 @@ def float_of(x, width):
return reinterpret_bits(float(x), "!e", "!e")


def sign(x):
def is_negative(x: SupportsFloat) -> bool:
try:
return math.copysign(1.0, x)
return math.copysign(1.0, x) < 0
except TypeError:
raise TypeError(
f"Expected float but got {x!r} of type {type(x).__name__}"
) from None


def is_negative(x):
return sign(x) < 0


def count_between_floats(x, y, width=64):
assert x <= y
if is_negative(x):
Expand Down
Loading