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

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

250 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 _has_init, 

27 cached_os_path_isfile, 

28 file_info_from_modpath, 

29 get_source_file, 

30 is_module_name_part_of_extension_package_whitelist, 

31 is_python_source, 

32 is_stdlib_module, 

33 load_module_from_name, 

34 modpath_from_file, 

35) 

36from astroid.transforms import TransformVisitor 

37from astroid.typing import AstroidManagerBrain, InferenceResult 

38 

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

40 

41 

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

43 try: 

44 return repr(obj) 

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

46 return "???" 

47 

48 

49class AstroidManager: 

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

51 

52 Use the Borg (singleton) pattern. 

53 """ 

54 

55 name = "astroid loader" 

56 brain: ClassVar[AstroidManagerBrain] = { 

57 "astroid_cache": {}, 

58 "_mod_file_cache": {}, 

59 "_failed_import_hooks": [], 

60 "always_load_extensions": False, 

61 "optimize_ast": False, 

62 "max_inferable_values": 100, 

63 "extension_package_whitelist": set(), 

64 "module_denylist": set(), 

65 "_transform": TransformVisitor(), 

66 "prefer_stubs": False, 

67 } 

68 

69 def __init__(self) -> None: 

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

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

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

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

74 self.extension_package_whitelist = AstroidManager.brain[ 

75 "extension_package_whitelist" 

76 ] 

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

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

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

80 

81 @property 

82 def always_load_extensions(self) -> bool: 

83 return AstroidManager.brain["always_load_extensions"] 

84 

85 @always_load_extensions.setter 

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

87 AstroidManager.brain["always_load_extensions"] = value 

88 

89 @property 

90 def optimize_ast(self) -> bool: 

91 return AstroidManager.brain["optimize_ast"] 

92 

93 @optimize_ast.setter 

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

95 AstroidManager.brain["optimize_ast"] = value 

96 

97 @property 

98 def max_inferable_values(self) -> int: 

99 return AstroidManager.brain["max_inferable_values"] 

100 

101 @max_inferable_values.setter 

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

103 AstroidManager.brain["max_inferable_values"] = value 

104 

105 @property 

106 def register_transform(self): 

107 # This and unregister_transform below are exported for convenience 

108 return self._transform.register_transform 

109 

110 @property 

111 def unregister_transform(self): 

112 return self._transform.unregister_transform 

113 

114 @property 

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

116 return self.astroid_cache["builtins"] 

117 

118 @property 

119 def prefer_stubs(self) -> bool: 

120 return AstroidManager.brain["prefer_stubs"] 

121 

122 @prefer_stubs.setter 

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

124 AstroidManager.brain["prefer_stubs"] = value 

125 

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

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

128 return self._transform.visit(node) 

129 

130 def ast_from_file( 

131 self, 

132 filepath: str, 

133 modname: str | None = None, 

134 fallback: bool = True, 

135 source: bool = False, 

136 ) -> nodes.Module: 

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

138 if modname is None: 

139 try: 

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

141 except ImportError: 

142 modname = filepath 

143 if ( 

144 modname in self.astroid_cache 

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

146 ): 

147 return self.astroid_cache[modname] 

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

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

150 try: 

151 filepath = get_source_file( 

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

153 ) 

154 source = True 

155 except NoSourceFile: 

156 pass 

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

158 if ( 

159 modname in self.astroid_cache 

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

161 ): 

162 return self.astroid_cache[modname] 

163 if source: 

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

165 from astroid.builder import AstroidBuilder 

166 

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

168 if fallback and modname: 

169 return self.ast_from_module_name(modname) 

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

171 

172 def ast_from_string( 

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

174 ) -> nodes.Module: 

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

176 object. 

177 """ 

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

179 from astroid.builder import AstroidBuilder 

180 

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

182 

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

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

185 from astroid.builder import AstroidBuilder 

186 

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

188 

