Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/astroid/builder.py: 53%

222 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-07 06:53 +0000

1# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html 

2# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE 

3# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt 

4 

5"""The AstroidBuilder makes astroid from living object and / or from _ast. 

6 

7The builder is not thread safe and can't be used to parse different sources 

8at the same time. 

9""" 

10 

11from __future__ import annotations 

12 

13import ast 

14import os 

15import textwrap 

16import types 

17from collections.abc import Iterator, Sequence 

18from io import TextIOWrapper 

19from tokenize import detect_encoding 

20 

21from astroid import bases, modutils, nodes, raw_building, rebuilder, util 

22from astroid._ast import ParserModule, get_parser_module 

23from astroid.exceptions import AstroidBuildingError, AstroidSyntaxError, InferenceError 

24from astroid.manager import AstroidManager 

25 

26# The name of the transient function that is used to 

27# wrap expressions to be extracted when calling 

28# extract_node. 

29_TRANSIENT_FUNCTION = "__" 

30 

31# The comment used to select a statement to be extracted 

32# when calling extract_node. 

33_STATEMENT_SELECTOR = "#@" 

34MISPLACED_TYPE_ANNOTATION_ERROR = "misplaced type annotation" 

35 

36 

37def open_source_file(filename: str) -> tuple[TextIOWrapper, str, str]: 

38 # pylint: disable=consider-using-with 

39 with open(filename, "rb") as byte_stream: 

40 encoding = detect_encoding(byte_stream.readline)[0] 

41 stream = open(filename, newline=None, encoding=encoding) 

42 data = stream.read() 

43 return stream, encoding, data 

44 

45 

46def _can_assign_attr(node: nodes.ClassDef, attrname: str | None) -> bool: 

47 try: 

48 slots = node.slots() 

49 except NotImplementedError: 

50 pass 

51 else: 

52 if slots and attrname not in {slot.value for slot in slots}: 

53 return False 

54 return node.qname() != "builtins.object" 

55 

56 

57class AstroidBuilder(raw_building.InspectBuilder): 

58 """Class for building an astroid tree from source code or from a live module. 

59 

60 The param *manager* specifies the manager class which should be used. 

61 If no manager is given, then the default one will be used. The 

62 param *apply_transforms* determines if the transforms should be 

63 applied after the tree was built from source or from a live object, 

64 by default being True. 

65 """ 

66 

67 def __init__( 

68 self, manager: AstroidManager | None = None, apply_transforms: bool = True 

69 ) -> None: 

70 super().__init__(manager) 

71 self._apply_transforms = apply_transforms 

72 

73 def module_build( 

74 self, module: types.ModuleType, modname: str | None = None 

75 ) -> nodes.Module: 

76 """Build an astroid from a living module instance.""" 

77 node = None 

78 path = getattr(module, "__file__", None) 

79 loader = getattr(module, "__loader__", None) 

80 # Prefer the loader to get the source rather than assuming we have a 

81 # filesystem to read the source file from ourselves. 

82 if loader: 

83 modname = modname or module.__name__ 

84 source = loader.get_source(modname) 

85 if source: 

86 node = self.string_build(source, modname, path=path) 

87 if node is None and path is not None: 

88 path_, ext = os.path.splitext(modutils._path_from_filename(path)) 

89 if ext in {".py", ".pyc", ".pyo"} and os.path.exists(path_ + ".py"): 

90 node = self.file_build(path_ + ".py", modname) 

91 if node is None: 

92 # this is a built-in module 

93 # get a partial representation by introspection 

94 node = self.inspect_build(module, modname=modname, path=path) 

95 if self._apply_transforms: 

96 # We have to handle transformation by ourselves since the 

97 # rebuilder isn't called for builtin nodes 

98 node = self._manager.visit_transforms(node) 

99 assert isinstance(node, nodes.Module) 

100 return node 

101 

102 def file_build(self, path: str, modname: str | None = None) -> nodes.Module: 

103 """Build astroid from a source code file (i.e. from an ast). 

104 

105 *path* is expected to be a python source file 

106 """ 

107 try: 

108 stream, encoding, data = open_source_file(path) 

109 except OSError as exc: 

110 raise AstroidBuildingError( 

111 "Unable to load file {path}:\n{error}", 

112 modname=modname, 

113 path=path, 

114 error=exc, 

115 ) from exc 

116 except (SyntaxError, LookupError) as exc: 

117 raise AstroidSyntaxError( 

118 "Python 3 encoding specification error or unknown encoding:\n" 

119 "{error}", 

120 modname=modname, 

121 path=path, 

122 error=exc, 

123 ) from exc 

