Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jedi-0.20.1-py3.11.egg/jedi/api/project.py: 43%

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

229 statements  

1""" 

2Projects are a way to handle Python projects within Jedi. For simpler plugins 

3you might not want to deal with projects, but if you want to give the user more 

4flexibility to define sys paths and Python interpreters for a project, 

5:class:`.Project` is the perfect way to allow for that. 

6 

7Projects can be saved to disk and loaded again, to allow project definitions to 

8be used across repositories. 

9""" 

10import json 

11from pathlib import Path 

12from itertools import chain 

13 

14from jedi import debug 

15from jedi.api.environment import get_cached_default_environment, create_environment 

16from jedi.api.exceptions import WrongVersion 

17from jedi.api.completion import search_in_module 

18from jedi.api.helpers import split_search_string, get_module_names, recursion_limit_generator 

19from jedi.inference.imports import load_module_from_path, \ 

20 load_namespace_from_path, iter_module_names 

21from jedi.inference.sys_path import discover_buildout_paths 

22from jedi.inference.cache import inference_state_as_method_param_cache 

23from jedi.inference.references import recurse_find_python_folders_and_files, search_in_file_ios 

24from jedi.file_io import FolderIO 

25 

26_CONFIG_FOLDER = '.jedi' 

27_CONTAINS_POTENTIAL_PROJECT = \ 

28 'setup.py', '.git', '.hg', 'requirements.txt', 'MANIFEST.in', 'pyproject.toml' 

29 

30_SERIALIZER_VERSION = 1 

31 

32 

33def _try_to_skip_duplicates(func): 

34 def wrapper(*args, **kwargs): 

35 found_tree_nodes = [] 

36 found_modules = [] 

37 for definition in func(*args, **kwargs): 

38 tree_node = definition._name.tree_name 

39 if tree_node is not None and tree_node in found_tree_nodes: 

40 continue 

41 if definition.type == 'module' and definition.module_path is not None: 

42 if definition.module_path in found_modules: 

43 continue 

44 found_modules.append(definition.module_path) 

45 yield definition 

46 found_tree_nodes.append(tree_node) 

47 return wrapper 

48 

49 

50def _remove_duplicates_from_path(path): 

51 used = set() 

52 for p in path: 

53 if p in used: 

54 continue 

55 used.add(p) 

56 yield p 

57 

58 

59class Project: 

60 """ 

61 Projects are a simple way to manage Python folders and define how Jedi does 

62 import resolution. It is mostly used as a parameter to :class:`.Script`. 

63 Additionally there are functions to search a whole project. 

64 """ 

65 _environment = None 

66 

67 @staticmethod 

68 def _get_config_folder_path(base_path): 

69 return base_path.joinpath(_CONFIG_FOLDER) 

70 

71 @staticmethod 

72 def _get_json_path(base_path): 

73 return Project._get_config_folder_path(base_path).joinpath('project.json') 

74 

75 @classmethod 

76 def load(cls, path): 

77 """ 

78 Loads a project from a specific path. You should not provide the path 

79 to ``.jedi/project.json``, but rather the path to the project folder. 

80 

81 :param path: The path of the directory you want to use as a project. 

82 """ 

83 if isinstance(path, str): 

84 path = Path(path) 

85 with open(cls._get_json_path(path)) as f: 

86 version, data = json.load(f) 

87 

88 if version == 1: 

89 # A code base must not be able to enable the loading of its own 

90 # extensions or point Jedi at a binary to execute, see the docs 

91 # about security. The project file can be part of a checked out 

92 # repository, so these settings are ignored for loaded projects. 

93 if data.pop('load_unsafe_extensions', False): 

94 debug.warning('load_unsafe_extensions is ignored for loaded projects') 

95 if data.pop('environment_path', None) is not None: 

96 debug.warning('environment_path is ignored for loaded projects') 

97 return cls(**data) 

98 else: 

99 raise WrongVersion( 

100 "The Jedi version of this project seems newer than what we can handle." 

101 ) 

102 

103 def save(self): 

104 """ 

105 Saves the project configuration in the project in ``.jedi/project.json``. 

106 """ 

107 data = dict(self.__dict__) 

108 data.pop('_environment', None) 