189 def _build_namespace_module( 

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

191 ) -> nodes.Module: 

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

193 from astroid.builder import build_namespace_package_module 

194 

195 return build_namespace_package_module(modname, path) 

196 

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

198 if self.always_load_extensions: 

199 return True 

200 if is_stdlib_module(modname): 

201 return True 

202 return is_module_name_part_of_extension_package_whitelist( 

203 modname, self.extension_package_whitelist 

204 ) 

205 

206 def ast_from_module_name( # noqa: C901 

207 self, 

208 modname: str | None, 

209 context_file: str | None = None, 

210 use_cache: bool = True, 

211 ) -> nodes.Module: 

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

213 if modname is None: 

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

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

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

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

218 # module. 

219 if modname in self.module_denylist: 

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

221 if modname in self.astroid_cache and use_cache: 

222 return self.astroid_cache[modname] 

223 if modname == "__main__": 

224 return self._build_stub_module(modname) 

225 if context_file: 

226 old_cwd = os.getcwd() 

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

228 try: 

229 found_spec = self.file_from_module_name(modname, context_file) 

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

231 module = self.zip_import_data(found_spec.location) 

232 if module is not None: 

233 return module 

234 

235 elif found_spec.type in ( 

236 spec.ModuleType.C_BUILTIN, 

237 spec.ModuleType.C_EXTENSION, 

238 ): 

239 if ( 

240 found_spec.type == spec.ModuleType.C_EXTENSION 

241 and not self._can_load_extension(modname) 

242 ): 

243 return self._build_stub_module(modname) 

244 try: 

245 named_module = load_module_from_name(modname) 

246 except Exception as e: 

247 raise AstroidImportError( 

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

249 modname=modname, 

250 path=found_spec.location, 

251 ) from e 

252 return self.ast_from_module(named_module, modname) 

253 

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

255 raise AstroidImportError( 

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

257 modname=modname, 

258 path=found_spec.location, 

259 ) 

260 

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

262 return self._build_namespace_module( 

263 modname, found_spec.submodule_search_locations or [] 

264 ) 

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

266 if found_spec.location is None: 

267 return self._build_stub_module(modname) 

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

269 # can therefore create a module from the source file 

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

271 

272 if found_spec.location is None: 

273 raise AstroidImportError( 

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

275 ) 

276 

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

278 except AstroidBuildingError as e: 

279 for hook in self._failed_import_hooks: 

280 try: 

281 return hook(modname) 

282 except AstroidBuildingError: 

283 pass 

284 raise e 

285 finally: 

286 if context_file: 

287 os.chdir(old_cwd) 

288 

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

290 if zipimport is None: 

291 return None 

292 

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

294 from astroid.builder import AstroidBuilder 

295 

296 builder = AstroidBuilder(self) 

297 for ext in ZIP_IMPORT_EXTS: 

298 try: 

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

300 except ValueError: 

301 continue 

302 try: 

303 importer = zipimport.zipimporter(eggpath + ext) 

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

305 if importer.is_package(resource): 

306 zmodname = zmodname + ".__init__" 

307 module = builder.string_build( 

308 importer.get_source(resource), zmodname, filepath 

309 ) 

310 return module 

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

312 continue 

313 return None 

314 

315 def file_from_module_name( 

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

317 ) -> spec.ModuleSpec: 

318 try: 

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

320 except KeyError: 

321 try: 

322 value = file_info_from_modpath( 

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

324 ) 

325 except ImportError as e: 

326 value = AstroidImportError( 

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

328 modname=modname, 

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

330 error=e.with_traceback(None), 

331 ) 

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

333 if isinstance(value, AstroidBuildingError): 

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

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

336 return value 

337 

338 def ast_from_module( 

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

340 ) -> nodes.Module: 

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

342 modname = modname or module.__name__ 

343 if modname in self.astroid_cache: 

344 return self.astroid_cache[modname] 