124 except UnicodeError as exc: # wrong encoding 

125 # detect_encoding returns utf-8 if no encoding specified 

126 raise AstroidBuildingError( 

127 "Wrong or no encoding specified for {filename}.", filename=path 

128 ) from exc 

129 with stream: 

130 # get module name if necessary 

131 if modname is None: 

132 try: 

133 modname = ".".join(modutils.modpath_from_file(path)) 

134 except ImportError: 

135 modname = os.path.splitext(os.path.basename(path))[0] 

136 # build astroid representation 

137 module, builder = self._data_build(data, modname, path) 

138 return self._post_build(module, builder, encoding) 

139 

140 def string_build( 

141 self, data: str, modname: str = "", path: str | None = None 

142 ) -> nodes.Module: 

143 """Build astroid from source code string.""" 

144 module, builder = self._data_build(data, modname, path) 

145 module.file_bytes = data.encode("utf-8") 

146 return self._post_build(module, builder, "utf-8") 

147 

148 def _post_build( 

149 self, module: nodes.Module, builder: rebuilder.TreeRebuilder, encoding: str 

150 ) -> nodes.Module: 

151 """Handles encoding and delayed nodes after a module has been built.""" 

152 module.file_encoding = encoding 

153 self._manager.cache_module(module) 

154 # post tree building steps after we stored the module in the cache: 

155 for from_node in builder._import_from_nodes: 

156 if from_node.modname == "__future__": 

157 for symbol, _ in from_node.names: 

158 module.future_imports.add(symbol) 

159 self.add_from_names_to_locals(from_node) 

160 # handle delayed assattr nodes 

161 for delayed in builder._delayed_assattr: 

162 self.delayed_assattr(delayed) 

163 

164 # Visit the transforms 

165 if self._apply_transforms: 

166 module = self._manager.visit_transforms(module) 

167 return module 

168 

169 def _data_build( 

170 self, data: str, modname: str, path: str | None 

171 ) -> tuple[nodes.Module, rebuilder.TreeRebuilder]: 

172 """Build tree node from data and add some informations.""" 

173 try: 

174 node, parser_module = _parse_string(data, type_comments=True) 

175 except (TypeError, ValueError, SyntaxError) as exc: 

176 raise AstroidSyntaxError( 

177 "Parsing Python code failed:\n{error}", 

178 source=data, 

179 modname=modname, 

180 path=path, 

181 error=exc, 

182 ) from exc 

183 

184 if path is not None: 

185 node_file = os.path.abspath(path) 

186 else: 

187 node_file = "<?>" 

188 if modname.endswith(".__init__"): 

189 modname = modname[:-9] 

190 package = True 

191 else: 

192 package = ( 

193 path is not None 

194 and os.path.splitext(os.path.basename(path))[0] == "__init__" 

195 ) 

196 builder = rebuilder.TreeRebuilder(self._manager, parser_module, data) 

197 module = builder.visit_module(node, modname, node_file, package) 

198 return module, builder 

199 

200 def add_from_names_to_locals(self, node: nodes.ImportFrom) -> None: 

201 """Store imported names to the locals. 

202 

203 Resort the locals if coming from a delayed node 

204 """ 

205 

206 def _key_func(node: nodes.NodeNG) -> int: 

207 return node.fromlineno or 0 

208 

209 def sort_locals(my_list: list[nodes.NodeNG]) -> None: 

210 my_list.sort(key=_key_func) 

211 

212 assert node.parent # It should always default to the module 

213 for name, asname in node.names: 

214 if name == "*": 

215 try: 

216 imported = node.do_import_module() 

217 except AstroidBuildingError: 

218 continue 

219 for name in imported.public_names(): 

220 node.parent.set_local(name, node) 

221 sort_locals(node.parent.scope().locals[name]) # type: ignore[arg-type] 

222 else: 

223 node.parent.set_local(asname or name, node) 

224 sort_locals(node.parent.scope().locals[asname or name]) # type: ignore[arg-type] 

225 

226 def delayed_assattr(self, node: nodes.AssignAttr) -> None: 

227 """Visit a AssAttr node. 

228 

229 This adds name to locals and handle members definition. 

230 """ 

231 from astroid import objects # pylint: disable=import-outside-toplevel 

232 

233 try: 

234 frame = node.frame(future=True) 

235 for inferred in node.expr.infer(): 

236 if isinstance(inferred, util.UninferableBase): 

237 continue 

238 try: 

239 # pylint: disable=unidiomatic-typecheck # We want a narrow check on the 

240 # parent type, not all of its subclasses 

