Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/hypothesis/internal/constants_ast.py: 36%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

138 statements  

1# This file is part of Hypothesis, which may be found at 

2# https://github.com/HypothesisWorks/hypothesis/ 

3# 

4# Copyright the Hypothesis Authors. 

5# Individual contributors are listed in AUTHORS.rst and the git log. 

6# 

7# This Source Code Form is subject to the terms of the Mozilla Public License, 

8# v. 2.0. If a copy of the MPL was not distributed with this file, You can 

9# obtain one at https://mozilla.org/MPL/2.0/. 

10 

11import ast 

12import hashlib 

13import inspect 

14import math 

15import os 

16import sys 

17import tempfile 

18from ast import Constant, Expr, NodeVisitor, UnaryOp, USub 

19from collections.abc import Iterator, MutableSet 

20from functools import lru_cache 

21from itertools import chain 

22from pathlib import Path 

23from types import ModuleType 

24from typing import TypeAlias 

25 

26import hypothesis 

27from hypothesis.configuration import storage_directory 

28from hypothesis.internal.conjecture.choice import ChoiceTypeT 

29from hypothesis.internal.escalation import is_hypothesis_file 

30 

31ConstantT: TypeAlias = int | float | bytes | str 

32 

33# unfortunate collision with builtin. I don't want to name the init arg bytes_. 

34bytesT = bytes 

35 

36 

37class Constants: 

38 def __init__( 

39 self, 

40 *, 

41 integers: MutableSet[int] | None = None, 

42 floats: MutableSet[float] | None = None, 

43 bytes: MutableSet[bytes] | None = None, 

44 strings: MutableSet[str] | None = None, 

45 ): 

46 self.integers: MutableSet[int] = set() if integers is None else integers 

47 self.floats: MutableSet[float] = set() if floats is None else floats 

48 self.bytes: MutableSet[bytesT] = set() if bytes is None else bytes 

49 self.strings: MutableSet[str] = set() if strings is None else strings 

50 

51 def set_for_type( 

52 self, constant_type: type[ConstantT] | ChoiceTypeT 

53 ) -> MutableSet[int] | MutableSet[float] | MutableSet[bytes] | MutableSet[str]: 

54 if constant_type is int or constant_type == "integer": 

55 return self.integers 

56 elif constant_type is float or constant_type == "float": 

57 return self.floats 

58 elif constant_type is bytes or constant_type == "bytes": 

59 return self.bytes 

60 elif constant_type is str or constant_type == "string": 

61 return self.strings 

62 raise ValueError(f"unknown constant_type {constant_type}") 

63 

64 def add(self, constant: ConstantT) -> None: 

65 self.set_for_type(type(constant)).add(constant) # type: ignore 

66 

67 def __contains__(self, constant: ConstantT) -> bool: 

68 return constant in self.set_for_type(type(constant)) 

69 

70 def __or__(self, other: "Constants") -> "Constants": 

71 return Constants( 

72 integers=self.integers | other.integers, # type: ignore 

73 floats=self.floats | other.floats, # type: ignore 

74 bytes=self.bytes | other.bytes, # type: ignore 

75 strings=self.strings | other.strings, # type: ignore 

76 ) 

77 

78 def __iter__(self) -> Iterator[ConstantT]: 

79 return iter(chain(self.integers, self.floats, self.bytes, self.strings)) 

80 

81 def __len__(self) -> int: 

82 return ( 

83 len(self.integers) + len(self.floats) + len(self.bytes) + len(self.strings) 

84 ) 

85 

86 def __repr__(self) -> str: 

87 return f"Constants({self.integers=}, {self.floats=}, {self.bytes=}, {self.strings=})" 

88 

89 def __eq__(self, other: object) -> bool: 

90 if not isinstance(other, Constants): 

91 return False 

92 return ( 

93 self.integers == other.integers 

94 and self.floats == other.floats 

95 and self.bytes == other.bytes 

96 and self.strings == other.strings 

97 ) 

98 

99 

100class TooManyConstants(Exception): 

101 # a control flow exception which we raise in ConstantsVisitor when the 

102 # number of constants in a module gets too large. 

103 pass 

104 

105 

106class ConstantVisitor(NodeVisitor): 

107 CONSTANTS_LIMIT: int = 1024 

108 

109 def __init__(self, *, limit: bool): 

110 super().__init__() 

111 self.constants = Constants() 

112 self.limit = limit 

113 

114 def _add_constant(self, value: object) -> None: 

115 if self.limit and len(self.constants) >= self.CONSTANTS_LIMIT: 

116 raise TooManyConstants 

117 

118 if isinstance(value, str) and ( 

119 value.isspace() 

120 or value == "" 

121 # long strings are unlikely to be useful. 

122 or len(value) > 20 

123 ): 

124 return 

125 if isinstance(value, bytes) and ( 

126 value == b"" 

127 # long bytes seem plausibly more likely to be useful than long strings 

128 # (e.g. AES-256 has a 32 byte key), but we still want to cap at some 

129 # point to avoid performance issues. 

130 or len(value) > 50 

131 ): 

132 return 

133 if isinstance(value, bool): 

134 return 

135 if isinstance(value, float) and (math.isinf(value) or value == 0.0): 

136 # we already upweight inf and ±0. 

137 return 

138 if isinstance(value, int) and -100 < value < 100: 

139 # we already upweight small integers. 

140 return 

141 if isinstance(value, (int, float, bytes, str)): 

142 self.constants.add(value) 

143 return 

144 if isinstance(value, complex): 

145 self._add_constant(value.imag) 

146 self._add_constant(value.real) 