109 data.pop('_django', None) # TODO make django setting public? 

110 

111 # These are dropped anyway 

112 data.pop('_load_unsafe_extensions', None) 

113 data.pop('_environment_path', None) 

114 

115 data = {k.lstrip('_'): v for k, v in data.items()} 

116 data['path'] = str(data['path']) 

117 

118 self._get_config_folder_path(self._path).mkdir(parents=True, exist_ok=True) 

119 with open(self._get_json_path(self._path), 'w') as f: 

120 return json.dump((_SERIALIZER_VERSION, data), f) 

121 

122 def __init__( 

123 self, 

124 path, 

125 *, 

126 environment_path=None, 

127 load_unsafe_extensions=False, 

128 sys_path=None, 

129 added_sys_path=(), 

130 smart_sys_path=True, 

131 ) -> None: 

132 """ 

133 :param path: The base path for this project. 

134 :param environment_path: The Python executable path, typically the path 

135 of a virtual environment. 

136 :param load_unsafe_extensions: Default False, Loads extensions that are not in the 

137 sys path and in the local directories. With this option enabled, 

138 this is potentially unsafe if you clone a git repository and 

139 analyze it's code, because those compiled extensions will be 

140 important and therefore have execution privileges. 

141 :param sys_path: list of str. You can override the sys path if you 

142 want. By default the ``sys.path.`` is generated by the 

143 environment (virtualenvs, etc). 

144 :param added_sys_path: list of str. Adds these paths at the end of the 

145 sys path. 

146 :param smart_sys_path: If this is enabled (default), adds paths from 

147 local directories. Otherwise you will have to rely on your packages 

148 being properly configured on the ``sys.path``. 

149 """ 

150 

151 if isinstance(path, str): 

152 path = Path(path).absolute() 

153 self._path = path 

154 

155 self._environment_path = environment_path 

156 if sys_path is not None: 

157 # Remap potential pathlib.Path entries 

158 sys_path = list(map(str, sys_path)) 

159 self._sys_path = sys_path 

160 self._smart_sys_path = smart_sys_path 

161 self._load_unsafe_extensions = load_unsafe_extensions 

162 self._django = False 

163 # Remap potential pathlib.Path entries 

164 self.added_sys_path = list(map(str, added_sys_path)) 

165 """The sys path that is going to be added at the end of the """ 

166 

167 @property 

168 def path(self): 

169 """ 

170 The base path for this project. 

171 """ 

172 return self._path 

173 

174 @property 

175 def sys_path(self): 

176 """ 

177 The sys path provided to this project. This can be None and in that 

178 case will be auto generated. 

179 """ 

180 return self._sys_path 

181 

182 @property 

183 def smart_sys_path(self): 

184 """ 

185 If the sys path is going to be calculated in a smart way, where 

186 additional paths are added. 

187 """ 

188 return self._smart_sys_path 

189 

190 @property 

191 def load_unsafe_extensions(self): 

192 """ 

193 Wheter the project loads unsafe extensions. 

194 """ 

195 return self._load_unsafe_extensions 

196 

197 @inference_state_as_method_param_cache() 

198 def _get_base_sys_path(self, inference_state): 

199 # The sys path has not been set explicitly. 

200 sys_path = list(inference_state.environment.get_sys_path()) 

201 try: 

202 sys_path.remove('') 

203 except ValueError: 

204 pass 

205 return sys_path 

206 

207 @inference_state_as_method_param_cache() 

208 def _get_sys_path(self, inference_state, add_parent_paths=True, add_init_paths=False): 

209 """ 

210 Keep this method private for all users of jedi. However internally this 

211 one is used like a public method. 

212 """ 

213 suffixed = list(self.added_sys_path) 

214 prefixed = [] 

215 

216 if self._sys_path is None: 

217 sys_path = list(self._get_base_sys_path(inference_state)) 

218 else: 

219 sys_path = list(self._sys_path) 

220 

221 if self._smart_sys_path: 

222 prefixed.append(str(self._path)) 

223 

224 if inference_state.script_path is not None: 

225 suffixed += map(str, discover_buildout_paths( 

226 inference_state, 

227 inference_state.script_path 

228 )) 