241 if ( 

242 type(inferred) == bases.Instance 

243 or type(inferred) == objects.ExceptionInstance 

244 ): 

245 inferred = inferred._proxied 

246 iattrs = inferred.instance_attrs 

247 if not _can_assign_attr(inferred, node.attrname): 

248 continue 

249 elif isinstance(inferred, bases.Instance): 

250 # Const, Tuple or other containers that inherit from 

251 # `Instance` 

252 continue 

253 elif isinstance(inferred, (bases.Proxy, util.UninferableBase)): 

254 continue 

255 elif inferred.is_function: 

256 iattrs = inferred.instance_attrs 

257 else: 

258 iattrs = inferred.locals 

259 except AttributeError: 

260 # XXX log error 

261 continue 

262 values = iattrs.setdefault(node.attrname, []) 

263 if node in values: 

264 continue 

265 # get assign in __init__ first XXX useful ? 

266 if ( 

267 frame.name == "__init__" 

268 and values 

269 and values[0].frame(future=True).name != "__init__" 

270 ): 

271 values.insert(0, node) 

272 else: 

273 values.append(node) 

274 except InferenceError: 

275 pass 

276 

277 

278def build_namespace_package_module(name: str, path: Sequence[str]) -> nodes.Module: 

279 module = nodes.Module(name, path=path, package=True) 

280 module.postinit(body=[], doc_node=None) 

281 return module 

282 

283 

284def parse( 

285 code: str, 

286 module_name: str = "", 

287 path: str | None = None, 

288 apply_transforms: bool = True, 

289) -> nodes.Module: 

290 """Parses a source string in order to obtain an astroid AST from it. 

291 

292 :param str code: The code for the module. 

293 :param str module_name: The name for the module, if any 

294 :param str path: The path for the module 

295 :param bool apply_transforms: 

296 Apply the transforms for the give code. Use it if you 

297 don't want the default transforms to be applied. 

298 """ 

299 code = textwrap.dedent(code) 

300 builder = AstroidBuilder( 

301 manager=AstroidManager(), apply_transforms=apply_transforms 

302 ) 

303 return builder.string_build(code, modname=module_name, path=path) 

304 

305 

306def _extract_expressions(node: nodes.NodeNG) -> Iterator[nodes.NodeNG]: 

307 """Find expressions in a call to _TRANSIENT_FUNCTION and extract them. 

308 

309 The function walks the AST recursively to search for expressions that 

310 are wrapped into a call to _TRANSIENT_FUNCTION. If it finds such an 

311 expression, it completely removes the function call node from the tree, 

312 replacing it by the wrapped expression inside the parent. 

313 

314 :param node: An astroid node. 

315 :type node: astroid.bases.NodeNG 

316 :yields: The sequence of wrapped expressions on the modified tree 

317 expression can be found. 

318 """ 

319 if ( 

320 isinstance(node, nodes.Call) 

321 and isinstance(node.func, nodes.Name) 

322 and node.func.name == _TRANSIENT_FUNCTION 

323 ): 

324 real_expr = node.args[0] 

325 assert node.parent 

326 real_expr.parent = node.parent 

327 # Search for node in all _astng_fields (the fields checked when 

328 # get_children is called) of its parent. Some of those fields may 

329 # be lists or tuples, in which case the elements need to be checked. 

330 # When we find it, replace it by real_expr, so that the AST looks 

331 # like no call to _TRANSIENT_FUNCTION ever took place. 

332 for name in node.parent._astroid_fields: 

333 child = getattr(node.parent, name) 

334 if isinstance(child, list): 

335 for idx, compound_child in enumerate(child): 

336 if compound_child is node: 

337 child[idx] = real_expr 

338 elif child is node: 

339 setattr(node.parent, name, real_expr) 

340 yield real_expr 

341 else: 

342 for child in node.get_children(): 

343 yield from _extract_expressions(child) 

344 

345 

346def _find_statement_by_line(node: nodes.NodeNG, line: int) -> nodes.NodeNG | None: 

347 """Extracts the statement on a specific line from an AST. 

348 

349 If the line number of node matches line, it will be returned; 

350 otherwise its children are iterated and the function is called 

351 recursively. 

352 

353 :param node: An astroid node. 

354 :type node: astroid.bases.NodeNG 

355 :param line: The line number of the statement to extract. 

356 :type line: int 

357 :returns: The statement on the line, or None if no statement for the line 

358 can be found. 

359 :rtype: astroid.bases.NodeNG or None 

360 """ 

361 if isinstance(node, (nodes.ClassDef, nodes.FunctionDef, nodes.MatchCase)): 

