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

133 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 sys 

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

17from collections.abc import Iterator, MutableSet 

18from functools import lru_cache 

19from itertools import chain 

20from pathlib import Path 

21from types import ModuleType 

22from typing import TypeAlias 

23 

24import hypothesis 

25from hypothesis.configuration import storage_directory 

26from hypothesis.internal.conjecture.choice import ChoiceTypeT 

27from hypothesis.internal.escalation import is_hypothesis_file 

28 

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

30 

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

32bytesT = bytes 

33 

34 

35class Constants: 

36 def __init__( 

37 self, 

38 *, 

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

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

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

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

43 ): 

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

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

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

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

48 

49 def set_for_type( 

50 self, constant_type: type[ConstantT] | ChoiceTypeT 

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

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

53 return self.integers 

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

55 return self.floats 

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

57 return self.bytes 

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

59 return self.strings 

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

61 

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

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

64 

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

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

67 

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

69 return Constants( 

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

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

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

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

74 ) 

75 

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

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

78 

79 def __len__(self) -> int: 

80 return ( 

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

82 ) 

83 

84 def __repr__(self) -> str: 

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

86 

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

88 if not isinstance(other, Constants): 

89 return False 

90 return ( 

91 self.integers == other.integers 

92 and self.floats == other.floats 

93 and self.bytes == other.bytes 

94 and self.strings == other.strings 

95 ) 

96 

97 

98class TooManyConstants(Exception): 

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

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

101 pass 

102 

103 

104class ConstantVisitor(NodeVisitor): 

105 CONSTANTS_LIMIT: int = 1024 

106 

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

108 super().__init__() 

109 self.constants = Constants() 

110 self.limit = limit 

111 

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

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

114 raise TooManyConstants 

115 

116 if isinstance(value, str) and ( 

117 value.isspace() 

118 or value == "" 

119 # long strings are unlikely to be useful. 

120 or len(value) > 20 

121 ): 

122 return 

123 if isinstance(value, bytes) and ( 

124 value == b"" 

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

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

127 # point to avoid performance issues. 

128 or len(value) > 50 

129 ): 

130 return 

131 if isinstance(value, bool): 

132 return 

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

134 # we already upweight inf and ±0. 

135 return 

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

137 # we already upweight small integers. 

138 return 

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

140 self.constants.add(value) 

141 return 

142 if isinstance(value, complex): 

143 self._add_constant(value.imag) 

144 self._add_constant(value.real) 

145 return 

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

147 return 

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

149 # additional cases 

150 return # pragma: no cover 

151 

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

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

154 if ( 

155 isinstance(node.op, USub) 

156 and isinstance(node.operand, Constant) 

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

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

159 ): 

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

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

162 return 

163 

164 self.generic_visit(node) 

165 

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

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

168 return 

169 

170 self.generic_visit(node) 

171 

172 def visit_JoinedStr(self, node): 

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

174 # in f strings are unlikely to be helpful. 

175 return 

176 

177 def visit_Constant(self, node): 

178 self._add_constant(node.value) 

179 self.generic_visit(node) 

180 

181 

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

183 tree = ast.parse(source) 

184 visitor = ConstantVisitor(limit=limit) 

185 

186 try: 

187 visitor.visit(tree) 

188 except TooManyConstants: 

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

190 # muddying caches etc. 

191 return Constants() 

192 

193 return visitor.constants 

194 

195 

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

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

198 

199 

200@lru_cache(4096) 

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

202 try: 

203 module_file = inspect.getsourcefile(module) 

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

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

206 except Exception: 

207 return Constants() 

208 

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

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

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

212 # of writing. 

213 return Constants() 

214 

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

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

217 cache_dir = storage_directory("constants") 

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

219 try: 

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

221 except Exception: 

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

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

224 pass 

225 

226 try: 

227 constants = _constants_from_source(source_bytes, limit=limit) 

228 except Exception: 

229 # A bunch of things can go wrong here. 

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

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

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

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

234 return Constants() 

235 

236 try: 

237 cache_dir.create_if_missing() 

238 cache_p.write_text( 

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

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

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

242 # on it in the future! 

243 + _constants_file_str(constants), 

244 encoding="utf-8", 

245 ) 

246 except Exception: # pragma: no cover 

247 pass 

248 

249 return constants 

250 

251 

252@lru_cache(4096) 

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

254 from hypothesis.internal.scrutineer import ModuleLocation 

255 

256 return ( 

257 # Skip expensive path lookup for stdlib modules. 

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

259 # same as a stdlib module. 

260 path not in sys.stdlib_module_names 

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

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

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

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

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

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

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

268 # ignore here. 

269 and not is_hypothesis_file(path) 

270 # avoid collecting constants from test files 

271 and not ( 

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

273 or "tests" in p.parts 

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

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

276 ) 

277 )