229 

230 if add_parent_paths: 

231 # Collect directories in upward search by: 

232 # 1. Skipping directories with __init__.py 

233 # 2. Stopping immediately when above self._path 

234 traversed = [] 

235 for parent_path in inference_state.script_path.parents: 

236 if parent_path == self._path \ 

237 or self._path not in parent_path.parents: 

238 break 

239 if not add_init_paths \ 

240 and parent_path.joinpath("__init__.py").is_file(): 

241 continue 

242 traversed.append(str(parent_path)) 

243 

244 # AFAIK some libraries have imports like `foo.foo.bar`, which 

245 # leads to the conclusion to by default prefer longer paths 

246 # rather than shorter ones by default. 

247 suffixed += reversed(traversed) 

248 

249 if self._django: 

250 prefixed.append(str(self._path)) 

251 

252 path = prefixed + sys_path + suffixed 

253 return list(_remove_duplicates_from_path(path)) 

254 

255 def get_environment(self): 

256 if self._environment is None: 

257 if self._environment_path is not None: 

258 # environment_path can only be set by the user directly, it is 

259 # never loaded from a project file, so it is trusted. 

260 self._environment = create_environment( 

261 self._environment_path, safe=False) 

262 else: 

263 self._environment = get_cached_default_environment() 

264 return self._environment 

265 

266 def search(self, string, *, all_scopes=False): 

267 """ 

268 Searches a name in the whole project. If the project is very big, 

269 at some point Jedi will stop searching. However it's also very much 

270 recommended to not exhaust the generator. Just display the first ten 

271 results to the user. 

272 

273 There are currently three different search patterns: 

274 

275 - ``foo`` to search for a definition foo in any file or a file called 

276 ``foo.py`` or ``foo.pyi``. 

277 - ``foo.bar`` to search for the ``foo`` and then an attribute ``bar`` 

278 in it. 

279 - ``class foo.bar.Bar`` or ``def foo.bar.baz`` to search for a specific 

280 API type. 

281 

282 :param bool all_scopes: Default False; searches not only for 

283 definitions on the top level of a module level, but also in 

284 functions and classes. 

285 :yields: :class:`.Name` 

286 """ 

287 return self._search_func(string, all_scopes=all_scopes) 

288 

289 def complete_search(self, string, **kwargs): 

290 """ 

291 Like :meth:`.Script.search`, but completes that string. An empty string 

292 lists all definitions in a project, so be careful with that. 

293 

294 :param bool all_scopes: Default False; searches not only for 

295 definitions on the top level of a module level, but also in 

296 functions and classes. 

297 :yields: :class:`.Completion` 

298 """ 

299 return self._search_func(string, complete=True, **kwargs) 

300 

301 @recursion_limit_generator 

302 @_try_to_skip_duplicates 

303 def _search_func(self, string, complete=False, all_scopes=False): 

304 # Using a Script is they easiest way to get an empty module context. 

305 from jedi import Script 

306 s = Script('', project=self) 

307 inference_state = s._inference_state 

308 empty_module_context = s._get_module_context() 

309 

310 debug.dbg('Search for string %s, complete=%s', string, complete) 

311 wanted_type, wanted_names = split_search_string(string) 

312 name = wanted_names[0] 

313 stub_folder_name = name + '-stubs' 

314 

315 ios = recurse_find_python_folders_and_files(FolderIO(str(self._path))) 

316 file_ios = [] 

317 

318 # 1. Search for modules in the current project 

319 for folder_io, file_io in ios: 

320 if file_io is None: 

321 file_name = folder_io.get_base_name() 

322 if file_name == name or file_name == stub_folder_name: 

323 f = folder_io.get_file_io('__init__.py') 

324 try: 

325 m = load_module_from_path(inference_state, f).as_context() 

326 except FileNotFoundError: 

327 f = folder_io.get_file_io('__init__.pyi') 

328 try: 

329 m = load_module_from_path(inference_state, f).as_context() 

330 except FileNotFoundError: 

331 m = load_namespace_from_path(inference_state, folder_io).as_context() 

332 else: 

333 continue 

334 else: 

335 file_ios.append(file_io) 

336 if Path(file_io.path).name in (name + '.py', name + '.pyi'): 

