Skip to content

Commit f2b3500

Browse files
authored
Merge pull request #4105 from tybug/conjecture-typing
Add more internal type hints
2 parents dcbfab0 + ad455e4 commit f2b3500

File tree

14 files changed

+169
-124
lines changed

14 files changed

+169
-124
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+
This patch adds more type hints to internal Hypothesis code.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,7 +531,7 @@ def arrays(
531531
lambda s: arrays(dtype, s, elements=elements, fill=fill, unique=unique)
532532
)
533533
# From here on, we're only dealing with values and it's relatively simple.
534-
dtype = np.dtype(dtype) # type: ignore[arg-type,assignment]
534+
dtype = np.dtype(dtype) # type: ignore[arg-type]
535535
assert isinstance(dtype, np.dtype) # help mypy out a bit...
536536
if elements is None or isinstance(elements, Mapping):
537537
if dtype.kind in ("m", "M") and "[" not in dtype.str:

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

Lines changed: 85 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,64 @@
1515
import sys
1616
import tempfile
1717
import unicodedata
18+
from collections.abc import Iterable
1819
from functools import lru_cache
20+
from pathlib import Path
21+
from typing import TYPE_CHECKING, Literal, Optional
1922

2023
from hypothesis.configuration import storage_directory
2124
from hypothesis.control import _current_build_context
2225
from hypothesis.errors import InvalidArgument
23-
from hypothesis.internal.intervalsets import IntervalSet
24-
25-
intervals = tuple[tuple[int, int], ...]
26-
cache_type = dict[tuple[tuple[str, ...], int, int, intervals], IntervalSet]
27-
28-
29-
def charmap_file(fname="charmap"):
26+
from hypothesis.internal.intervalsets import IntervalSet, IntervalsT
27+
28+
if TYPE_CHECKING:
29+
from typing import TypeAlias
30+
31+
# See https://en.wikipedia.org/wiki/Unicode_character_property#General_Category
32+
CategoryName: "TypeAlias" = Literal[
33+
"L", # Letter
34+
"Lu", # Letter, uppercase
35+
"Ll", # Letter, lowercase
36+
"Lt", # Letter, titlecase
37+
"Lm", # Letter, modifier
38+
"Lo", # Letter, other
39+
"M", # Mark
40+
"Mn", # Mark, nonspacing
41+
"Mc", # Mark, spacing combining
42+
"Me", # Mark, enclosing
43+
"N", # Number
44+
"Nd", # Number, decimal digit
45+
"Nl", # Number, letter
46+
"No", # Number, other
47+
"P", # Punctuation
48+
"Pc", # Punctuation, connector
49+
"Pd", # Punctuation, dash
50+
"Ps", # Punctuation, open
51+
"Pe", # Punctuation, close
52+
"Pi", # Punctuation, initial quote
53+
"Pf", # Punctuation, final quote
54+
"Po", # Punctuation, other
55+
"S", # Symbol
56+
"Sm", # Symbol, math
57+
"Sc", # Symbol, currency
58+
"Sk", # Symbol, modifier
59+
"So", # Symbol, other
60+
"Z", # Separator
61+
"Zs", # Separator, space
62+
"Zl", # Separator, line
63+
"Zp", # Separator, paragraph
64+
"C", # Other
65+
"Cc", # Other, control
66+
"Cf", # Other, format
67+
"Cs", # Other, surrogate
68+
"Co", # Other, private use
69+
"Cn", # Other, not assigned
70+
]
71+
Categories: "TypeAlias" = Iterable[CategoryName]
72+
CategoriesTuple: "TypeAlias" = tuple[CategoryName, ...]
73+
74+
75+
def charmap_file(fname: str = "charmap") -> Path:
3076
return storage_directory(
3177
"unicode_data", unicodedata.unidata_version, f"{fname}.json.gz"
3278
)
@@ -35,7 +81,7 @@ def charmap_file(fname="charmap"):
3581
_charmap = None
3682

3783

