pycore.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410
  1. import bisect
  2. import difflib
  3. import sys
  4. import warnings
  5. import rope.base.oi.doa
  6. import rope.base.oi.objectinfo
  7. import rope.base.oi.soa
  8. from rope.base import ast, exceptions, taskhandle, utils, stdmods
  9. from rope.base.exceptions import ModuleNotFoundError
  10. from rope.base.pyobjectsdef import PyModule, PyPackage, PyClass
  11. import rope.base.resources
  12. import rope.base.resourceobserver
  13. from rope.base import builtins
  14. class PyCore(object):
  15. def __init__(self, project):
  16. self.project = project
  17. self._init_resource_observer()
  18. self.cache_observers = []
  19. self.module_cache = _ModuleCache(self)
  20. self.extension_cache = _ExtensionCache(self)
  21. self.object_info = rope.base.oi.objectinfo.ObjectInfoManager(project)
  22. self._init_python_files()
  23. self._init_automatic_soa()
  24. self._init_source_folders()
  25. def _init_python_files(self):
  26. self.python_matcher = None
  27. patterns = self.project.prefs.get('python_files', None)
  28. if patterns is not None:
  29. self.python_matcher = rope.base.resources._ResourceMatcher()
  30. self.python_matcher.set_patterns(patterns)
  31. def _init_resource_observer(self):
  32. callback = self._invalidate_resource_cache
  33. observer = rope.base.resourceobserver.ResourceObserver(
  34. changed=callback, moved=callback, removed=callback)
  35. self.observer = rope.base.resourceobserver.FilteredResourceObserver(observer)
  36. self.project.add_observer(self.observer)
  37. def _init_source_folders(self):
  38. self._custom_source_folders = []
  39. for path in self.project.prefs.get('source_folders', []):
  40. folder = self.project.get_resource(path)
  41. self._custom_source_folders.append(folder)
  42. def _init_automatic_soa(self):
  43. if not self.automatic_soa:
  44. return
  45. callback = self._file_changed_for_soa
  46. observer = rope.base.resourceobserver.ResourceObserver(
  47. changed=callback, moved=callback, removed=callback)
  48. self.project.add_observer(observer)
  49. @property
  50. def automatic_soa(self):
  51. auto_soa = self.project.prefs.get('automatic_soi', None)
  52. return self.project.prefs.get('automatic_soa', auto_soa)
  53. def _file_changed_for_soa(self, resource, new_resource=None):
  54. old_contents = self.project.history.\
  55. contents_before_current_change(resource)
  56. if old_contents is not None:
  57. perform_soa_on_changed_scopes(self.project, resource, old_contents)
  58. def is_python_file(self, resource):
  59. if resource.is_folder():
  60. return False
  61. if self.python_matcher is None:
  62. return resource.name.endswith('.py')
  63. return self.python_matcher.does_match(resource)
  64. def get_module(self, name, folder=None):
  65. """Returns a `PyObject` if the module was found."""
  66. # check if this is a builtin module
  67. pymod = self._builtin_module(name)
  68. if pymod is not None:
  69. return pymod
  70. module = self.find_module(name, folder)
  71. if module is None:
  72. raise ModuleNotFoundError('Module %s not found' % name)
  73. return self.resource_to_pyobject(module)
  74. def _builtin_submodules(self, modname):
  75. result = {}
  76. for extension in self.extension_modules:
  77. if extension.startswith(modname + '.'):
  78. name = extension[len(modname) + 1:]
  79. if '.' not in name:
  80. result[name] = self._builtin_module(extension)
  81. return result
  82. def _builtin_module(self, name):
  83. return self.extension_cache.get_pymodule(name)
  84. def get_relative_module(self, name, folder, level):
  85. module = self.find_relative_module(name, folder, level)
  86. if module is None:
  87. raise ModuleNotFoundError('Module %s not found' % name)
  88. return self.resource_to_pyobject(module)
  89. def get_string_module(self, code, resource=None, force_errors=False):
  90. """Returns a `PyObject` object for the given code
  91. If `force_errors` is `True`, `exceptions.ModuleSyntaxError` is
  92. raised if module has syntax errors. This overrides
  93. ``ignore_syntax_errors`` project config.
  94. """
  95. return PyModule(self, code, resource, force_errors=force_errors)
  96. def get_string_scope(self, code, resource=None):
  97. """Returns a `Scope` object for the given code"""
  98. return self.get_string_module(code, resource).get_scope()
  99. def _invalidate_resource_cache(self, resource, new_resource=None):
  100. for observer in self.cache_observers:
  101. observer(resource)
  102. def _find_module_in_folder(self, folder, modname):
  103. module = folder
  104. packages = modname.split('.')
  105. for pkg in packages[:-1]:
  106. if module.is_folder() and module.has_child(pkg):
  107. module = module.get_child(pkg)
  108. else:
  109. return None
  110. if module.is_folder():
  111. if module.has_child(packages[-1]) and \
  112. module.get_child(packages[-1]).is_folder():
  113. return module.get_child(packages[-1])
  114. elif module.has_child(packages[-1] + '.py') and \
  115. not module.get_child(packages[-1] + '.py').is_folder():
  116. return module.get_child(packages[-1] + '.py')
  117. def get_python_path_folders(self):
  118. import rope.base.project
  119. result = []
  120. for src in self.project.prefs.get('python_path', []) + sys.path:
  121. try:
  122. src_folder = rope.base.project.get_no_project().get_resource(src)
  123. result.append(src_folder)
  124. except rope.base.exceptions.ResourceNotFoundError:
  125. pass
  126. return result
  127. def find_module(self, modname, folder=None):
  128. """Returns a resource corresponding to the given module
  129. returns None if it can not be found
  130. """
  131. return self._find_module(modname, folder)
  132. def find_relative_module(self, modname, folder, level):
  133. for i in range(level - 1):
  134. folder = folder.parent
  135. if modname == '':
  136. return folder
  137. else:
  138. return self._find_module_in_folder(folder, modname)
  139. def _find_module(self, modname, folder=None):
  140. """Return `modname` module resource"""
  141. for src in self.get_source_folders():
  142. module = self._find_module_in_folder(src, modname)
  143. if module is not None:
  144. return module
  145. for src in self.get_python_path_folders():
  146. module = self._find_module_in_folder(src, modname)
  147. if module is not None:
  148. return module
  149. if folder is not None:
  150. module = self._find_module_in_folder(folder, modname)
  151. if module is not None:
  152. return module
  153. return None
  154. # INFO: It was decided not to cache source folders, since:
  155. # - Does not take much time when the root folder contains
  156. # packages, that is most of the time
  157. # - We need a separate resource observer; `self.observer`
  158. # does not get notified about module and folder creations
  159. def get_source_folders(self):
  160. """Returns project source folders"""
  161. if self.project.root is None:
  162. return []
  163. result = list(self._custom_source_folders)
  164. result.extend(self._find_source_folders(self.project.root))
  165. return result
  166. def resource_to_pyobject(self, resource, force_errors=False):
  167. return self.module_cache.get_pymodule(resource, force_errors)
  168. def get_python_files(self):
  169. """Returns all python files available in the project"""
  170. return [resource for resource in self.project.get_files()
  171. if self.is_python_file(resource)]
  172. def _is_package(self, folder):
  173. if folder.has_child('__init__.py') and \
  174. not folder.get_child('__init__.py').is_folder():
  175. return True
  176. else:
  177. return False
  178. def _find_source_folders(self, folder):
  179. for resource in folder.get_folders():
  180. if self._is_package(resource):
  181. return [folder]
  182. result = []
  183. for resource in folder.get_files():
  184. if resource.name.endswith('.py'):
  185. result.append(folder)
  186. break
  187. for resource in folder.get_folders():
  188. result.extend(self._find_source_folders(resource))
  189. return result
  190. def run_module(self, resource, args=None, stdin=None, stdout=None):
  191. """Run `resource` module
  192. Returns a `rope.base.oi.doa.PythonFileRunner` object for
  193. controlling the process.
  194. """
  195. perform_doa = self.project.prefs.get('perform_doi', True)
  196. perform_doa = self.project.prefs.get('perform_doa', perform_doa)
  197. receiver = self.object_info.doa_data_received
  198. if not perform_doa:
  199. receiver = None
  200. runner = rope.base.oi.doa.PythonFileRunner(
  201. self, resource, args, stdin, stdout, receiver)
  202. runner.add_finishing_observer(self.module_cache.forget_all_data)
  203. runner.run()
  204. return runner
  205. def analyze_module(self, resource, should_analyze=lambda py: True,
  206. search_subscopes=lambda py: True, followed_calls=None):
  207. """Analyze `resource` module for static object inference
  208. This function forces rope to analyze this module to collect
  209. information about function calls. `should_analyze` is a
  210. function that is called with a `PyDefinedObject` argument. If
  211. it returns `True` the element is analyzed. If it is `None` or
  212. returns `False` the element is not analyzed.
  213. `search_subscopes` is like `should_analyze`; The difference is
  214. that if it returns `False` the sub-scopes are all ignored.
  215. That is it is assumed that `should_analyze` returns `False`
  216. for all of its subscopes.
  217. `followed_calls` override the value of ``soa_followed_calls``
  218. project config.
  219. """
  220. if followed_calls is None:
  221. followed_calls = self.project.prefs.get('soa_followed_calls', 0)
  222. pymodule = self.resource_to_pyobject(resource)
  223. self.module_cache.forget_all_data()
  224. rope.base.oi.soa.analyze_module(
  225. self, pymodule, should_analyze, search_subscopes, followed_calls)
  226. def get_classes(self, task_handle=taskhandle.NullTaskHandle()):
  227. warnings.warn('`PyCore.get_classes()` is deprecated',
  228. DeprecationWarning, stacklevel=2)
  229. return []
  230. def __str__(self):
  231. return str(self.module_cache) + str(self.object_info)
  232. def modname(self, resource):
  233. if resource.is_folder():
  234. module_name = resource.name
  235. source_folder = resource.parent
  236. elif resource.name == '__init__.py':
  237. module_name = resource.parent.name
  238. source_folder = resource.parent.parent
  239. else:
  240. module_name = resource.name[:-3]
  241. source_folder = resource.parent
  242. while source_folder != source_folder.parent and \
  243. source_folder.has_child('__init__.py'):
  244. module_name = source_folder.name + '.' + module_name
  245. source_folder = source_folder.parent
  246. return module_name
  247. @property
  248. @utils.cacheit
  249. def extension_modules(self):
  250. result = set(self.project.prefs.get('extension_modules', []))
  251. if self.project.prefs.get('import_dynload_stdmods', False):
  252. result.update(stdmods.dynload_modules())
  253. return result
  254. class _ModuleCache(object):
  255. def __init__(self, pycore):
  256. self.pycore = pycore
  257. self.module_map = {}
  258. self.pycore.cache_observers.append(self._invalidate_resource)
  259. self.observer = self.pycore.observer
  260. def _invalidate_resource(self, resource):
  261. if resource in self.module_map:
  262. self.forget_all_data()
  263. self.observer.remove_resource(resource)
  264. del self.module_map[resource]
  265. def get_pymodule(self, resource, force_errors=False):
  266. if resource in self.module_map:
  267. return self.module_map[resource]
  268. if resource.is_folder():
  269. result = PyPackage(self.pycore, resource,
  270. force_errors=force_errors)
  271. else:
  272. result = PyModule(self.pycore, resource=resource,
  273. force_errors=force_errors)
  274. if result.has_errors:
  275. return result
  276. self.module_map[resource] = result
  277. self.observer.add_resource(resource)
  278. return result
  279. def forget_all_data(self):
  280. for pymodule in self.module_map.values():
  281. pymodule._forget_concluded_data()
  282. def __str__(self):
  283. return 'PyCore caches %d PyModules\n' % len(self.module_map)
  284. class _ExtensionCache(object):
  285. def __init__(self, pycore):
  286. self.pycore = pycore
  287. self.extensions = {}
  288. def get_pymodule(self, name):
  289. if name == '__builtin__':
  290. return builtins.builtins
  291. allowed = self.pycore.extension_modules
  292. if name not in self.extensions and name in allowed:
  293. self.extensions[name] = builtins.BuiltinModule(name, self.pycore)
  294. return self.extensions.get(name)
  295. def perform_soa_on_changed_scopes(project, resource, old_contents):
  296. pycore = project.pycore
  297. if resource.exists() and pycore.is_python_file(resource):
  298. try:
  299. new_contents = resource.read()
  300. # detecting changes in new_contents relative to old_contents
  301. detector = _TextChangeDetector(new_contents, old_contents)
  302. def search_subscopes(pydefined):
  303. scope = pydefined.get_scope()
  304. return detector.is_changed(scope.get_start(), scope.get_end())
  305. def should_analyze(pydefined):
  306. scope = pydefined.get_scope()
  307. start = scope.get_start()
  308. end = scope.get_end()
  309. return detector.consume_changes(start, end)
  310. pycore.analyze_module(resource, should_analyze, search_subscopes)
  311. except exceptions.ModuleSyntaxError:
  312. pass
  313. class _TextChangeDetector(object):
  314. def __init__(self, old, new):
  315. self.old = old
  316. self.new = new
  317. self._set_diffs()
  318. def _set_diffs(self):
  319. differ = difflib.Differ()
  320. self.lines = []
  321. lineno = 0
  322. for line in differ.compare(self.old.splitlines(True),
  323. self.new.splitlines(True)):
  324. if line.startswith(' '):
  325. lineno += 1
  326. elif line.startswith('-'):
  327. lineno += 1
  328. self.lines.append(lineno)
  329. def is_changed(self, start, end):
  330. """Tell whether any of start till end lines have changed
  331. The end points are inclusive and indices start from 1.
  332. """
  333. left, right = self._get_changed(start, end)
  334. if left < right:
  335. return True
  336. return False
  337. def consume_changes(self, start, end):
  338. """Clear the changed status of lines from start till end"""
  339. left, right = self._get_changed(start, end)
  340. if left < right:
  341. del self.lines[left:right]
  342. return left < right
  343. def _get_changed(self, start, end):
  344. left = bisect.bisect_left(self.lines, start)
  345. right = bisect.bisect_right(self.lines, end)
  346. return left, right