337 m = load_module_from_path(inference_state, file_io).as_context() 

338 else: 

339 continue 

340 

341 debug.dbg('Search of a specific module %s', m) 

342 yield from search_in_module( 

343 inference_state, 

344 m, 

345 names=[m.name], 

346 wanted_type=wanted_type, 

347 wanted_names=wanted_names, 

348 complete=complete, 

349 convert=True, 

350 ignore_imports=True, 

351 ) 

352 

353 # 2. Search for identifiers in the project. 

354 for module_context in search_in_file_ios(inference_state, file_ios, 

355 name, complete=complete): 

356 names = get_module_names(module_context.tree_node, all_scopes=all_scopes) 

357 names = [module_context.create_name(n) for n in names] 

358 names = _remove_imports(names) 

359 yield from search_in_module( 

360 inference_state, 

361 module_context, 

362 names=names, 

363 wanted_type=wanted_type, 

364 wanted_names=wanted_names, 

365 complete=complete, 

366 ignore_imports=True, 

367 ) 

368 

369 # 3. Search for modules on sys.path 

370 sys_path = [ 

371 p for p in self._get_sys_path(inference_state) 

372 # Exclude the current folder which is handled by recursing the folders. 

373 if p != self._path 

374 ] 

375 names = list(iter_module_names(inference_state, empty_module_context, sys_path)) 

376 yield from search_in_module( 

377 inference_state, 

378 empty_module_context, 

379 names=names, 

380 wanted_type=wanted_type, 

381 wanted_names=wanted_names, 

382 complete=complete, 

383 convert=True, 

384 ) 

385 

386 def __repr__(self): 

387 return '<%s: %s>' % (self.__class__.__name__, self._path) 

388 

389 

390def _is_potential_project(path): 

391 for name in _CONTAINS_POTENTIAL_PROJECT: 

392 try: 

393 if path.joinpath(name).exists(): 

394 return True 

395 except OSError: 

396 continue 

397 return False 

398 

399 

400def _is_django_path(directory): 

401 """ Detects the path of the very well known Django library (if used) """ 

402 try: 

403 with open(directory.joinpath('manage.py'), 'rb') as f: 

404 return b"DJANGO_SETTINGS_MODULE" in f.read() 

405 except (FileNotFoundError, IsADirectoryError, PermissionError): 

406 return False 

407 

408 

409def get_default_project(path=None): 

410 """ 

411 If a project is not defined by the user, Jedi tries to define a project by 

412 itself as well as possible. Jedi traverses folders until it finds one of 

413 the following: 

414 

415 1. A ``.jedi/config.json`` 

416 2. One of the following files: ``setup.py``, ``.git``, ``.hg``, 

417 ``requirements.txt`` and ``MANIFEST.in``. 

418 """ 

419 if path is None: 

420 path = Path.cwd() 

421 elif isinstance(path, str): 

422 path = Path(path) 

423 

424 check = path.absolute() 

425 probable_path = None 

426 first_no_init_file = None 

427 for dir in chain([check], check.parents): 

428 try: 

429 return Project.load(dir) 

430 except (FileNotFoundError, IsADirectoryError, PermissionError): 

431 pass 

432 except NotADirectoryError: 

433 continue 

434 

435 if first_no_init_file is None: 

436 if dir.joinpath('__init__.py').exists(): 

437 # In the case that a __init__.py exists, it's in 99% just a 

438 # Python package and the project sits at least one level above. 

439 continue 

440 elif not dir.is_file(): 

441 first_no_init_file = dir 

442 

443 if _is_django_path(dir): 

444 project = Project(dir) 

445 project._django = True 

446 return project 

447 

448 if probable_path is None and _is_potential_project(dir): 

449 probable_path = dir 

450 

451 if probable_path is not None: 

452 return Project(probable_path) 

453 

454 if first_no_init_file is not None: 

455 return Project(first_no_init_file) 

456 

457 curdir = path if path.is_dir() else path.parent 

458 return Project(curdir) 

459 

460 

461def _remove_imports(names): 

462 return [ 

463 n for n in names 

464 if n.tree_name is None or n.api_type not in ('module', 'namespace') 

465 ]