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

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

247 statements  

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"""astroid manager: avoid multiple astroid build of a same module when 

6possible by providing a class responsible to get astroid representation 

7from various source and using a cache of built modules) 

8""" 

9 

10from __future__ import annotations 

11 

12import collections 

13import os 

14import types 

15import zipimport 

16from collections.abc import Callable, Iterator, Sequence 

17from typing import Any, ClassVar 

18 

19from astroid import nodes 

20from astroid.context import InferenceContext, _invalidate_cache 

21from astroid.exceptions import AstroidBuildingError, AstroidImportError 

22from astroid.interpreter._import import spec, util 

23from astroid.modutils import ( 

24 NoSourceFile, 

25 _cache_normalize_path_, 

26 file_info_from_modpath, 

27 get_source_file, 

28 is_module_name_part_of_extension_package_whitelist, 

29 is_python_source, 

30 is_stdlib_module, 

31 load_module_from_name, 

32 modpath_from_file, 

33) 

34from astroid.transforms import TransformVisitor 

35from astroid.typing import AstroidManagerBrain, InferenceResult 

36 

37ZIP_IMPORT_EXTS = (".zip", ".egg", ".whl", ".pyz", ".pyzw") 

38 

39 

40def safe_repr(obj: Any) -> str: 

41 try: 

42 return repr(obj) 

43 except Exception: # pylint: disable=broad-except 

44 return "???" 

45 

46 

47class AstroidManager: 

48 """Responsible to build astroid from files or modules. 

49 

50 Use the Borg (singleton) pattern. 

51 """ 

52 

53 name = "astroid loader" 

54 brain: ClassVar[AstroidManagerBrain] = { 

55 "astroid_cache": {}, 

56 "_mod_file_cache": {}, 

57 "_failed_import_hooks": [], 

58 "always_load_extensions": False, 

59 "optimize_ast": False, 

60 "max_inferable_values": 100, 

61 "extension_package_whitelist": set(), 

62 "module_denylist": set(), 

63 "_transform": TransformVisitor(), 

64 "prefer_stubs": False, 

65 } 

66 

67 def __init__(self) -> None: 

68 # NOTE: cache entries are added by the [re]builder 

69 self.astroid_cache = AstroidManager.brain["astroid_cache"] 

70 self._mod_file_cache = AstroidManager.brain["_mod_file_cache"] 

71 self._failed_import_hooks = AstroidManager.brain["_failed_import_hooks"] 

72 self.extension_package_whitelist = AstroidManager.brain[ 

73 "extension_package_whitelist" 

74 ] 

75 self.module_denylist = AstroidManager.brain["module_denylist"] 

76 self._transform = AstroidManager.brain["_transform"] 

77 self.prefer_stubs = AstroidManager.brain["prefer_stubs"] 

78 

79 @property 

80 def always_load_extensions(self) -> bool: 

81 return AstroidManager.brain["always_load_extensions"] 

82 

83 @always_load_extensions.setter 

84 def always_load_extensions(self, value: bool) -> None: 

85 AstroidManager.brain["always_load_extensions"] = value 

86 

87 @property 

88 def optimize_ast(self) -> bool: 

89 return AstroidManager.brain["optimize_ast"] 

90 

91 @optimize_ast.setter 

92 def optimize_ast(self, value: bool) -> None: 

93 AstroidManager.brain["optimize_ast"] = value 

94 

95 @property 

96 def max_inferable_values(self) -> int: 

97 return AstroidManager.brain["max_inferable_values"] 

98 

99 @max_inferable_values.setter 

100 def max_inferable_values(self, value: int) -> None: 

101 AstroidManager.brain["max_inferable_values"] = value 

102 

103 @property 

104 def register_transform(self): 

105 # This and unregister_transform below are exported for convenience 

106 return self._transform.register_transform 

107 

108 @property 

109 def unregister_transform(self): 

110 return self._transform.unregister_transform 

111 

112 @property 

113 def builtins_module(self) -> nodes.Module: 

114 return self.astroid_cache["builtins"] 

115 

116 @property 

117 def prefer_stubs(self) -> bool: 

118 return AstroidManager.brain["prefer_stubs"] 

119 

120 @prefer_stubs.setter 

121 def prefer_stubs(self, value: bool) -> None: 

122 AstroidManager.brain["prefer_stubs"] = value 

123 

124 def visit_transforms(self, node: nodes.NodeNG) -> InferenceResult: 

125 """Visit the transforms and apply them to the given *node*.""" 

126 return self._transform.visit(node) 

127 