38-
def charmap():
84+
def charmap() -> dict[CategoryName, IntervalsT]:
3985
"""Return a dict that maps a Unicode category, to a tuple of 2-tuples
4086
covering the codepoint intervals for characters in that category.
4187
@@ -49,8 +95,8 @@ def charmap():
4995
if _charmap is None:
5096
f = charmap_file()
5197
try:
52-
with gzip.GzipFile(f, "rb") as i:
53-
tmp_charmap = dict(json.load(i))
98+
with gzip.GzipFile(f, "rb") as d:
99+
tmp_charmap = dict(json.load(d))
54100

55101
except Exception:
56102
# This loop is reduced to using only local variables for performance;
@@ -63,9 +109,9 @@ def charmap():
63109
for i in range(1, sys.maxunicode + 1):
64110
cat = category(chr(i))
65111
if cat != last_cat:
66-
tmp_charmap.setdefault(last_cat, []).append([last_start, i - 1])
112+
tmp_charmap.setdefault(last_cat, []).append((last_start, i - 1))
67113
last_cat, last_start = cat, i
68-
tmp_charmap.setdefault(last_cat, []).append([last_start, sys.maxunicode])
114+
tmp_charmap.setdefault(last_cat, []).append((last_start, sys.maxunicode))
69115

70116
try:
71117
# Write the Unicode table atomically
@@ -135,10 +181,10 @@ def intervals_from_codec(codec_name: str) -> IntervalSet: # pragma: no cover
135181
return res
136182

137183

138-
_categories = None
184+
_categories: Optional[Categories] = None
139185

140186

141-
def categories():
187+
def categories() -> Categories:
142188
"""Return a tuple of Unicode categories in a normalised order.
143189
144190
>>> categories() # doctest: +ELLIPSIS
@@ -147,15 +193,16 @@ def categories():
147193
global _categories
148194
if _categories is None:
149195
cm = charmap()
150-
_categories = sorted(cm.keys(), key=lambda c: len(cm[c]))
151-
_categories.remove("Cc") # Other, Control
152-
_categories.remove("Cs") # Other, Surrogate
153-
_categories.append("Cc")
154-
_categories.append("Cs")
155-
return tuple(_categories)
196+
categories = sorted(cm.keys(), key=lambda c: len(cm[c]))
197+
categories.remove("Cc") # Other, Control
198+
categories.remove("Cs") # Other, Surrogate
199+
categories.append("Cc")
200+
categories.append("Cs")
201+
_categories = tuple(categories)
202+
return _categories
156203

157204

158-
def as_general_categories(cats, name="cats"):
205+
def as_general_categories(cats: Categories, name: str = "cats") -> CategoriesTuple:
159206
"""Return a tuple of Unicode categories in a normalised order.
160207
161208
This function expands one-letter designations of a major class to include
@@ -170,8 +217,6 @@ def as_general_categories(cats, name="cats"):
170217
If the collection ``cats`` includes any elements that do not represent a
171218
major class or a class with subclass, a deprecation warning is raised.
172219
"""
173-
if cats is None:
174-
return None
175220
major_classes = ("L", "M", "N", "P", "S", "Z", "C")
176221
cs = categories()
177222
out = set(cats)
@@ -186,10 +231,10 @@ def as_general_categories(cats, name="cats"):
186231
return tuple(c for c in cs if c in out)
187232

188233

189-
category_index_cache = {(): ()}
234+
category_index_cache: dict[frozenset[CategoryName], IntervalsT] = {frozenset(): ()}
190235

191236