345 try: 

346 # some builtin modules don't have __file__ attribute 

347 filepath = module.__file__ 

348 if is_python_source(filepath): 

349 # Type is checked in is_python_source 

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

351 except AttributeError: 

352 pass 

353 

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

355 from astroid.builder import AstroidBuilder 

356 

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

358 

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

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

361 if modname is None: 

362 try: 

363 modname = klass.__module__ 

364 except AttributeError as exc: 

365 raise AstroidBuildingError( 

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

367 cls=klass, 

368 class_repr=safe_repr(klass), 

369 modname=modname, 

370 ) from exc 

371 modastroid = self.ast_from_module_name(modname) 

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

373 assert isinstance(ret, nodes.ClassDef) 

374 return ret 

375 

376 def infer_ast_from_something( 

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

378 ) -> Iterator[InferenceResult]: 

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

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

381 klass = obj.__class__ 

382 elif isinstance(obj, type): 

383 klass = obj 

384 else: 

385 raise AstroidBuildingError( # pragma: no cover 

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

387 cls=None, 

388 class_repr=safe_repr(obj), 

389 ) 

390 try: 

391 modname = klass.__module__ 

392 except AttributeError as exc: 

393 raise AstroidBuildingError( 

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

395 cls=klass, 

396 class_repr=safe_repr(klass), 

397 ) from exc 

398 except Exception as exc: 

399 raise AstroidImportError( 

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

401 "{error}", 

402 cls=klass, 

403 class_repr=safe_repr(klass), 

404 ) from exc 

405 try: 

406 name = klass.__name__ 

407 except AttributeError as exc: 

408 raise AstroidBuildingError( 

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

410 cls=klass, 

411 class_repr=safe_repr(klass), 

412 ) from exc 

413 except Exception as exc: 

414 raise AstroidImportError( 

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

416 cls=klass, 

417 class_repr=safe_repr(klass), 

418 ) from exc 

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

420 modastroid = self.ast_from_module_name(modname) 

421 if klass is obj: 

422 yield from modastroid.igetattr(name, context) 

423 else: 

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

425 yield inferred.instantiate_class() 

426 

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

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

429 

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

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

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

433 otherwise, it must raise `AstroidBuildingError`. 

434 """ 

435 self._failed_import_hooks.append(hook) 

436 

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

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

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

440 

441 def bootstrap(self) -> None: 

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

443 

444 The bootstrap usually involves building the AST for the builtins 

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

446 """ 

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

448 

449 raw_building._astroid_bootstrapping() 

450 

451 def clear_cache(self) -> None: 

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

453 re-register transforms. 

454 """ 

455 # import here because of cyclic imports 

456 # pylint: disable=import-outside-toplevel 

457 from astroid.brain.helpers import register_all_brains 

458 from astroid.inference_tip import clear_inference_tip_cache 

459 from astroid.interpreter._import.spec import _find_spec 

460 from astroid.interpreter.objectmodel import ObjectModel 

461 from astroid.nodes._base_nodes import LookupMixIn 

462 from astroid.nodes.scoped_nodes import ClassDef 

463 

464 clear_inference_tip_cache() 

465 _invalidate_cache() # inference context cache 

466 

467 self.astroid_cache.clear() 

468 self._mod_file_cache.clear() 

469 

470 # NB: not a new TransformVisitor() 

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

472 

473 for lru_cache in ( 

474 LookupMixIn.lookup, 

475 _cache_normalize_path_, 

476 _has_init, 

477 cached_os_path_isfile, 

478 util.is_namespace, 

479 ObjectModel.attributes, 

480 ClassDef._metaclass_lookup_attribute, 

481 _find_spec, 

482 ): 

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

484 

485 for finder in spec._SPEC_FINDERS: 

486 finder.find_module.cache_clear() 

487 

488 self.bootstrap() 

489 

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

491 register_all_brains(self)