Skip to content

Commit b186203

Browse files
authored
Merge pull request #4126 from Zac-HD/various-cleanups
Various cleanups
2 parents e182f69 + fa0d507 commit b186203

29 files changed

+238
-115
lines changed

hypothesis-python/RELEASE.rst

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
RELEASE_TYPE: patch
2+
3+
This patch tweaks the paths in ``@example(...)`` patches, so that
4+
both ``git apply`` and ``patch`` will work by default.

hypothesis-python/scripts/other-tests.sh

+6-5
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,6 @@ pip uninstall -y lark
5151
if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel == \'final\' and platform.python_implementation() not in ("PyPy", "GraalVM"))')" = "True" ] ; then
5252
pip install ".[codemods,cli]"
5353
$PYTEST tests/codemods/
54-
pip install "$(grep -E 'black(==| @)' ../requirements/coverage.txt)"
55-
if [ "$(python -c 'import sys; print(sys.version_info[:2] >= (3, 9))')" = "True" ] ; then
56-
$PYTEST tests/patching/
57-
fi
58-
pip uninstall -y libcst
5954

6055
if [ "$(python -c 'import sys; print(sys.version_info[:2] == (3, 8))')" = "True" ] ; then
6156
# Per NEP-29, this is the last version to support Python 3.8
@@ -64,6 +59,12 @@ if [ "$(python -c $'import platform, sys; print(sys.version_info.releaselevel ==
6459
pip install "$(grep 'numpy==' ../requirements/coverage.txt)"
6560
fi
6661

62+
pip install "$(grep -E 'black(==| @)' ../requirements/coverage.txt)"
63+
if [ "$(python -c 'import sys; print(sys.version_info[:2] >= (3, 9))')" = "True" ] ; then
64+
$PYTEST tests/patching/
65+
fi
66+
pip uninstall -y libcst
67+
6768
$PYTEST tests/ghostwriter/
6869
pip uninstall -y black
6970

hypothesis-python/src/_hypothesis_ftz_detector.py

+18-8
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@
1818

1919
import importlib
2020
import sys
21+
from typing import TYPE_CHECKING, Callable, Optional, Set, Tuple
22+
23+
if TYPE_CHECKING:
24+
from multiprocessing import Queue
25+
from typing import TypeAlias
26+
27+
FTZCulprits: "TypeAlias" = Tuple[Optional[bool], Set[str]]
28+
2129

2230
KNOWN_EVER_CULPRITS = (
2331
# https://moyix.blogspot.com/2022/09/someones-been-messing-with-my-subnormals.html
@@ -35,32 +43,34 @@
3543
)
3644

3745

38-
def flush_to_zero():
46+
def flush_to_zero() -> bool:
3947
# If this subnormal number compares equal to zero we have a problem
4048
return 2.0**-1073 == 0
4149

4250

43-
def run_in_process(fn, *args):
51+
def run_in_process(fn: Callable[..., FTZCulprits], *args: object) -> FTZCulprits:
4452
import multiprocessing as mp
4553

4654
mp.set_start_method("spawn", force=True)
47-
q = mp.Queue()
55+
q: "Queue[FTZCulprits]" = mp.Queue()
4856
p = mp.Process(target=target, args=(q, fn, *args))
4957
p.start()
5058
retval = q.get()
5159
p.join()
5260
return retval
5361

5462

55-
def target(q, fn, *args):
63+
def target(
64+
q: "Queue[FTZCulprits]", fn: Callable[..., FTZCulprits], *args: object
65+
) -> None:
5666
q.put(fn(*args))
5767

5868

59-
def always_imported_modules():
69+
def always_imported_modules() -> FTZCulprits:
6070
return flush_to_zero(), set(sys.modules)
6171

6272

63-
def modules_imported_by(mod):
73+
def modules_imported_by(mod: str) -> FTZCulprits:
6474
"""Return the set of modules imported transitively by mod."""
6575
before = set(sys.modules)
6676
try:
@@ -77,7 +87,7 @@ def modules_imported_by(mod):
7787
CHECKED_CACHE = set()
7888

7989

80-
def identify_ftz_culprits():
90+
def identify_ftz_culprits() -> str:
8191
"""Find the modules in sys.modules which cause "mod" to be imported."""
8292
# If we've run this function before, return the same result.
8393
global KNOWN_FTZ
@@ -94,7 +104,7 @@ def identify_ftz_culprits():
94104
# that importing them in a new process sets the FTZ state. As a heuristic, we'll
95105
# start with packages known to have ever enabled FTZ, then top-level packages as
96106
# a way to eliminate large fractions of the search space relatively quickly.
97-
def key(name):
107+
def key(name: str) -> Tuple[bool, int, str]:
98108
"""Prefer known-FTZ modules, then top-level packages, then alphabetical."""
99109
return (name not in KNOWN_EVER_CULPRITS, name.count("."), name)
100110

hypothesis-python/src/hypothesis/configuration.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@
1212
import sys
1313
import warnings
1414
from pathlib import Path
15+
from typing import Union
1516

1617
import _hypothesis_globals
1718

1819
from hypothesis.errors import HypothesisSideeffectWarning
1920

2021
__hypothesis_home_directory_default = Path.cwd() / ".hypothesis"
21-
2222
__hypothesis_home_directory = None
2323

2424

25-
def set_hypothesis_home_dir(directory):
25+
def set_hypothesis_home_dir(directory: Union[str, Path, None]) -> None:
2626
global __hypothesis_home_directory
2727
__hypothesis_home_directory = None if directory is None else Path(directory)
2828

2929

30-
def storage_directory(*names, intent_to_write=True):
30+
def storage_directory(*names: str, intent_to_write: bool = True) -> Path:
3131
if intent_to_write:
3232
check_sideeffect_during_initialization(
3333
"accessing storage for {}", "/".join(names)

hypothesis-python/src/hypothesis/errors.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ class StopTest(BaseException):
214214
the Hypothesis engine, which should then continue normally.
215215
"""
216216

217-
def __init__(self, testcounter):
217+
def __init__(self, testcounter: int) -> None:
218218
super().__init__(repr(testcounter))
219219
self.testcounter = testcounter
220220

@@ -230,7 +230,7 @@ class Found(HypothesisException):
230230
class RewindRecursive(Exception):
231231
"""Signal that the type inference should be rewound due to recursive types. Internal use only."""
232232

233-
def __init__(self, target):
233+
def __init__(self, target: object) -> None:
234234
self.target = target
235235

236236

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

+54-5
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818
during fuzzing.
1919
"""
2020

21+
import ast
2122
import difflib
2223
import hashlib
2324
import inspect
2425
import re
2526
import sys
27+
import types
2628
from ast import literal_eval
2729
from contextlib import suppress
2830
from datetime import date, datetime, timedelta, timezone
@@ -31,6 +33,7 @@
3133
import libcst as cst
3234
from libcst import matchers as m
3335
from libcst.codemod import CodemodContext, VisitorBasedCodemodCommand
36+
from libcst.metadata import ExpressionContext, ExpressionContextProvider
3437

3538
from hypothesis.configuration import storage_directory
3639
from hypothesis.version import __version__
@@ -148,18 +151,64 @@ def get_patch_for(func, failing_examples, *, strip_via=()):
148151
except Exception:
149152
return None
150153

154+
modules_in_test_scope = sorted(
155+
(
156+
(k, v)
157+
for (k, v) in module.__dict__.items()
158+
if isinstance(v, types.ModuleType)
159+
),
160+
key=lambda kv: len(kv[1].__name__),
161+
)
162+
151163
# The printed examples might include object reprs which are invalid syntax,
152164
# so we parse here and skip over those. If _none_ are valid, there's no patch.
153165
call_nodes = []
154166
for ex, via in set(failing_examples):
155167
with suppress(Exception):
156-
node = cst.parse_expression(ex)
157-
assert isinstance(node, cst.Call), node
168+
node = cst.parse_module(ex)
169+
the_call = node.body[0].body[0].value
170+
assert isinstance(the_call, cst.Call), the_call
158171
# Check for st.data(), which doesn't support explicit examples
159172
data = m.Arg(m.Call(m.Name("data"), args=[m.Arg(m.Ellipsis())]))
160-
if m.matches(node, m.Call(args=[m.ZeroOrMore(), data, m.ZeroOrMore()])):
173+
if m.matches(the_call, m.Call(args=[m.ZeroOrMore(), data, m.ZeroOrMore()])):
161174
return None
175+
176+
# Many reprs use the unqualified name of the type, e.g. np.array()
177+
# -> array([...]), so here we find undefined names and look them up
178+
# on each module which was in the test's global scope.
179+
names = {}
180+
for anode in ast.walk(ast.parse(ex, "eval")):
181+
if (
182+
isinstance(anode, ast.Name)
183+
and isinstance(anode.ctx, ast.Load)
184+
and anode.id not in names
185+
and anode.id not in module.__dict__
186+
):
187+
for k, v in modules_in_test_scope:
188+
if anode.id in v.__dict__:
189+
names[anode.id] = cst.parse_expression(f"{k}.{anode.id}")
190+
break
191+
192+
# LibCST doesn't track Load()/Store() state of names by default, so we have
193+
# to do a bit of a dance here, *and* explicitly handle keyword arguments
194+
# which are treated as Load() context - but even if that's fixed later
195+
# we'll still want to support older versions.
196+
with suppress(Exception):
197+
wrapper = cst.metadata.MetadataWrapper(node)
198+
kwarg_names = {
199+
a.keyword for a in m.findall(wrapper, m.Arg(keyword=m.Name()))
200+
}
201+
node = m.replace(
202+
wrapper,
203+
m.Name(value=m.MatchIfTrue(names.__contains__))
204+
& m.MatchMetadata(ExpressionContextProvider, ExpressionContext.LOAD)
205+
& m.MatchIfTrue(lambda n, k=kwarg_names: n not in k),
206+
replacement=lambda node, _, ns=names: ns[node.value],
207+
)
208+
node = node.body[0].body[0].value
209+
assert isinstance(node, cst.Call), node
162210
call_nodes.append((node, via))
211+
163212
if not call_nodes:
164213
return None
165214

@@ -205,8 +254,8 @@ def make_patch(triples, *, msg="Hypothesis: add explicit examples", when=None):
205254
ud = difflib.unified_diff(
206255
source_before.splitlines(keepends=True),
207256
source_after.splitlines(keepends=True),
208-
fromfile=str(fname),
209-
tofile=str(fname),
257+
fromfile=f"./{fname}", # git strips the first part of the path by default
258+
tofile=f"./{fname}",
210259
)
211260
diffs.append("".join(ud))
212261
return "".join(diffs)

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ class LRUCache:
307307
# Anecdotally, OrderedDict seems quite competitive with lru_cache, but perhaps
308308
# that is localized to our access patterns.
309309

310-
def __init__(self, max_size):
310+
def __init__(self, max_size: int) -> None:
311311
assert max_size > 0
312312
self.max_size = max_size
313313
self._threadlocal = threading.local()

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from sys import float_info
1313

1414

15-
def cathetus(h, a):
15+
def cathetus(h: float, a: float) -> float:
1616
"""Given the lengths of the hypotenuse and a side of a right triangle,
1717
return the length of the other side.
1818

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ def int_to_byte(i: int) -> bytes:
110110
return bytes([i])
111111

112112

113-
def is_typed_named_tuple(cls):
113+
def is_typed_named_tuple(cls: type) -> bool:
114114
"""Return True if cls is probably a subtype of `typing.NamedTuple`.
115115
116116
Unfortunately types created with `class T(NamedTuple):` actually

0 commit comments

Comments
 (0)