192-
def _category_key(cats):
237+
def _category_key(cats: Optional[Iterable[str]]) -> CategoriesTuple:
193238
"""Return a normalised tuple of all Unicode categories that are in
194239
`include`, but not in `exclude`.
195240
@@ -205,7 +250,7 @@ def _category_key(cats):
205250
return tuple(c for c in cs if c in cats)
206251

207252

208-
def _query_for_key(key):
253+
def _query_for_key(key: Categories) -> IntervalsT:
209254
"""Return a tuple of codepoint intervals covering characters that match one
210255
or more categories in the tuple of categories `key`.
211256
@@ -214,10 +259,13 @@ def _query_for_key(key):
214259
>>> _query_for_key(('Zl', 'Zp', 'Co'))
215260
((8232, 8233), (57344, 63743), (983040, 1048573), (1048576, 1114109))
216261
"""
262+
key = tuple(key)
263+
# ignore ordering on the cache key to increase potential cache hits.
264+
cache_key = frozenset(key)
217265
context = _current_build_context.value
218266
if context is None or not context.data.provider.avoid_realization:
219267
try:
220-
return category_index_cache[key]
268+
return category_index_cache[cache_key]
221269
except KeyError:
222270
pass
223271
elif not key: # pragma: no cover # only on alternative backends
@@ -231,21 +279,23 @@ def _query_for_key(key):
231279
)
232280
assert isinstance(result, IntervalSet)
233281
if context is None or not context.data.provider.avoid_realization:
234-
category_index_cache[key] = result.intervals
282+
category_index_cache[cache_key] = result.intervals
235283
return result.intervals
236284

237285

238-
limited_category_index_cache: cache_type = {}
286+
limited_category_index_cache: dict[
287+
tuple[CategoriesTuple, int, int, IntervalsT, IntervalsT], IntervalSet
288+
] = {}
239289

240290

241291
def query(
242292
*,
243-
categories=None,
244-
min_codepoint=None,
245-
max_codepoint=None,
246-
include_characters="",
247-
exclude_characters="",
248-
):
293+
categories: Optional[Categories] = None,
294+
min_codepoint: Optional[int] = None,
295+
max_codepoint: Optional[int] = None,
296+
include_characters: str = "",
297+
exclude_characters: str = "",
298+
) -> IntervalSet:
249299
"""Return a tuple of intervals covering the codepoints for all characters
250300
that meet the criteria.
251301

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
TypedDict as TypedDict,
3737
override as override,
3838
)
39+
40+
from hypothesis.internal.conjecture.engine import ConjectureRunner
3941
else:
4042
# In order to use NotRequired, we need the version of TypedDict included in Python 3.11+.
4143
if sys.version_info[:2] >= (3, 11):
@@ -129,7 +131,7 @@ def _hint_and_args(x):
129131
return (x, *get_args(x))
130132

131133

132-
def get_type_hints(thing):
134+
def get_type_hints(thing: object) -> dict[str, Any]:
133135
"""Like the typing version, but tries harder and never errors.
134136
135137
Tries harder: if the thing to inspect is a class but typing.get_type_hints
@@ -237,7 +239,7 @@ def extract_bits(x: int, /, width: Optional[int] = None) -> list[int]:
237239
bit_count = lambda self: sum(extract_bits(abs(self)))
238240

239241

240-
def bad_django_TestCase(runner):
242+
def bad_django_TestCase(runner: Optional["ConjectureRunner"]) -> bool:
241243
if runner is None or "django.test" not in sys.modules:
242244
return False
243245
else: # pragma: no cover

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -569,7 +569,7 @@ def ir_ends(self) -> IntList:
569569

570570
class _discarded(ExampleProperty):
571571
def begin(self) -> None:
572-
self.result: "set[int]" = set()
572+
self.result: set[int] = set()
573573

574574
def finish(self) -> frozenset[int]:
575575
return frozenset(self.result)
@@ -583,7 +583,7 @@ def stop_example(self, i: int, *, discarded: bool) -> None:
583583
class _trivial(ExampleProperty):
584584
def begin(self) -> None:
585585
self.nontrivial = IntList.of_length(len(self.examples))
586-
self.result: "set[int]" = set()
586+
self.result: set[int] = set()
587587

588588
def block(self, i: int) -> None:
589589
if not self.examples.blocks.trivial(i):

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,10 @@ def __init__(self, weights: Sequence[float], *, observe: bool = True):
160160
table[small.pop()][2] = zero
161161

162162
self.table: "list[tuple[int, int, float]]" = []
163-
for base, alternate, alternate_chance in table: # type: ignore
163+
for base, alternate, alternate_chance in table:
164164
assert isinstance(base, int)
165165
assert isinstance(alternate, int) or alternate is None
166+
assert alternate_chance is not None
166167
if alternate is None:
167168
self.table.append((base, base, alternate_chance))
168169
elif alternate < base:

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

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
import random
1414
import sys
1515
import warnings
16-
from collections.abc import Hashable
16+
from collections.abc import Generator, Hashable
1717
from itertools import count
18-
from typing import TYPE_CHECKING, Any, Callable
18+
from typing import TYPE_CHECKING, Any, Callable, Optional
1919
from weakref import WeakValueDictionary
2020

2121
import hypothesis.core
@@ -28,9 +28,9 @@
2828
# we can't use this at runtime until from_type supports
2929
# protocols -- breaks ghostwriter tests
3030
class RandomLike(Protocol):
31-
seed: Callable[..., Any]
32-
getstate: Callable[[], Any]
33-
setstate: Callable[..., Any]
31+
def seed(self, *args: Any, **kwargs: Any) -> Any: ...
32+
def getstate(self, *args: Any, **kwargs: Any) -> Any: ...
33+
def setstate(self, *args: Any, **kwargs: Any) -> Any: ...
3434

3535
else: # pragma: no cover
3636
RandomLike = random.Random
@@ -39,11 +39,13 @@ class RandomLike(Protocol):
3939
# with their respective Random instances even as new ones are registered and old
4040
# ones go out of scope and get garbage collected. Keys are ascending integers.
4141
_RKEY = count()
42-
RANDOMS_TO_MANAGE: WeakValueDictionary = WeakValueDictionary({next(_RKEY): random})
42+
RANDOMS_TO_MANAGE: WeakValueDictionary[int, RandomLike] = WeakValueDictionary(
43+
{next(_RKEY): random}
44+
)
4345

4446

4547
class NumpyRandomWrapper:
46-
def __init__(self):
48+
def __init__(self) -> None:
4749
assert "numpy" in sys.modules
4850
# This class provides a shim that matches the numpy to stdlib random,
4951
# and lets us avoid importing Numpy until it's already in use.
@@ -54,7 +56,7 @@ def __init__(self):
5456
self.setstate = numpy.random.set_state
5557

5658

57-
NP_RANDOM = None
59+
NP_RANDOM: Optional[RandomLike] = None
5860

5961

6062
if not (PYPY or GRAALPY):
@@ -160,21 +162,21 @@ def get_seeder_and_restorer(
160162
"""
161163
assert isinstance(seed, int)
162164
assert 0 <= seed < 2**32
163-
states: dict = {}
165+
states: dict[int, object] = {}
164166