128 def ast_from_file( 

129 self, 

130 filepath: str, 

131 modname: str | None = None, 

132 fallback: bool = True, 

133 source: bool = False, 

134 ) -> nodes.Module: 

135 """Given a module name, return the astroid object.""" 

136 if modname is None: 

137 try: 

138 modname = ".".join(modpath_from_file(filepath)) 

139 except ImportError: 

140 modname = filepath 

141 if ( 

142 modname in self.astroid_cache 

143 and self.astroid_cache[modname].file == filepath 

144 ): 

145 return self.astroid_cache[modname] 

146 # Call get_source_file() only after a cache miss, 

147 # since it calls os.path.exists(). 

148 try: 

149 filepath = get_source_file( 

150 filepath, include_no_ext=True, prefer_stubs=self.prefer_stubs 

151 ) 

152 source = True 

153 except NoSourceFile: 

154 pass 

155 # Second attempt on the cache after get_source_file(). 

156 if ( 

157 modname in self.astroid_cache 

158 and self.astroid_cache[modname].file == filepath 

159 ): 

160 return self.astroid_cache[modname] 

161 if source: 

162 # pylint: disable=import-outside-toplevel; circular import 

163 from astroid.builder import AstroidBuilder 

164 

165 return AstroidBuilder(self).file_build(filepath, modname) 

166 if fallback and modname: 

167 return self.ast_from_module_name(modname) 

168 raise AstroidBuildingError("Unable to build an AST for {path}.", path=filepath) 

169 

170 def ast_from_string( 

171 self, data: str, modname: str = "", filepath: str | None = None 

172 ) -> nodes.Module: 

173 """Given some source code as a string, return its corresponding astroid 

174 object. 

175 """ 

176 # pylint: disable=import-outside-toplevel; circular import 

177 from astroid.builder import AstroidBuilder 

178 

179 return AstroidBuilder(self).string_build(data, modname, filepath) 

180 

181 def _build_stub_module(self, modname: str) -> nodes.Module: 

182 # pylint: disable=import-outside-toplevel; circular import 

183 from astroid.builder import AstroidBuilder 

184 

185 return AstroidBuilder(self).string_build("", modname) 

186 

187 def _build_namespace_module( 

188 self, modname: str, path: Sequence[str] 

189 ) -> nodes.Module: 

190 # pylint: disable=import-outside-toplevel; circular import 

191 from astroid.builder import build_namespace_package_module 

192 

193 return build_namespace_package_module(modname, path) 

194 

195 def _can_load_extension(self, modname: str) -> bool: 

196 if self.always_load_extensions: 

197 return True 

198 if is_stdlib_module(modname): 

199 return True 

200 return is_module_name_part_of_extension_package_whitelist( 

201 modname, self.extension_package_whitelist 

202 ) 

203 

204 def ast_from_module_name( # noqa: C901 

205 self, 

206 modname: str | None, 

207 context_file: str | None = None, 

208 use_cache: bool = True, 

209 ) -> nodes.Module: 

210 """Given a module name, return the astroid object.""" 

211 if modname is None: 

212 raise AstroidBuildingError("No module name given.") 

213 # Sometimes we don't want to use the cache. For example, when we're 

214 # importing a module with the same name as the file that is importing 

215 # we want to fallback on the import system to make sure we get the correct 

216 # module. 

217 if modname in self.module_denylist: 

218 raise AstroidImportError(f"Skipping ignored module {modname!r}") 

219 if modname in self.astroid_cache and use_cache: 

220 return self.astroid_cache[modname] 

221 if modname == "__main__": 

222 return self._build_stub_module(modname) 

223 if context_file: 

224 old_cwd = os.getcwd() 

225 os.chdir(os.path.dirname(context_file)) 

226 try: 

227 found_spec = self.file_from_module_name(modname, context_file) 

228 if found_spec.type == spec.ModuleType.PY_ZIPMODULE: 

229 module = self.zip_import_data(found_spec.location) 

230 if module is not None: 

231 return module 

232 

233 elif found_spec.type in ( 

234 spec.ModuleType.C_BUILTIN, 

235 spec.ModuleType.C_EXTENSION, 

236 ): 

237 if ( 

238 found_spec.type == spec.ModuleType.C_EXTENSION 

239 and not self._can_load_extension(modname) 

240 ): 

241 return self._build_stub_module(modname) 

242 try: 

243 named_module = load_module_from_name(modname) 

244 except Exception as e: 

245 raise AstroidImportError( 

246 "Loading {modname} failed with:\n{error}", 

247 modname=modname, 

248 path=found_spec.location, 

249 ) from e 