362 # This is an inaccuracy in the AST: the nodes that can be 

363 # decorated do not carry explicit information on which line 

364 # the actual definition (class/def), but .fromline seems to 

365 # be close enough. 

366 node_line = node.fromlineno 

367 else: 

368 node_line = node.lineno 

369 

370 if node_line == line: 

371 return node 

372 

373 for child in node.get_children(): 

374 result = _find_statement_by_line(child, line) 

375 if result: 

376 return result 

377 

378 return None 

379 

380 

381def extract_node(code: str, module_name: str = "") -> nodes.NodeNG | list[nodes.NodeNG]: 

382 """Parses some Python code as a module and extracts a designated AST node. 

383 

384 Statements: 

385 To extract one or more statement nodes, append #@ to the end of the line 

386 

387 Examples: 

388 >>> def x(): 

389 >>> def y(): 

390 >>> return 1 #@ 

391 

392 The return statement will be extracted. 

393 

394 >>> class X(object): 

395 >>> def meth(self): #@ 

396 >>> pass 

397 

398 The function object 'meth' will be extracted. 

399 

400 Expressions: 

401 To extract arbitrary expressions, surround them with the fake 

402 function call __(...). After parsing, the surrounded expression 

403 will be returned and the whole AST (accessible via the returned 

404 node's parent attribute) will look like the function call was 

405 never there in the first place. 

406 

407 Examples: 

408 >>> a = __(1) 

409 

410 The const node will be extracted. 

411 

412 >>> def x(d=__(foo.bar)): pass 

413 

414 The node containing the default argument will be extracted. 

415 

416 >>> def foo(a, b): 

417 >>> return 0 < __(len(a)) < b 

418 

419 The node containing the function call 'len' will be extracted. 

420 

421 If no statements or expressions are selected, the last toplevel 

422 statement will be returned. 

423 

424 If the selected statement is a discard statement, (i.e. an expression 

425 turned into a statement), the wrapped expression is returned instead. 

426 

427 For convenience, singleton lists are unpacked. 

428 

429 :param str code: A piece of Python code that is parsed as 

430 a module. Will be passed through textwrap.dedent first. 

431 :param str module_name: The name of the module. 

432 :returns: The designated node from the parse tree, or a list of nodes. 

433 """ 

434 

435 def _extract(node: nodes.NodeNG | None) -> nodes.NodeNG | None: 

436 if isinstance(node, nodes.Expr): 

437 return node.value 

438 

439 return node 

440 

441 requested_lines: list[int] = [] 

442 for idx, line in enumerate(code.splitlines()): 

443 if line.strip().endswith(_STATEMENT_SELECTOR): 

444 requested_lines.append(idx + 1) 

445 

446 tree = parse(code, module_name=module_name) 

447 if not tree.body: 

448 raise ValueError("Empty tree, cannot extract from it") 

449 

450 extracted: list[nodes.NodeNG | None] = [] 

451 if requested_lines: 

452 extracted = [_find_statement_by_line(tree, line) for line in requested_lines] 

453 

454 # Modifies the tree. 

455 extracted.extend(_extract_expressions(tree)) 

456 

457 if not extracted: 

458 extracted.append(tree.body[-1]) 

459 

460 extracted = [_extract(node) for node in extracted] 

461 extracted_without_none = [node for node in extracted if node is not None] 

462 if len(extracted_without_none) == 1: 

463 return extracted_without_none[0] 

464 return extracted_without_none 

465 

466 

467def _extract_single_node(code: str, module_name: str = "") -> nodes.NodeNG: 

468 """Call extract_node while making sure that only one value is returned.""" 

469 ret = extract_node(code, module_name) 

470 if isinstance(ret, list): 

471 return ret[0] 

472 return ret 

473 

474 

475def _parse_string( 

476 data: str, type_comments: bool = True 

477) -> tuple[ast.Module, ParserModule]: 

478 parser_module = get_parser_module(type_comments=type_comments) 

479 try: 

480 parsed = parser_module.parse(data + "\n", type_comments=type_comments) 

481 except SyntaxError as exc: 

482 # If the type annotations are misplaced for some reason, we do not want 

483 # to fail the entire parsing of the file, so we need to retry the parsing without 

484 # type comment support. 

485 if exc.args[0] != MISPLACED_TYPE_ANNOTATION_ERROR or not type_comments: 

486 raise 

487 

488 parser_module = get_parser_module(type_comments=False) 

489 parsed = parser_module.parse(data + "\n", type_comments=False) 

490 return parsed, parser_module