Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/jedi-0.20.0-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

228 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 

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 # Set for projects loaded from a ``.jedi/project.json``. That file can be 

67 # part of a checked out repository, so it is not trusted to run binaries. 

68 _loaded_from_file = False 

69 

70 @staticmethod 

71 def _get_config_folder_path(base_path): 

72 return base_path.joinpath(_CONFIG_FOLDER) 

73 

74 @staticmethod 

75 def _get_json_path(base_path): 

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

77 

78 @classmethod 

79 def load(cls, path): 

80 """ 

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

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

83 

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

85 """ 

86 if isinstance(path, str): 

87 path = Path(path) 

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

89 version, data = json.load(f) 

90 

91 if version == 1: 

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

93 # extensions, see the docs about security. 

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

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

96 project = cls(**data) 

97 project._loaded_from_file = True 

98 return project 

99 else: 

100 raise WrongVersion( 

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

102 ) 

103 

104 def save(self): 

105 """ 

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

107 """ 

108 data = dict(self.__dict__) 

109 data.pop('_environment', None) 

110 data.pop('_loaded_from_file', None) 

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

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

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

114 

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

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

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

118 

119 def __init__( 

120 self, 

121 path, 

122 *, 

123 environment_path=None, 

124 load_unsafe_extensions=False, 

125 sys_path=None, 

126 added_sys_path=(), 

127 smart_sys_path=True, 

128 ) -> None: 

129 """ 

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

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

132 of a virtual environment. 

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

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

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

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

137 important and therefore have execution privileges. 

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

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

140 environment (virtualenvs, etc). 

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

142 sys path. 

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

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

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

146 """ 

147 

148 if isinstance(path, str): 

149 path = Path(path).absolute() 

150 self._path = path 

151 

152 self._environment_path = environment_path 

153 if sys_path is not None: 

154 # Remap potential pathlib.Path entries 

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

156 self._sys_path = sys_path 

157 self._smart_sys_path = smart_sys_path 

158 self._load_unsafe_extensions = load_unsafe_extensions 

159 self._django = False 

160 # Remap potential pathlib.Path entries 

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

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

163 

164 @property 

165 def path(self): 

166 """ 

167 The base path for this project. 

168 """ 

169 return self._path 

170 

171 @property 

172 def sys_path(self): 

173 """ 

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

175 case will be auto generated. 

176 """ 

177 return self._sys_path 

178 

179 @property 

180 def smart_sys_path(self): 

181 """ 

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

183 additional paths are added. 

184 """ 

185 return self._smart_sys_path 

186 

187 @property 

188 def load_unsafe_extensions(self): 

189 """ 

190 Wheter the project loads unsafe extensions. 

191 """ 

192 return self._load_unsafe_extensions 

193 

194 @inference_state_as_method_param_cache() 

195 def _get_base_sys_path(self, inference_state): 

196 # The sys path has not been set explicitly. 

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

198 try: 

199 sys_path.remove('') 

200 except ValueError: 

201 pass 

202 return sys_path 

203 

204 @inference_state_as_method_param_cache() 

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

206 """ 

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

208 one is used like a public method. 

209 """ 

210 suffixed = list(self.added_sys_path) 

211 prefixed = [] 

212 

213 if self._sys_path is None: 

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

215 else: 

216 sys_path = list(self._sys_path) 

217 

218 if self._smart_sys_path: 

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

220 

221 if inference_state.script_path is not None: 

222 suffixed += map(str, discover_buildout_paths( 

223 inference_state, 

224 inference_state.script_path 

225 )) 

226 

227 if add_parent_paths: 

228 # Collect directories in upward search by: 

229 # 1. Skipping directories with __init__.py 

230 # 2. Stopping immediately when above self._path 

231 traversed = [] 

232 for parent_path in inference_state.script_path.parents: 

233 if parent_path == self._path \ 

234 or self._path not in parent_path.parents: 

235 break 

236 if not add_init_paths \ 

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

238 continue 

239 traversed.append(str(parent_path)) 

240 

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

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

243 # rather than shorter ones by default. 

244 suffixed += reversed(traversed) 

245 

246 if self._django: 

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

248 

249 path = prefixed + sys_path + suffixed 

250 return list(_remove_duplicates_from_path(path)) 

251 

252 def get_environment(self): 

253 if self._environment is None: 

254 if self._environment_path is not None: 

255 # An environment path from a loaded project file is checked 