165167
if "numpy" in sys.modules:
166168
global NP_RANDOM
167169
if NP_RANDOM is None:
168170
# Protect this from garbage-collection by adding it to global scope
169171
NP_RANDOM = RANDOMS_TO_MANAGE[next(_RKEY)] = NumpyRandomWrapper()
170172

171-
def seed_all():
173+
def seed_all() -> None:
172174
assert not states
173175
for k, r in RANDOMS_TO_MANAGE.items():
174176
states[k] = r.getstate()
175177
r.seed(seed)
176178

177-
def restore_all():
179+
def restore_all() -> None:
178180
for k, state in states.items():
179181
r = RANDOMS_TO_MANAGE.get(k)
180182
if r is not None: # i.e., hasn't been garbage-collected
@@ -185,7 +187,7 @@ def restore_all():
185187

186188

187189
@contextlib.contextmanager
188-
def deterministic_PRNG(seed=0):
190+
def deterministic_PRNG(seed: int = 0) -> Generator[None, None, None]:
189191
"""Context manager that handles random.seed without polluting global state.
190192
191193
See issue #1255 and PR #1295 for details and motivation - in short,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ def get_numeric_predicate_bounds(predicate: Predicate) -> ConstructivePredicate:
282282

283283

284284
def get_integer_predicate_bounds(predicate: Predicate) -> ConstructivePredicate:
285-
kwargs, predicate = get_numeric_predicate_bounds(predicate) # type: ignore
285+
kwargs, predicate = get_numeric_predicate_bounds(predicate)
286286

287287
if "min_value" in kwargs:
288288
if kwargs["min_value"] == -math.inf:
@@ -310,7 +310,7 @@ def get_integer_predicate_bounds(predicate: Predicate) -> ConstructivePredicate:
310310

311311

312312
def get_float_predicate_bounds(predicate: Predicate) -> ConstructivePredicate:
313-
kwargs, predicate = get_numeric_predicate_bounds(predicate) # type: ignore
313+
kwargs, predicate = get_numeric_predicate_bounds(predicate)
314314

315315
if "min_value" in kwargs:
316316
min_value = kwargs["min_value"]

0 commit comments

Comments
 (0)