Skip to content

Commit a2d540d

Browse files
authored
Merge pull request #4324 from tybug/typing
Add more type hints
2 parents 2f37b69 + 5c01b40 commit a2d540d

File tree

14 files changed

+154
-106
lines changed

14 files changed

+154
-106
lines changed

hypothesis-python/RELEASE.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
RELEASE_TYPE: patch
2+
3+
Improve our internal type hints.

hypothesis-python/src/hypothesis/extra/ghostwriter.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,7 @@ def _imports_for_strategy(strategy):
648648
for f in strategy.flat_conditions:
649649
imports |= _imports_for_object(f)
650650
if isinstance(strategy, FlatMapStrategy):
651-
imports |= _imports_for_strategy(strategy.flatmapped_strategy)
651+
imports |= _imports_for_strategy(strategy.base)
652652
imports |= _imports_for_object(strategy.expand)
653653

654654
# recurse through one_of to handle e.g. from_type(Optional[Foo])

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import math
1212
import time
1313
from collections import defaultdict
14-
from collections.abc import Iterable, Iterator, Sequence
14+
from collections.abc import Hashable, Iterable, Iterator, Sequence
1515
from enum import IntEnum
1616
from functools import cached_property
1717
from random import Random
@@ -704,6 +704,7 @@ def __init__(
704704
self._sampled_from_all_strategies_elements_message: Optional[
705705
tuple[str, object]
706706
] = None
707+
self._shared_strategy_draws: dict[Hashable, Any] = {}
707708

708709
self.expected_exception: Optional[BaseException] = None
709710
self.expected_traceback: Optional[str] = None

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -653,7 +653,7 @@ def test_function(self, data: ConjectureData) -> None:
653653

654654
self.record_for_health_check(data)
655655

656-
def on_pareto_evict(self, data: ConjectureData) -> None:
656+
def on_pareto_evict(self, data: ConjectureResult) -> None:
657657
self.settings.database.delete(self.pareto_key, choices_to_bytes(data.choices))
658658

659659
def generate_novel_prefix(self) -> tuple[ChoiceT, ...]:

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

+33-17
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,28 @@
88
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
99
# obtain one at https://mozilla.org/MPL/2.0/.
1010

11+
from collections.abc import Iterator
1112
from enum import Enum
13+
from random import Random
14+
from typing import TYPE_CHECKING, Callable, Optional, Union
1215

1316
from sortedcontainers import SortedList
1417

1518
from hypothesis.internal.conjecture.choice import choices_key
16-
from hypothesis.internal.conjecture.data import ConjectureData, ConjectureResult, Status
19+
from hypothesis.internal.conjecture.data import (
20+
ConjectureData,
21+
ConjectureResult,
22+
Status,
23+
_Overrun,
24+
)
1725
from hypothesis.internal.conjecture.junkdrawer import LazySequenceCopy
1826
from hypothesis.internal.conjecture.shrinker import sort_key
1927

2028
NO_SCORE = float("-inf")
2129

30+
if TYPE_CHECKING:
31+
from hypothesis.internal.conjecture.engine import ConjectureRunner
32+
2233

2334
class DominanceRelation(Enum):
2435
NO_DOMINANCE = 0
@@ -27,7 +38,7 @@ class DominanceRelation(Enum):
2738
RIGHT_DOMINATES = 3
2839

2940

30-
def dominance(left, right):
41+
def dominance(left: ConjectureResult, right: ConjectureResult) -> DominanceRelation:
3142
"""Returns the dominance relation between ``left`` and ``right``, according
3243
to the rules that one ConjectureResult dominates another if and only if it
3344
is better in every way.
@@ -125,21 +136,25 @@ class ParetoFront:
125136
see how much of a problem this is in practice before we try that.
126137
"""
127138

128-
def __init__(self, random):
139+
def __init__(self, random: Random) -> None:
129140
self.__random = random
130-
self.__eviction_listeners = []
141+
self.__eviction_listeners: list[Callable[[ConjectureResult], None]] = []
131142

132-
self.front = SortedList(key=lambda d: sort_key(d.nodes))
133-
self.__pending = None
143+
self.front: SortedList[ConjectureResult] = SortedList(
144+
key=lambda d: sort_key(d.nodes)
145+
)
146+
self.__pending: Optional[ConjectureResult] = None
134147

135-
def add(self, data):
148+
def add(self, data: Union[ConjectureData, ConjectureResult, _Overrun]) -> bool:
136149
"""Attempts to add ``data`` to the pareto front. Returns True if
137150
``data`` is now in the front, including if data is already in the
138151
collection, and False otherwise"""
139152
if data.status < Status.VALID:
140153
return False
141154

155+
assert not isinstance(data, _Overrun)
142156
data = data.as_result()
157+
assert not isinstance(data, _Overrun)
143158

144159
if not self.front:
145160
self.front.add(data)
@@ -166,7 +181,7 @@ def add(self, data):
166181
# We track which values we are going to remove and remove them all
167182
# at the end so the shape of the front doesn't change while we're
168183
# using it.
169-
to_remove = []
184+
to_remove: list[ConjectureResult] = []
170185

171186
# We now iteratively sample elements from the approximate pareto
172187
# front to check whether they should be retained. When the set of
@@ -238,26 +253,26 @@ def add(self, data):
238253
finally:
239254
self.__pending = None
240255

241-
def on_evict(self, f):
256+
def on_evict(self, f: Callable[[ConjectureResult], None]) -> None:
242257
"""Register a listener function that will be called with data when it
243258
gets removed from the front because something else dominates it."""
244259
self.__eviction_listeners.append(f)
245260

246-
def __contains__(self, data):
261+
def __contains__(self, data: object) -> bool:
247262
return isinstance(data, (ConjectureData, ConjectureResult)) and (
248263
data.as_result() in self.front
249264
)
250265

251-
def __iter__(self):
266+
def __iter__(self) -> Iterator[ConjectureResult]:
252267
return iter(self.front)
253268

254-
def __getitem__(self, i):
269+
def __getitem__(self, i: int) -> ConjectureResult:
255270
return self.front[i]
256271

257-
def __len__(self):
272+
def __len__(self) -> int:
258273
return len(self.front)
259274

260-
def __remove(self, data):
275+
def __remove(self, data: ConjectureResult) -> None:
261276
try:
262277
self.front.remove(data)
263278
except ValueError:
@@ -277,11 +292,12 @@ class ParetoOptimiser:
277292
grow more powerful over time.
278293
"""
279294

280-
def __init__(self, engine):
295+
def __init__(self, engine: "ConjectureRunner") -> None:
281296
self.__engine = engine
282-
self.front = self.__engine.pareto_front
297+
assert self.__engine.pareto_front is not None
298+
self.front: ParetoFront = self.__engine.pareto_front
283299

284-
def run(self):
300+
def run(self) -> None:
285301
seen = set()
286302

287303
# We iterate backwards through the pareto front, using the shrinker to

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

+36-31
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@
2323
import warnings
2424
from collections.abc import MutableMapping, Sequence
2525
from functools import partial, wraps
26+
from inspect import Parameter, Signature
2627
from io import StringIO
2728
from keyword import iskeyword
2829
from random import _inst as global_random_instance
2930
from tokenize import COMMENT, detect_encoding, generate_tokens, untokenize
3031
from types import ModuleType
31-
from typing import Any, Callable, TypeVar
32+
from typing import Any, Callable, Optional, TypeVar
3233
from unittest.mock import _patch as PatchType
3334
from weakref import WeakKeyDictionary
3435

@@ -43,7 +44,7 @@
4344
LAMBDA_SOURCE_CACHE: MutableMapping[Callable, str] = WeakKeyDictionary()
4445

4546

46-
def is_mock(obj):
47+
def is_mock(obj: object) -> bool:
4748
"""Determine if the given argument is a mock type."""
4849

4950
# We want to be able to detect these when dealing with various test
@@ -118,7 +119,7 @@ def function_digest(function: Any) -> bytes:
118119
return hasher.digest()
119120

120121

121-
def check_signature(sig: inspect.Signature) -> None:
122+
def check_signature(sig: Signature) -> None:
122123
# Backport from Python 3.11; see https://github.com/python/cpython/pull/92065
123124
for p in sig.parameters.values():
124125
if iskeyword(p.name) and p.kind is not p.POSITIONAL_ONLY:
@@ -132,17 +133,19 @@ def check_signature(sig: inspect.Signature) -> None:
132133

133134
def get_signature(
134135
target: Any, *, follow_wrapped: bool = True, eval_str: bool = False
135-
) -> inspect.Signature:
136+
) -> Signature:
136137
# Special case for use of `@unittest.mock.patch` decorator, mimicking the
137138
# behaviour of getfullargspec instead of reporting unusable arguments.
138139
patches = getattr(target, "patchings", None)
139140
if isinstance(patches, list) and all(isinstance(p, PatchType) for p in patches):
140-
P = inspect.Parameter
141-
return inspect.Signature(
142-
[P("args", P.VAR_POSITIONAL), P("keywargs", P.VAR_KEYWORD)]
141+
return Signature(
142+
[
143+
Parameter("args", Parameter.VAR_POSITIONAL),
144+
Parameter("keywargs", Parameter.VAR_KEYWORD),
145+
]
143146
)
144147

145-
if isinstance(getattr(target, "__signature__", None), inspect.Signature):
148+
if isinstance(getattr(target, "__signature__", None), Signature):
146149
# This special case covers unusual codegen like Pydantic models
147150
sig = target.__signature__
148151
check_signature(sig)
@@ -152,7 +155,7 @@ def get_signature(
152155
selfy = next(iter(sig.parameters.values()))
153156
if (
154157
selfy.name == "self"
155-
and selfy.default is inspect.Parameter.empty
158+
and selfy.default is Parameter.empty
156159
and selfy.kind.name.startswith("POSITIONAL_")
157160
):
158161
return sig.replace(
@@ -172,10 +175,10 @@ def get_signature(
172175
return sig
173176

174177

175-
def arg_is_required(param):
176-
return param.default is inspect.Parameter.empty and param.kind in (
177-
inspect.Parameter.POSITIONAL_OR_KEYWORD,
178-
inspect.Parameter.KEYWORD_ONLY,
178+
def arg_is_required(param: Parameter) -> bool:
179+
return param.default is Parameter.empty and param.kind in (
180+
Parameter.POSITIONAL_OR_KEYWORD,
181+
Parameter.KEYWORD_ONLY,
179182
)
180183

181184

@@ -204,7 +207,9 @@ def required_args(target, args=(), kwargs=()):
204207
}
205208

206209

207-
def convert_keyword_arguments(function, args, kwargs):
210+
def convert_keyword_arguments(
211+
function: Any, args: Sequence[object], kwargs: dict[str, object]
212+
) -> tuple[tuple[object, ...], dict[str, object]]:
208213
"""Returns a pair of a tuple and a dictionary which would be equivalent
209214
passed as positional and keyword args to the function. Unless function has
210215
kwonlyargs or **kwargs the dictionary will always be empty.
@@ -238,24 +243,22 @@ def convert_positional_arguments(
238243
return tuple(new_args), new_kwargs
239244

240245

241-
def ast_arguments_matches_signature(args, sig):
242-
assert isinstance(args, ast.arguments)
243-
assert isinstance(sig, inspect.Signature)
244-
expected = []
246+
def ast_arguments_matches_signature(args: ast.arguments, sig: Signature) -> bool:
247+
expected: list[tuple[str, int]] = []
245248
for node in args.posonlyargs:
246-
expected.append((node.arg, inspect.Parameter.POSITIONAL_ONLY))
249+
expected.append((node.arg, Parameter.POSITIONAL_ONLY))
247250
for node in args.args:
248-
expected.append((node.arg, inspect.Parameter.POSITIONAL_OR_KEYWORD))
251+
expected.append((node.arg, Parameter.POSITIONAL_OR_KEYWORD))
249252
if args.vararg is not None:
250-
expected.append((args.vararg.arg, inspect.Parameter.VAR_POSITIONAL))
253+
expected.append((args.vararg.arg, Parameter.VAR_POSITIONAL))
251254
for node in args.kwonlyargs:
252-
expected.append((node.arg, inspect.Parameter.KEYWORD_ONLY))
255+
expected.append((node.arg, Parameter.KEYWORD_ONLY))
253256
if args.kwarg is not None:
254-
expected.append((args.kwarg.arg, inspect.Parameter.VAR_KEYWORD))
257+
expected.append((args.kwarg.arg, Parameter.VAR_KEYWORD))
255258
return expected == [(p.name, p.kind) for p in sig.parameters.values()]
256259

257260

258-
def is_first_param_referenced_in_function(f):
261+
def is_first_param_referenced_in_function(f: Any) -> bool:
259262
"""Is the given name referenced within f?"""
260263
try:
261264
tree = ast.parse(textwrap.dedent(inspect.getsource(f)))
@@ -301,7 +304,7 @@ def _extract_lambda_source(f):
301304
# The answer is that we add this at runtime, in new_given_signature(),
302305
# and we do support strange choices as applying @given() to a lambda.
303306
sig = inspect.signature(f)
304-
assert sig.return_annotation in (inspect.Parameter.empty, None), sig
307+
assert sig.return_annotation in (Parameter.empty, None), sig
305308

306309
# Using pytest-xdist on Python 3.13, there's an entry in the linecache for
307310
# file "<string>", which then returns nonsense to getsource. Discard it.
@@ -459,7 +462,7 @@ def get_pretty_function_description(f: object) -> str:
459462
return name
460463

461464

462-
def nicerepr(v):
465+
def nicerepr(v: Any) -> str:
463466
if inspect.isfunction(v):
464467
return get_pretty_function_description(v)
465468
elif isinstance(v, type):
@@ -500,15 +503,15 @@ def repr_call(
500503
return rep + "(" + ", ".join(bits) + ")"
501504

502505

503-
def check_valid_identifier(identifier):
506+
def check_valid_identifier(identifier: str) -> None:
504507
if not identifier.isidentifier():
505508
raise ValueError(f"{identifier!r} is not a valid python identifier")
506509

507510

508-
eval_cache: dict = {}
511+
eval_cache: dict[str, ModuleType] = {}
509512

510513

511-
def source_exec_as_module(source):
514+
def source_exec_as_module(source: str) -> ModuleType:
512515
try:
513516
return eval_cache[source]
514517
except KeyError:
@@ -532,7 +535,9 @@ def {name}{signature}:
532535
""".lstrip()
533536

534537

535-
def get_varargs(sig, kind=inspect.Parameter.VAR_POSITIONAL):
538+
def get_varargs(
539+
sig: Signature, kind: int = Parameter.VAR_POSITIONAL
540+
) -> Optional[Parameter]:
536541
for p in sig.parameters.values():
537542
if p.kind is kind:
538543
return p
@@ -583,7 +588,7 @@ def accept(f):
583588
for p in signature.parameters.values():
584589
if p.kind is p.KEYWORD_ONLY:
585590
invocation_parts.append(f"{p.name}={p.name}")
586-
varkw = get_varargs(signature, kind=inspect.Parameter.VAR_KEYWORD)
591+
varkw = get_varargs(signature, kind=Parameter.VAR_KEYWORD)
587592
if varkw:
588593
invocation_parts.append("**" + varkw.name)
589594

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
get_type_hints,
8080
is_typed_named_tuple,
8181
)
82+
from hypothesis.internal.conjecture.data import ConjectureData
8283
from hypothesis.internal.conjecture.utils import (
8384
calc_label_from_cls,
8485
check_sample,
@@ -2199,7 +2200,7 @@ class DataObject:
21992200
# Note that "only exists" here really means "is only exported to users",
22002201
# but we want to treat it as "semi-stable", not document it as "public API".
22012202

2202-
def __init__(self, data):
2203+
def __init__(self, data: ConjectureData) -> None:
22032204
self.count = 0
22042205
self.conjecture_data = data
22052206

0 commit comments

Comments
 (0)