project.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  1. """
  2. Projects are a way to handle Python projects within Jedi. For simpler plugins
  3. you might not want to deal with projects, but if you want to give the user more
  4. flexibility to define sys paths and Python interpreters for a project,
  5. :class:`.Project` is the perfect way to allow for that.
  6. Projects can be saved to disk and loaded again, to allow project definitions to
  7. be used across repositories.
  8. """
  9. import json
  10. from pathlib import Path
  11. from itertools import chain
  12. from jedi import debug
  13. from jedi.api.environment import get_cached_default_environment, create_environment
  14. from jedi.api.exceptions import WrongVersion
  15. from jedi.api.completion import search_in_module
  16. from jedi.api.helpers import split_search_string, get_module_names
  17. from jedi.inference.imports import load_module_from_path, \
  18. load_namespace_from_path, iter_module_names
  19. from jedi.inference.sys_path import discover_buildout_paths
  20. from jedi.inference.cache import inference_state_as_method_param_cache
  21. from jedi.inference.references import recurse_find_python_folders_and_files, search_in_file_ios
  22. from jedi.file_io import FolderIO
  23. _CONFIG_FOLDER = '.jedi'
  24. _CONTAINS_POTENTIAL_PROJECT = \
  25. 'setup.py', '.git', '.hg', 'requirements.txt', 'MANIFEST.in', 'pyproject.toml'
  26. _SERIALIZER_VERSION = 1
  27. def _try_to_skip_duplicates(func):
  28. def wrapper(*args, **kwargs):
  29. found_tree_nodes = []
  30. found_modules = []
  31. for definition in func(*args, **kwargs):
  32. tree_node = definition._name.tree_name
  33. if tree_node is not None and tree_node in found_tree_nodes:
  34. continue
  35. if definition.type == 'module' and definition.module_path is not None:
  36. if definition.module_path in found_modules:
  37. continue
  38. found_modules.append(definition.module_path)
  39. yield definition
  40. found_tree_nodes.append(tree_node)
  41. return wrapper
  42. def _remove_duplicates_from_path(path):
  43. used = set()
  44. for p in path:
  45. if p in used:
  46. continue
  47. used.add(p)
  48. yield p
  49. class Project:
  50. """
  51. Projects are a simple way to manage Python folders and define how Jedi does
  52. import resolution. It is mostly used as a parameter to :class:`.Script`.
  53. Additionally there are functions to search a whole project.
  54. """
  55. _environment = None
  56. @staticmethod
  57. def _get_config_folder_path(base_path):
  58. return base_path.joinpath(_CONFIG_FOLDER)
  59. @staticmethod
  60. def _get_json_path(base_path):
  61. return Project._get_config_folder_path(base_path).joinpath('project.json')
  62. @classmethod
  63. def load(cls, path):
  64. """
  65. Loads a project from a specific path. You should not provide the path
  66. to ``.jedi/project.json``, but rather the path to the project folder.
  67. :param path: The path of the directory you want to use as a project.
  68. """
  69. if isinstance(path, str):
  70. path = Path(path)
  71. with open(cls._get_json_path(path)) as f:
  72. version, data = json.load(f)
  73. if version == 1:
  74. return cls(**data)
  75. else:
  76. raise WrongVersion(
  77. "The Jedi version of this project seems newer than what we can handle."
  78. )
  79. def save(self):
  80. """
  81. Saves the project configuration in the project in ``.jedi/project.json``.
  82. """
  83. data = dict(self.__dict__)
  84. data.pop('_environment', None)
  85. data.pop('_django', None) # TODO make django setting public?
  86. data = {k.lstrip('_'): v for k, v in data.items()}
  87. data['path'] = str(data['path'])
  88. self._get_config_folder_path(self._path).mkdir(parents=True, exist_ok=True)
  89. with open(self._get_json_path(self._path), 'w') as f:
  90. return json.dump((_SERIALIZER_VERSION, data), f)
  91. def __init__(
  92. self,
  93. path,
  94. *,
  95. environment_path=None,
  96. load_unsafe_extensions=False,
  97. sys_path=None,
  98. added_sys_path=(),
  99. smart_sys_path=True,
  100. ) -> None:
  101. """
  102. :param path: The base path for this project.
  103. :param environment_path: The Python executable path, typically the path
  104. of a virtual environment.
  105. :param load_unsafe_extensions: Default False, Loads extensions that are not in the
  106. sys path and in the local directories. With this option enabled,
  107. this is potentially unsafe if you clone a git repository and
  108. analyze it's code, because those compiled extensions will be
  109. important and therefore have execution privileges.
  110. :param sys_path: list of str. You can override the sys path if you
  111. want. By default the ``sys.path.`` is generated by the
  112. environment (virtualenvs, etc).
  113. :param added_sys_path: list of str. Adds these paths at the end of the
  114. sys path.
  115. :param smart_sys_path: If this is enabled (default), adds paths from
  116. local directories. Otherwise you will have to rely on your packages
  117. being properly configured on the ``sys.path``.
  118. """
  119. if isinstance(path, str):
  120. path = Path(path).absolute()
  121. self._path = path
  122. self._environment_path = environment_path
  123. if sys_path is not None:
  124. # Remap potential pathlib.Path entries
  125. sys_path = list(map(str, sys_path))
  126. self._sys_path = sys_path
  127. self._smart_sys_path = smart_sys_path
  128. self._load_unsafe_extensions = load_unsafe_extensions
  129. self._django = False
  130. # Remap potential pathlib.Path entries
  131. self.added_sys_path = list(map(str, added_sys_path))
  132. """The sys path that is going to be added at the end of the """
  133. @property
  134. def path(self):
  135. """
  136. The base path for this project.
  137. """
  138. return self._path
  139. @property
  140. def sys_path(self):
  141. """
  142. The sys path provided to this project. This can be None and in that
  143. case will be auto generated.
  144. """
  145. return self._sys_path
  146. @property
  147. def smart_sys_path(self):
  148. """
  149. If the sys path is going to be calculated in a smart way, where
  150. additional paths are added.
  151. """
  152. return self._smart_sys_path
  153. @property
  154. def load_unsafe_extensions(self):
  155. """
  156. Wheter the project loads unsafe extensions.
  157. """
  158. return self._load_unsafe_extensions
  159. @inference_state_as_method_param_cache()
  160. def _get_base_sys_path(self, inference_state):
  161. # The sys path has not been set explicitly.
  162. sys_path = list(inference_state.environment.get_sys_path())
  163. try:
  164. sys_path.remove('')
  165. except ValueError:
  166. pass
  167. return sys_path
  168. @inference_state_as_method_param_cache()
  169. def _get_sys_path(self, inference_state, add_parent_paths=True, add_init_paths=False):
  170. """
  171. Keep this method private for all users of jedi. However internally this
  172. one is used like a public method.
  173. """
  174. suffixed = list(self.added_sys_path)
  175. prefixed = []
  176. if self._sys_path is None:
  177. sys_path = list(self._get_base_sys_path(inference_state))
  178. else:
  179. sys_path = list(self._sys_path)
  180. if self._smart_sys_path:
  181. prefixed.append(str(self._path))
  182. if inference_state.script_path is not None:
  183. suffixed += map(str, discover_buildout_paths(
  184. inference_state,
  185. inference_state.script_path
  186. ))
  187. if add_parent_paths:
  188. # Collect directories in upward search by:
  189. # 1. Skipping directories with __init__.py
  190. # 2. Stopping immediately when above self._path
  191. traversed = []
  192. for parent_path in inference_state.script_path.parents:
  193. if parent_path == self._path \
  194. or self._path not in parent_path.parents:
  195. break
  196. if not add_init_paths \
  197. and parent_path.joinpath("__init__.py").is_file():
  198. continue
  199. traversed.append(str(parent_path))
  200. # AFAIK some libraries have imports like `foo.foo.bar`, which
  201. # leads to the conclusion to by default prefer longer paths
  202. # rather than shorter ones by default.
  203. suffixed += reversed(traversed)
  204. if self._django:
  205. prefixed.append(str(self._path))
  206. path = prefixed + sys_path + suffixed
  207. return list(_remove_duplicates_from_path(path))
  208. def get_environment(self):
  209. if self._environment is None:
  210. if self._environment_path is not None:
  211. self._environment = create_environment(self._environment_path, safe=False)
  212. else:
  213. self._environment = get_cached_default_environment()
  214. return self._environment
  215. def search(self, string, *, all_scopes=False):
  216. """
  217. Searches a name in the whole project. If the project is very big,
  218. at some point Jedi will stop searching. However it's also very much
  219. recommended to not exhaust the generator. Just display the first ten
  220. results to the user.
  221. There are currently three different search patterns:
  222. - ``foo`` to search for a definition foo in any file or a file called
  223. ``foo.py`` or ``foo.pyi``.
  224. - ``foo.bar`` to search for the ``foo`` and then an attribute ``bar``
  225. in it.
  226. - ``class foo.bar.Bar`` or ``def foo.bar.baz`` to search for a specific
  227. API type.
  228. :param bool all_scopes: Default False; searches not only for
  229. definitions on the top level of a module level, but also in
  230. functions and classes.
  231. :yields: :class:`.Name`
  232. """
  233. return self._search_func(string, all_scopes=all_scopes)
  234. def complete_search(self, string, **kwargs):
  235. """
  236. Like :meth:`.Script.search`, but completes that string. An empty string
  237. lists all definitions in a project, so be careful with that.
  238. :param bool all_scopes: Default False; searches not only for
  239. definitions on the top level of a module level, but also in
  240. functions and classes.
  241. :yields: :class:`.Completion`
  242. """
  243. return self._search_func(string, complete=True, **kwargs)
  244. @_try_to_skip_duplicates
  245. def _search_func(self, string, complete=False, all_scopes=False):
  246. # Using a Script is they easiest way to get an empty module context.
  247. from jedi import Script
  248. s = Script('', project=self)
  249. inference_state = s._inference_state
  250. empty_module_context = s._get_module_context()
  251. debug.dbg('Search for string %s, complete=%s', string, complete)
  252. wanted_type, wanted_names = split_search_string(string)
  253. name = wanted_names[0]
  254. stub_folder_name = name + '-stubs'
  255. ios = recurse_find_python_folders_and_files(FolderIO(str(self._path)))
  256. file_ios = []
  257. # 1. Search for modules in the current project
  258. for folder_io, file_io in ios:
  259. if file_io is None:
  260. file_name = folder_io.get_base_name()
  261. if file_name == name or file_name == stub_folder_name:
  262. f = folder_io.get_file_io('__init__.py')
  263. try:
  264. m = load_module_from_path(inference_state, f).as_context()
  265. except FileNotFoundError:
  266. f = folder_io.get_file_io('__init__.pyi')
  267. try:
  268. m = load_module_from_path(inference_state, f).as_context()
  269. except FileNotFoundError:
  270. m = load_namespace_from_path(inference_state, folder_io).as_context()
  271. else:
  272. continue
  273. else:
  274. file_ios.append(file_io)
  275. if Path(file_io.path).name in (name + '.py', name + '.pyi'):
  276. m = load_module_from_path(inference_state, file_io).as_context()
  277. else:
  278. continue
  279. debug.dbg('Search of a specific module %s', m)
  280. yield from search_in_module(
  281. inference_state,
  282. m,
  283. names=[m.name],
  284. wanted_type=wanted_type,
  285. wanted_names=wanted_names,
  286. complete=complete,
  287. convert=True,
  288. ignore_imports=True,
  289. )
  290. # 2. Search for identifiers in the project.
  291. for module_context in search_in_file_ios(inference_state, file_ios,
  292. name, complete=complete):
  293. names = get_module_names(module_context.tree_node, all_scopes=all_scopes)
  294. names = [module_context.create_name(n) for n in names]
  295. names = _remove_imports(names)
  296. yield from search_in_module(
  297. inference_state,
  298. module_context,
  299. names=names,
  300. wanted_type=wanted_type,
  301. wanted_names=wanted_names,
  302. complete=complete,
  303. ignore_imports=True,
  304. )
  305. # 3. Search for modules on sys.path
  306. sys_path = [
  307. p for p in self._get_sys_path(inference_state)
  308. # Exclude the current folder which is handled by recursing the folders.
  309. if p != self._path
  310. ]
  311. names = list(iter_module_names(inference_state, empty_module_context, sys_path))
  312. yield from search_in_module(
  313. inference_state,
  314. empty_module_context,
  315. names=names,
  316. wanted_type=wanted_type,
  317. wanted_names=wanted_names,
  318. complete=complete,
  319. convert=True,
  320. )
  321. def __repr__(self):
  322. return '<%s: %s>' % (self.__class__.__name__, self._path)
  323. def _is_potential_project(path):
  324. for name in _CONTAINS_POTENTIAL_PROJECT:
  325. try:
  326. if path.joinpath(name).exists():
  327. return True
  328. except OSError:
  329. continue
  330. return False
  331. def _is_django_path(directory):
  332. """ Detects the path of the very well known Django library (if used) """
  333. try:
  334. with open(directory.joinpath('manage.py'), 'rb') as f:
  335. return b"DJANGO_SETTINGS_MODULE" in f.read()
  336. except (FileNotFoundError, IsADirectoryError, PermissionError):
  337. return False
  338. def get_default_project(path=None):
  339. """
  340. If a project is not defined by the user, Jedi tries to define a project by
  341. itself as well as possible. Jedi traverses folders until it finds one of
  342. the following:
  343. 1. A ``.jedi/config.json``
  344. 2. One of the following files: ``setup.py``, ``.git``, ``.hg``,
  345. ``requirements.txt`` and ``MANIFEST.in``.
  346. """
  347. if path is None:
  348. path = Path.cwd()
  349. elif isinstance(path, str):
  350. path = Path(path)
  351. check = path.absolute()
  352. probable_path = None
  353. first_no_init_file = None
  354. for dir in chain([check], check.parents):
  355. try:
  356. return Project.load(dir)
  357. except (FileNotFoundError, IsADirectoryError, PermissionError):
  358. pass
  359. except NotADirectoryError:
  360. continue
  361. if first_no_init_file is None:
  362. if dir.joinpath('__init__.py').exists():
  363. # In the case that a __init__.py exists, it's in 99% just a
  364. # Python package and the project sits at least one level above.
  365. continue
  366. elif not dir.is_file():
  367. first_no_init_file = dir
  368. if _is_django_path(dir):
  369. project = Project(dir)
  370. project._django = True
  371. return project
  372. if probable_path is None and _is_potential_project(dir):
  373. probable_path = dir
  374. if probable_path is not None:
  375. return Project(probable_path)
  376. if first_no_init_file is not None:
  377. return Project(first_no_init_file)
  378. curdir = path if path.is_dir() else path.parent
  379. return Project(curdir)
  380. def _remove_imports(names):
  381. return [
  382. n for n in names
  383. if n.tree_name is None or n.api_type not in ('module', 'namespace')
  384. ]