256 # like a scanned virtualenv, see find_virtualenvs. 

257 self._environment = create_environment( 

258 self._environment_path, safe=self._loaded_from_file) 

259 else: 

260 self._environment = get_cached_default_environment() 

261 return self._environment 

262 

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

264 """ 

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

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

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

268 results to the user. 

269 

270 There are currently three different search patterns: 

271 

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

273 ``foo.py`` or ``foo.pyi``. 

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

275 in it. 

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

277 API type. 

278 

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

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

281 functions and classes. 

282 :yields: :class:`.Name` 

283 """ 

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

285 

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

287 """ 

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

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

290 

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

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

293 functions and classes. 

294 :yields: :class:`.Completion` 

295 """ 

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

297 

298 @_try_to_skip_duplicates 

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

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

301 from jedi import Script 

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

303 inference_state = s._inference_state 

304 empty_module_context = s._get_module_context() 

305 

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

307 wanted_type, wanted_names = split_search_string(string) 

308 name = wanted_names[0] 

309 stub_folder_name = name + '-stubs' 

310 

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

312 file_ios = [] 

313 

314 # 1. Search for modules in the current project 

315 for folder_io, file_io in ios: 

316 if file_io is None: 

317 file_name = folder_io.get_base_name() 

318 if file_name == name or file_name == stub_folder_name: 

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

320 try: 

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

322 except FileNotFoundError: 

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

324 try: 

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

326 except FileNotFoundError: 

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

328 else: 

329 continue 

330 else: 

331 file_ios.append(file_io) 

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

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

334 else: 

335 continue 

336 

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

338 yield from search_in_module( 

339 inference_state, 

340 m, 

341 names=[m.name], 

342 wanted_type=wanted_type, 

343 wanted_names=wanted_names, 

344 complete=complete, 

345 convert=True, 

346 ignore_imports=True, 

347 ) 

348 

349 # 2. Search for identifiers in the project. 

350 for module_context in search_in_file_ios(inference_state, file_ios, 

351 name, complete=complete): 

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

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

354 names = _remove_imports(names) 

355 yield from search_in_module( 

356 inference_state, 

357 module_context, 

358 names=names, 

359 wanted_type=wanted_type, 

360 wanted_names=wanted_names, 

361 complete=complete, 

362 ignore_imports=True, 

363 ) 

364 

365 # 3. Search for modules on sys.path 

366 sys_path = [ 

367 p for p in self._get_sys_path(inference_state) 

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

369 if p != self._path 

370 ] 

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

372 yield from search_in_module( 

373 inference_state, 

374 empty_module_context, 

375 names=names, 

376 wanted_type=wanted_type, 

377 wanted_names=wanted_names, 

378 complete=complete, 

379 convert=True, 

380 ) 

381 

382 def __repr__(self): 

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

384 

385 

386def _is_potential_project(path): 

387 for name in _CONTAINS_POTENTIAL_PROJECT: 

388 try: 

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

390 return True 

391 except OSError: 

392 continue 

393 return False 

394 

395 

396def _is_django_path(directory): 

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

398 try: 

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

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

401 except (FileNotFoundError, IsADirectoryError, PermissionError): 

402 return False 

403 

404 

405def get_default_project(path=None): 

406 """ 

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

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

409 the following: 

410 

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

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

413 ``requirements.txt`` and ``MANIFEST.in``. 

414 """ 

415 if path is None: 

416 path = Path.cwd() 

417 elif isinstance(path, str): 

418 path = Path(path) 

419 

420 check = path.absolute() 

421 probable_path = None 

422 first_no_init_file = None 

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

424 try: 

425 return Project.load(dir) 

426 except (FileNotFoundError, IsADirectoryError, PermissionError): 

427 pass 

428 except NotADirectoryError: 

429 continue 

430 

431 if first_no_init_file is None: 

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

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

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

435 continue 

436 elif not dir.is_file(): 

437 first_no_init_file = dir 

438 

439 if _is_django_path(dir): 

440 project = Project(dir) 

441 project._django = True 

442 return project 

443 

444 if probable_path is None and _is_potential_project(dir): 

445 probable_path = dir 

446 

447 if probable_path is not None: 

448 return Project(probable_path) 

449 

450 if first_no_init_file is not None: 

451 return Project(first_no_init_file) 

452 

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

454 return Project(curdir) 

455 

456 

457def _remove_imports(names): 

458 return [ 

459 n for n in names 

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

461 ]