250 return self.ast_from_module(named_module, modname) 

251 

252 elif found_spec.type == spec.ModuleType.PY_COMPILED: 

253 raise AstroidImportError( 

254 "Unable to load compiled module {modname}.", 

255 modname=modname, 

256 path=found_spec.location, 

257 ) 

258 

259 elif found_spec.type == spec.ModuleType.PY_NAMESPACE: 

260 return self._build_namespace_module( 

261 modname, found_spec.submodule_search_locations or [] 

262 ) 

263 elif found_spec.type == spec.ModuleType.PY_FROZEN: 

264 if found_spec.location is None: 

265 return self._build_stub_module(modname) 

266 # For stdlib frozen modules we can determine the location and 

267 # can therefore create a module from the source file 

268 return self.ast_from_file(found_spec.location, modname, fallback=False) 

269 

270 if found_spec.location is None: 

271 raise AstroidImportError( 

272 "Can't find a file for module {modname}.", modname=modname 

273 ) 

274 

275 return self.ast_from_file(found_spec.location, modname, fallback=False) 

276 except AstroidBuildingError as e: 

277 for hook in self._failed_import_hooks: 

278 try: 

279 return hook(modname) 

280 except AstroidBuildingError: 

281 pass 

282 raise e 

283 finally: 

284 if context_file: 

285 os.chdir(old_cwd) 

286 

287 def zip_import_data(self, filepath: str) -> nodes.Module | None: 

288 if zipimport is None: 

289 return None 

290 

291 # pylint: disable=import-outside-toplevel; circular import 

292 from astroid.builder import AstroidBuilder 

293 

294 builder = AstroidBuilder(self) 

295 for ext in ZIP_IMPORT_EXTS: 

296 try: 

297 eggpath, resource = filepath.rsplit(ext + os.path.sep, 1) 

298 except ValueError: 

299 continue 

300 try: 

301 importer = zipimport.zipimporter(eggpath + ext) 

302 zmodname = resource.replace(os.path.sep, ".") 

303 if importer.is_package(resource): 

304 zmodname = zmodname + ".__init__" 

305 module = builder.string_build( 

306 importer.get_source(resource), zmodname, filepath 

307 ) 

308 return module 

309 except Exception: # pylint: disable=broad-except 

310 continue 

311 return None 

312 

313 def file_from_module_name( 

314 self, modname: str, contextfile: str | None 

315 ) -> spec.ModuleSpec: 

316 try: 

317 value = self._mod_file_cache[(modname, contextfile)] 

318 except KeyError: 

319 try: 

320 value = file_info_from_modpath( 

321 modname.split("."), context_file=contextfile 

322 ) 

323 except ImportError as e: 

324 value = AstroidImportError( 

325 "Failed to import module {modname} with error:\n{error}.", 

326 modname=modname, 

327 # we remove the traceback here to save on memory usage (since these exceptions are cached) 

328 error=e.with_traceback(None), 

329 ) 

330 self._mod_file_cache[(modname, contextfile)] = value 

331 if isinstance(value, AstroidBuildingError): 

332 # we remove the traceback here to save on memory usage (since these exceptions are cached) 

333 raise value.with_traceback(None) # pylint: disable=no-member 

334 return value 

335 

336 def ast_from_module( 

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

338 ) -> nodes.Module: 

339 """Given an imported module, return the astroid object.""" 

340 modname = modname or module.__name__ 

341 if modname in self.astroid_cache: 

342 return self.astroid_cache[modname] 

343 try: 

344 # some builtin modules don't have __file__ attribute 

345 filepath = module.__file__ 

346 if is_python_source(filepath): 

347 # Type is checked in is_python_source 

348 return self.ast_from_file(filepath, modname) # type: ignore[arg-type] 

349 except AttributeError: 

350 pass 

351 

352 # pylint: disable=import-outside-toplevel; circular import 

353 from astroid.builder import AstroidBuilder 

354 

355 return AstroidBuilder(self).module_build(module, modname) 

356 

357 def ast_from_class(self, klass: type, modname: str | None = None) -> nodes.ClassDef: 

358 """Get astroid for the given class.""" 

359 if modname is None: 

360 try: 

361 modname = klass.__module__ 

362 except AttributeError as exc: 

363 raise AstroidBuildingError( 

364 "Unable to get module for class {class_name}.", 

365 cls=klass, 

366 class_repr=safe_repr(klass), 

367 modname=modname, 

368 ) from exc 

369 modastroid = self.ast_from_module_name(modname) 

