manager.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. # Copyright (c) 2006-2011, 2013-2014 LOGILAB S.A. (Paris, FRANCE) <contact@logilab.fr>
  2. # Copyright (c) 2014-2020 Claudiu Popa <pcmanticore@gmail.com>
  3. # Copyright (c) 2014 BioGeek <jeroen.vangoey@gmail.com>
  4. # Copyright (c) 2014 Google, Inc.
  5. # Copyright (c) 2014 Eevee (Alex Munroe) <amunroe@yelp.com>
  6. # Copyright (c) 2015-2016 Ceridwen <ceridwenv@gmail.com>
  7. # Copyright (c) 2016 Derek Gustafson <degustaf@gmail.com>
  8. # Copyright (c) 2017 Iva Miholic <ivamiho@gmail.com>
  9. # Copyright (c) 2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
  10. # Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
  11. # Copyright (c) 2019 Raphael Gaschignard <raphael@makeleaps.com>
  12. # Copyright (c) 2020-2021 hippo91 <guillaume.peillex@gmail.com>
  13. # Copyright (c) 2020 Raphael Gaschignard <raphael@rtpg.co>
  14. # Copyright (c) 2020 Anubhav <35621759+anubh-v@users.noreply.github.com>
  15. # Copyright (c) 2020 Ashley Whetter <ashley@awhetter.co.uk>
  16. # Copyright (c) 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
  17. # Copyright (c) 2021 Daniël van Noord <13665637+DanielNoord@users.noreply.github.com>
  18. # Copyright (c) 2021 grayjk <grayjk@gmail.com>
  19. # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
  20. # Copyright (c) 2021 Andrew Haigh <hello@nelf.in>
  21. # Copyright (c) 2021 DudeNr33 <3929834+DudeNr33@users.noreply.github.com>
  22. # Copyright (c) 2021 pre-commit-ci[bot] <bot@noreply.github.com>
  23. # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
  24. # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
  25. """astroid manager: avoid multiple astroid build of a same module when
  26. possible by providing a class responsible to get astroid representation
  27. from various source and using a cache of built modules)
  28. """
  29. import os
  30. import types
  31. import zipimport
  32. from typing import TYPE_CHECKING, ClassVar, List, Optional
  33. from astroid.exceptions import AstroidBuildingError, AstroidImportError
  34. from astroid.interpreter._import import spec
  35. from astroid.modutils import (
  36. NoSourceFile,
  37. file_info_from_modpath,
  38. get_source_file,
  39. is_module_name_part_of_extension_package_whitelist,
  40. is_python_source,
  41. is_standard_module,
  42. load_module_from_name,
  43. modpath_from_file,
  44. )
  45. from astroid.transforms import TransformVisitor
  46. if TYPE_CHECKING:
  47. from astroid import nodes
  48. ZIP_IMPORT_EXTS = (".zip", ".egg", ".whl", ".pyz", ".pyzw")
  49. def safe_repr(obj):
  50. try:
  51. return repr(obj)
  52. except Exception: # pylint: disable=broad-except
  53. return "???"
  54. class AstroidManager:
  55. """Responsible to build astroid from files or modules.
  56. Use the Borg (singleton) pattern.
  57. """
  58. name = "astroid loader"
  59. brain = {}
  60. max_inferable_values: ClassVar[int] = 100
  61. def __init__(self):
  62. self.__dict__ = AstroidManager.brain
  63. if not self.__dict__:
  64. # NOTE: cache entries are added by the [re]builder
  65. self.astroid_cache = {}
  66. self._mod_file_cache = {}
  67. self._failed_import_hooks = []
  68. self.always_load_extensions = False
  69. self.optimize_ast = False
  70. self.extension_package_whitelist = set()
  71. self._transform = TransformVisitor()
  72. @property
  73. def register_transform(self):
  74. # This and unregister_transform below are exported for convenience
  75. return self._transform.register_transform
  76. @property
  77. def unregister_transform(self):
  78. return self._transform.unregister_transform
  79. @property
  80. def builtins_module(self):
  81. return self.astroid_cache["builtins"]
  82. def visit_transforms(self, node):
  83. """Visit the transforms and apply them to the given *node*."""
  84. return self._transform.visit(node)
  85. def ast_from_file(self, filepath, modname=None, fallback=True, source=False):
  86. """given a module name, return the astroid object"""
  87. try:
  88. filepath = get_source_file(filepath, include_no_ext=True)
  89. source = True
  90. except NoSourceFile:
  91. pass
  92. if modname is None:
  93. try:
  94. modname = ".".join(modpath_from_file(filepath))
  95. except ImportError:
  96. modname = filepath
  97. if (
  98. modname in self.astroid_cache
  99. and self.astroid_cache[modname].file == filepath
  100. ):
  101. return self.astroid_cache[modname]
  102. if source:
  103. # pylint: disable=import-outside-toplevel; circular import
  104. from astroid.builder import AstroidBuilder
  105. return AstroidBuilder(self).file_build(filepath, modname)
  106. if fallback and modname:
  107. return self.ast_from_module_name(modname)
  108. raise AstroidBuildingError("Unable to build an AST for {path}.", path=filepath)
  109. def ast_from_string(self, data, modname="", filepath=None):
  110. """Given some source code as a string, return its corresponding astroid object"""
  111. # pylint: disable=import-outside-toplevel; circular import
  112. from astroid.builder import AstroidBuilder
  113. return AstroidBuilder(self).string_build(data, modname, filepath)
  114. def _build_stub_module(self, modname):
  115. # pylint: disable=import-outside-toplevel; circular import
  116. from astroid.builder import AstroidBuilder
  117. return AstroidBuilder(self).string_build("", modname)
  118. def _build_namespace_module(self, modname: str, path: List[str]) -> "nodes.Module":
  119. # pylint: disable=import-outside-toplevel; circular import
  120. from astroid.builder import build_namespace_package_module
  121. return build_namespace_package_module(modname, path)
  122. def _can_load_extension(self, modname: str) -> bool:
  123. if self.always_load_extensions:
  124. return True
  125. if is_standard_module(modname):
  126. return True
  127. return is_module_name_part_of_extension_package_whitelist(
  128. modname, self.extension_package_whitelist
  129. )
  130. def ast_from_module_name(self, modname, context_file=None):
  131. """given a module name, return the astroid object"""
  132. if modname in self.astroid_cache:
  133. return self.astroid_cache[modname]
  134. if modname == "__main__":
  135. return self._build_stub_module(modname)
  136. if context_file:
  137. old_cwd = os.getcwd()
  138. os.chdir(os.path.dirname(context_file))
  139. try:
  140. found_spec = self.file_from_module_name(modname, context_file)
  141. if found_spec.type == spec.ModuleType.PY_ZIPMODULE:
  142. module = self.zip_import_data(found_spec.location)
  143. if module is not None:
  144. return module
  145. elif found_spec.type in (
  146. spec.ModuleType.C_BUILTIN,
  147. spec.ModuleType.C_EXTENSION,
  148. ):
  149. if (
  150. found_spec.type == spec.ModuleType.C_EXTENSION
  151. and not self._can_load_extension(modname)
  152. ):
  153. return self._build_stub_module(modname)
  154. try:
  155. module = load_module_from_name(modname)
  156. except Exception as e:
  157. raise AstroidImportError(
  158. "Loading {modname} failed with:\n{error}",
  159. modname=modname,
  160. path=found_spec.location,
  161. ) from e
  162. return self.ast_from_module(module, modname)
  163. elif found_spec.type == spec.ModuleType.PY_COMPILED:
  164. raise AstroidImportError(
  165. "Unable to load compiled module {modname}.",
  166. modname=modname,
  167. path=found_spec.location,
  168. )
  169. elif found_spec.type == spec.ModuleType.PY_NAMESPACE:
  170. return self._build_namespace_module(
  171. modname, found_spec.submodule_search_locations
  172. )
  173. elif found_spec.type == spec.ModuleType.PY_FROZEN:
  174. return self._build_stub_module(modname)
  175. if found_spec.location is None:
  176. raise AstroidImportError(
  177. "Can't find a file for module {modname}.", modname=modname
  178. )
  179. return self.ast_from_file(found_spec.location, modname, fallback=False)
  180. except AstroidBuildingError as e:
  181. for hook in self._failed_import_hooks:
  182. try:
  183. return hook(modname)
  184. except AstroidBuildingError:
  185. pass
  186. raise e
  187. finally:
  188. if context_file:
  189. os.chdir(old_cwd)
  190. def zip_import_data(self, filepath):
  191. if zipimport is None:
  192. return None
  193. # pylint: disable=import-outside-toplevel; circular import
  194. from astroid.builder import AstroidBuilder
  195. builder = AstroidBuilder(self)
  196. for ext in ZIP_IMPORT_EXTS:
  197. try:
  198. eggpath, resource = filepath.rsplit(ext + os.path.sep, 1)
  199. except ValueError:
  200. continue
  201. try:
  202. # pylint: disable-next=no-member
  203. importer = zipimport.zipimporter(eggpath + ext)
  204. zmodname = resource.replace(os.path.sep, ".")
  205. if importer.is_package(resource):
  206. zmodname = zmodname + ".__init__"
  207. module = builder.string_build(
  208. importer.get_source(resource), zmodname, filepath
  209. )
  210. return module
  211. except Exception: # pylint: disable=broad-except
  212. continue
  213. return None
  214. def file_from_module_name(self, modname, contextfile):
  215. try:
  216. value = self._mod_file_cache[(modname, contextfile)]
  217. except KeyError:
  218. try:
  219. value = file_info_from_modpath(
  220. modname.split("."), context_file=contextfile
  221. )
  222. except ImportError as e:
  223. value = AstroidImportError(
  224. "Failed to import module {modname} with error:\n{error}.",
  225. modname=modname,
  226. # we remove the traceback here to save on memory usage (since these exceptions are cached)
  227. error=e.with_traceback(None),
  228. )
  229. self._mod_file_cache[(modname, contextfile)] = value
  230. if isinstance(value, AstroidBuildingError):
  231. # we remove the traceback here to save on memory usage (since these exceptions are cached)
  232. raise value.with_traceback(None)
  233. return value
  234. def ast_from_module(self, module: types.ModuleType, modname: Optional[str] = None):
  235. """given an imported module, return the astroid object"""
  236. modname = modname or module.__name__
  237. if modname in self.astroid_cache:
  238. return self.astroid_cache[modname]
  239. try:
  240. # some builtin modules don't have __file__ attribute
  241. filepath = module.__file__
  242. if is_python_source(filepath):
  243. return self.ast_from_file(filepath, modname)
  244. except AttributeError:
  245. pass
  246. # pylint: disable=import-outside-toplevel; circular import
  247. from astroid.builder import AstroidBuilder
  248. return AstroidBuilder(self).module_build(module, modname)
  249. def ast_from_class(self, klass, modname=None):
  250. """get astroid for the given class"""
  251. if modname is None:
  252. try:
  253. modname = klass.__module__
  254. except AttributeError as exc:
  255. raise AstroidBuildingError(
  256. "Unable to get module for class {class_name}.",
  257. cls=klass,
  258. class_repr=safe_repr(klass),
  259. modname=modname,
  260. ) from exc
  261. modastroid = self.ast_from_module_name(modname)
  262. return modastroid.getattr(klass.__name__)[0] # XXX
  263. def infer_ast_from_something(self, obj, context=None):
  264. """infer astroid for the given class"""
  265. if hasattr(obj, "__class__") and not isinstance(obj, type):
  266. klass = obj.__class__
  267. else:
  268. klass = obj
  269. try:
  270. modname = klass.__module__
  271. except AttributeError as exc:
  272. raise AstroidBuildingError(
  273. "Unable to get module for {class_repr}.",
  274. cls=klass,
  275. class_repr=safe_repr(klass),
  276. ) from exc
  277. except Exception as exc:
  278. raise AstroidImportError(
  279. "Unexpected error while retrieving module for {class_repr}:\n"
  280. "{error}",
  281. cls=klass,
  282. class_repr=safe_repr(klass),
  283. ) from exc
  284. try:
  285. name = klass.__name__
  286. except AttributeError as exc:
  287. raise AstroidBuildingError(
  288. "Unable to get name for {class_repr}:\n",
  289. cls=klass,
  290. class_repr=safe_repr(klass),
  291. ) from exc
  292. except Exception as exc:
  293. raise AstroidImportError(
  294. "Unexpected error while retrieving name for {class_repr}:\n" "{error}",
  295. cls=klass,
  296. class_repr=safe_repr(klass),
  297. ) from exc
  298. # take care, on living object __module__ is regularly wrong :(
  299. modastroid = self.ast_from_module_name(modname)
  300. if klass is obj:
  301. for inferred in modastroid.igetattr(name, context):
  302. yield inferred
  303. else:
  304. for inferred in modastroid.igetattr(name, context):
  305. yield inferred.instantiate_class()
  306. def register_failed_import_hook(self, hook):
  307. """Registers a hook to resolve imports that cannot be found otherwise.
  308. `hook` must be a function that accepts a single argument `modname` which
  309. contains the name of the module or package that could not be imported.
  310. If `hook` can resolve the import, must return a node of type `astroid.Module`,
  311. otherwise, it must raise `AstroidBuildingError`.
  312. """
  313. self._failed_import_hooks.append(hook)
  314. def cache_module(self, module):
  315. """Cache a module if no module with the same name is known yet."""
  316. self.astroid_cache.setdefault(module.name, module)
  317. def bootstrap(self):
  318. """Bootstrap the required AST modules needed for the manager to work
  319. The bootstrap usually involves building the AST for the builtins
  320. module, which is required by the rest of astroid to work correctly.
  321. """
  322. from astroid import raw_building # pylint: disable=import-outside-toplevel
  323. raw_building._astroid_bootstrapping()
  324. def clear_cache(self):
  325. """Clear the underlying cache. Also bootstraps the builtins module."""
  326. self.astroid_cache.clear()
  327. self.bootstrap()