Skip to content

Commit 1773eec

Browse files
committed
address review comments
1 parent 3325a38 commit 1773eec

File tree

4 files changed

+34
-58
lines changed

4 files changed

+34
-58
lines changed

hypothesis-python/RELEASE.rst

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
11
RELEASE_TYPE: patch
22

3-
Improves several issues surrounding the code introduced in :ref:`version 6.131.1 <v6.131.1>`:
4-
5-
* Improve speed of constants collection.
6-
* Cache constants to the ``.hypothesis`` directory for future runs.
7-
* Fix a ``RecursionError`` on parsing deeply nested code.
3+
This patch makes the new features introduced in :ref:`version 6.131.1 <v6.131.1>` much
4+
faster, and fixes an internal ``RecursionError`` when working with deeply-nested code.

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@
209209
_local_modules: set[ModuleType] = set()
210210

211211

212-
def _get_local_constants(random: Random) -> "ConstantsT":
212+
def _get_local_constants() -> "ConstantsT":
213213
new_constants: set[ConstantT] = set()
214214
new_modules = list(local_modules() - _local_modules)
215215
for new_module in new_modules:
@@ -396,8 +396,8 @@ def __init__(self, conjecturedata: Optional["ConjectureData"], /):
396396

397397
@cached_property
398398
def _local_constants(self):
399-
assert self._random is not None
400-
return _get_local_constants(self._random)
399+
# defer computation of local constants until/if we need it
400+
return _get_local_constants()
401401

402402
def _maybe_draw_constant(
403403
self,

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

+25-38
Original file line numberDiff line numberDiff line change
@@ -96,65 +96,52 @@ def visit_Constant(self, node):
9696
self.generic_visit(node)
9797

9898

99-
def _constants_from_source(source: str) -> AbstractSet[ConstantT]:
100-
try:
101-
tree = ast.parse(source)
102-
visitor = ConstantVisitor()
103-
visitor.visit(tree)
104-
except Exception:
105-
# A bunch of things can go wrong here.
106-
# * ast.parse may fail on the source code
107-
# * NodeVisitor may hit a RecursionError (see many related issues on
108-
# e.g. libcst https://github.com/Instagram/LibCST/issues?q=recursion),
109-
# or a MemoryError (`"[1, " * 200 + "]" * 200`)
110-
return set()
111-
99+
def _constants_from_source(source: Union[str, bytes]) -> AbstractSet[ConstantT]:
100+
tree = ast.parse(source)
101+
visitor = ConstantVisitor()
102+
visitor.visit(tree)
112103
return visitor.constants
113104

114105

115106
@lru_cache(4096)
116107
def constants_from_module(module: ModuleType) -> AbstractSet[ConstantT]:
117108
try:
118109
module_file = inspect.getsourcefile(module)
110+
# use type: ignore because we know this might error
111+
source_bytes = Path(module_file).read_bytes() # type: ignore
119112
except Exception:
120113
return set()
121114

122-
if module_file is None:
123-
return set()
124-
115+
source_hash = hashlib.sha1(source_bytes).hexdigest()[:16]
116+
cache_p = storage_directory("constants") / source_hash
125117
try:
126-
source_bytes = Path(module_file).read_bytes()
118+
return _constants_from_source(cache_p.read_bytes())
127119
except Exception:
128-
return set()
129-
130-
constants_p = (
131-
storage_directory("constants") / hashlib.sha1(source_bytes).hexdigest()[:16]
132-
)
133-
if constants_p.exists():
134-
try:
135-
source = constants_p.read_text()
136-
except Exception:
137-
# if there's a problem reading the cached constants, fall back to
138-
# standard computation
139-
pass
140-
else:
141-
return _constants_from_source(source)
120+
# if the cached location doesn't exist, or it does exist but there was
121+
# a problem reading it, fall back to standard computation of the constants
122+
pass
142123

143124
try:
144-
source = inspect.getsource(module)
125+
constants = _constants_from_source(source_bytes)
145126
except Exception:
127+
# A bunch of things can go wrong here.
128+
# * ast.parse may fail on the source code
129+
# * NodeVisitor may hit a RecursionError (see many related issues on
130+
# e.g. libcst https://github.com/Instagram/LibCST/issues?q=recursion),
131+
# or a MemoryError (`"[1, " * 200 + "]" * 200`)
146132
return set()
147133

148-
constants = _constants_from_source(source)
149134
try:
150-
constants_p.parent.mkdir(parents=True, exist_ok=True)
151-
constants_p.write_text(
135+
cache_p.parent.mkdir(parents=True, exist_ok=True)
136+
cache_p.write_text(
152137
f"# file: {module_file}\n# hypothesis_version: {hypothesis.__version__}\n\n"
153-
+ str(constants),
138+
# somewhat arbitrary sort order. The cache file doesn't *have* to be
139+
# stable... but it is aesthetically pleasing, and means we could rely
140+
# on it in the future!
141+
+ str(sorted(constants, key=lambda v: (str(type(v)), v))),
154142
encoding="utf-8",
155143
)
156-
except Exception:
157-
# e.g. read-only filesystem
144+
except Exception: # pragma: no cover
158145
pass
159146

160147
return constants

hypothesis-python/tests/conjecture/test_local_constants.py

+4-12
Original file line numberDiff line numberDiff line change
@@ -29,37 +29,29 @@ def test_can_draw_local_constants_integers(monkeypatch, value):
2929
# _get_local_constants normally invalidates this cache for us, but we're
3030
# monkeypatching it.
3131
CONSTANTS_CACHE.cache.clear()
32-
monkeypatch.setattr(
33-
providers, "_get_local_constants", lambda random: {"integer": {value}}
34-
)
32+
monkeypatch.setattr(providers, "_get_local_constants", lambda: {"integer": {value}})
3533
find_any(st.integers(), lambda v: choice_equal(v, value))
3634

3735

3836
@xfail_on_crosshair(Why.undiscovered) # I think float_to_int is difficult for crosshair
3937
@pytest.mark.parametrize("value", [1.2938, -1823.0239, 1e999, math.nan])
4038
def test_can_draw_local_constants_floats(monkeypatch, value):
4139
CONSTANTS_CACHE.cache.clear()
42-
monkeypatch.setattr(
43-
providers, "_get_local_constants", lambda random: {"float": {value}}
44-
)
40+
monkeypatch.setattr(providers, "_get_local_constants", lambda: {"float": {value}})
4541
find_any(st.floats(), lambda v: choice_equal(v, value))
4642

4743

4844
@pytest.mark.parametrize("value", [b"abdefgh", b"a" * 50])
4945
def test_can_draw_local_constants_bytes(monkeypatch, value):
5046
CONSTANTS_CACHE.cache.clear()
51-
monkeypatch.setattr(
52-
providers, "_get_local_constants", lambda random: {"bytes": {value}}
53-
)
47+
monkeypatch.setattr(providers, "_get_local_constants", lambda: {"bytes": {value}})
5448
find_any(st.binary(), lambda v: choice_equal(v, value))
5549

5650

5751
@pytest.mark.parametrize("value", ["abdefgh", "a" * 50])
5852
def test_can_draw_local_constants_string(monkeypatch, value):
5953
CONSTANTS_CACHE.cache.clear()
60-
monkeypatch.setattr(
61-
providers, "_get_local_constants", lambda random: {"string": {value}}
62-
)
54+
monkeypatch.setattr(providers, "_get_local_constants", lambda: {"string": {value}})
6355
# we have a bunch of strings in GLOBAL_CONSTANTS, so it might take a while
6456
# to generate our local constant.
6557
find_any(

0 commit comments

Comments
 (0)