370 ret = modastroid.getattr(klass.__name__)[0] 

371 assert isinstance(ret, nodes.ClassDef) 

372 return ret 

373 

374 def infer_ast_from_something( 

375 self, obj: object, context: InferenceContext | None = None 

376 ) -> Iterator[InferenceResult]: 

377 """Infer astroid for the given class.""" 

378 if hasattr(obj, "__class__") and not isinstance(obj, type): 

379 klass = obj.__class__ 

380 elif isinstance(obj, type): 

381 klass = obj 

382 else: 

383 raise AstroidBuildingError( # pragma: no cover 

384 "Unable to get type for {class_repr}.", 

385 cls=None, 

386 class_repr=safe_repr(obj), 

387 ) 

388 try: 

389 modname = klass.__module__ 

390 except AttributeError as exc: 

391 raise AstroidBuildingError( 

392 "Unable to get module for {class_repr}.", 

393 cls=klass, 

394 class_repr=safe_repr(klass), 

395 ) from exc 

396 except Exception as exc: 

397 raise AstroidImportError( 

398 "Unexpected error while retrieving module for {class_repr}:\n" 

399 "{error}", 

400 cls=klass, 

401 class_repr=safe_repr(klass), 

402 ) from exc 

403 try: 

404 name = klass.__name__ 

405 except AttributeError as exc: 

406 raise AstroidBuildingError( 

407 "Unable to get name for {class_repr}:\n", 

408 cls=klass, 

409 class_repr=safe_repr(klass), 

410 ) from exc 

411 except Exception as exc: 

412 raise AstroidImportError( 

413 "Unexpected error while retrieving name for {class_repr}:\n{error}", 

414 cls=klass, 

415 class_repr=safe_repr(klass), 

416 ) from exc 

417 # take care, on living object __module__ is regularly wrong :( 

418 modastroid = self.ast_from_module_name(modname) 

419 if klass is obj: 

420 yield from modastroid.igetattr(name, context) 

421 else: 

422 for inferred in modastroid.igetattr(name, context): 

423 yield inferred.instantiate_class() 

424 

425 def register_failed_import_hook(self, hook: Callable[[str], nodes.Module]) -> None: 

426 """Registers a hook to resolve imports that cannot be found otherwise. 

427 

428 `hook` must be a function that accepts a single argument `modname` which 

429 contains the name of the module or package that could not be imported. 

430 If `hook` can resolve the import, must return a node of type `astroid.Module`, 

431 otherwise, it must raise `AstroidBuildingError`. 

432 """ 

433 self._failed_import_hooks.append(hook) 

434 

435 def cache_module(self, module: nodes.Module) -> None: 

436 """Cache a module if no module with the same name is known yet.""" 

437 self.astroid_cache.setdefault(module.name, module) 

438 

439 def bootstrap(self) -> None: 

440 """Bootstrap the required AST modules needed for the manager to work. 

441 

442 The bootstrap usually involves building the AST for the builtins 

443 module, which is required by the rest of astroid to work correctly. 

444 """ 

445 from astroid import raw_building # pylint: disable=import-outside-toplevel 

446 

447 raw_building._astroid_bootstrapping() 

448 

449 def clear_cache(self) -> None: 

450 """Clear the underlying caches, bootstrap the builtins module and 

451 re-register transforms. 

452 """ 

453 # import here because of cyclic imports 

454 # pylint: disable=import-outside-toplevel 

455 from astroid.brain.helpers import register_all_brains 

456 from astroid.inference_tip import clear_inference_tip_cache 

457 from astroid.interpreter._import.spec import _find_spec 

458 from astroid.interpreter.objectmodel import ObjectModel 

459 from astroid.nodes._base_nodes import LookupMixIn 

460 from astroid.nodes.scoped_nodes import ClassDef 

461 

462 clear_inference_tip_cache() 

463 _invalidate_cache() # inference context cache 

464 

465 self.astroid_cache.clear() 

466 # NB: not a new TransformVisitor() 

467 AstroidManager.brain["_transform"].transforms = collections.defaultdict(list) 

468 

469 for lru_cache in ( 

470 LookupMixIn.lookup, 

471 _cache_normalize_path_, 

472 util.is_namespace, 

473 ObjectModel.attributes, 

474 ClassDef._metaclass_lookup_attribute, 

475 _find_spec, 

476 ): 

477 lru_cache.cache_clear() # type: ignore[attr-defined] 

478 

479 self.bootstrap() 

480 

481 # Reload brain plugins. During initialisation this is done in astroid.manager.py 

482 register_all_brains(self)