147 return 

148 if value in (None, ...): 

149 return 

150 # we currently cover all possible cases, but future python versions may introduce 

151 # additional cases 

152 return # pragma: no cover 

153 

154 def visit_UnaryOp(self, node: UnaryOp) -> None: 

155 # `a = -1` is actually a combination of a USub and the constant 1. 

156 if ( 

157 isinstance(node.op, USub) 

158 and isinstance(node.operand, Constant) 

159 and isinstance(node.operand.value, (int, float, complex)) 

160 and not isinstance(node.operand.value, bool) 

161 ): 

162 self._add_constant(-node.operand.value) 

163 # don't recurse on this node to avoid adding the positive variant 

164 return 

165 

166 self.generic_visit(node) 

167 

168 def visit_Expr(self, node: Expr) -> None: 

169 if isinstance(node.value, Constant) and isinstance(node.value.value, str): 

170 return 

171 

172 self.generic_visit(node) 

173 

174 def visit_JoinedStr(self, node): 

175 # dont recurse on JoinedStr, i.e. f strings. Constants that appear *only* 

176 # in f strings are unlikely to be helpful. 

177 return 

178 

179 def visit_Constant(self, node): 

180 self._add_constant(node.value) 

181 self.generic_visit(node) 

182 

183 

184def _constants_from_source(source: str | bytes, *, limit: bool) -> Constants: 

185 tree = ast.parse(source) 

186 visitor = ConstantVisitor(limit=limit) 

187 

188 try: 

189 visitor.visit(tree) 

190 except TooManyConstants: 

191 # in the case of an incomplete collection, return nothing, to avoid 

192 # muddying caches etc. 

193 return Constants() 

194 

195 return visitor.constants 

196 

197 

198def _constants_file_str(constants: Constants) -> str: 

199 return str(sorted(constants, key=lambda v: (str(type(v)), v))) 

200 

201 

202@lru_cache(4096) 

203def constants_from_module(module: ModuleType, *, limit: bool = True) -> Constants: 

204 try: 

205 module_file = inspect.getsourcefile(module) 

206 # use type: ignore because we know this might error 

207 source_bytes = Path(module_file).read_bytes() # type: ignore 

208 except Exception: 

209 return Constants() 

210 

211 if limit and len(source_bytes) > 512 * 1024: 

212 # Skip files over 512kb. For reference, the largest source file 

213 # in Hypothesis is strategies/_internal/core.py at 107kb at time 

214 # of writing. 

215 return Constants() 

216 

217 source_hash = hashlib.sha1(source_bytes).hexdigest()[:16] 

218 # separate cache files for each limit param. see discussion in pull/4398 

219 cache_dir = storage_directory("constants") 

220 cache_p = cache_dir.path / (source_hash + ("" if limit else "_nolimit")) 

221 try: 

222 return _constants_from_source(cache_p.read_bytes(), limit=limit) 

223 except Exception: 

224 # if the cached location doesn't exist, or it does exist but there was 

225 # a problem reading it, fall back to standard computation of the constants 

226 pass 

227 

228 try: 

229 constants = _constants_from_source(source_bytes, limit=limit) 

230 except Exception: 

231 # A bunch of things can go wrong here. 

232 # * ast.parse may fail on the source code 

233 # * NodeVisitor may hit a RecursionError (see many related issues on 

234 # e.g. libcst https://github.com/Instagram/LibCST/issues?q=recursion), 

235 # or a MemoryError (`"[1, " * 200 + "]" * 200`) 

236 return Constants() 

237 

238 try: 

239 cache_dir.create_if_missing() 

240 # Write atomically, so that a concurrent process (e.g. another 

241 # pytest-xdist worker) never reads a partially written cache file. 

242 # A truncated file still parses, and would silently drop constants. 

243 fd, tmp_path = tempfile.mkstemp(dir=cache_dir.path, prefix=".tmp-") 

244 with os.fdopen(fd, "w", encoding="utf-8") as f: 

245 f.write( 

246 f"# file: {module_file}\n# hypothesis_version: {hypothesis.__version__}\n\n" 

247 # somewhat arbitrary sort order. The cache file doesn't *have* to be 

248 # stable... but it is aesthetically pleasing, and means we could rely 

249 # on it in the future! 

250 + _constants_file_str(constants) 

251 ) 

252 os.replace(tmp_path, cache_p) 

253 except Exception: # pragma: no cover 

254 pass 

255 

256 return constants 

257 

258 

259@lru_cache(4096) 

260def is_local_module_file(path: str) -> bool: 

261 from hypothesis.internal.scrutineer import ModuleLocation 

262 

263 return ( 

264 # Skip expensive path lookup for stdlib modules. 

265 # This will cause false negatives if a user names their module the 

266 # same as a stdlib module. 

267 path not in sys.stdlib_module_names 

268 # A path containing site-packages is extremely likely to be 

269 # ModuleLocation.SITE_PACKAGES. Skip the expensive path lookup here. 

270 and "/site-packages/" not in path 

271 and ModuleLocation.from_path(path) is ModuleLocation.LOCAL 

272 # normally, hypothesis is a third-party library and is not returned 

273 # by local_modules. However, if it is installed as an editable package 

274 # with pip install -e, then we will pick up on it. Just hardcode an 

275 # ignore here. 

276 and not is_hypothesis_file(path) 

277 # avoid collecting constants from test files 

278 and not ( 

279 "test" in (p := Path(path)).parts 

280 or "tests" in p.parts 

281 or p.stem.startswith("test_") 

282 or p.stem.endswith("_test") 

283 ) 

284 )