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

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

366 statements  

1""" 

2Helpers for the API 

3""" 

4import re 

5import sys 

6from collections import namedtuple 

7from contextlib import contextmanager 

8from textwrap import dedent 

9from itertools import chain 

10from functools import wraps 

11from inspect import Parameter 

12from typing import Callable, TypeVar, Any, ParamSpec 

13 

14from parso.python.parser import Parser 

15from parso.python import tree 

16 

17from jedi.inference.base_value import NO_VALUES 

18from jedi.inference.syntax_tree import infer_atom 

19from jedi.inference.helpers import infer_call_of_leaf 

20from jedi.inference.compiled import get_string_value_set 

21from jedi.cache import signature_time_cache, memoize_method 

22from jedi.parser_utils import get_parent_scope 

23 

24T = TypeVar("T") 

25P = ParamSpec("P") 

26 

27CompletionParts = namedtuple('CompletionParts', ['path', 'has_dot', 'name']) 

28 

29RECURSION_LIMIT = 3000 

30 

31 

32@contextmanager 

33def higher_recursion_limit(): 

34 """Jedi needs a higher recursion limit for parsing and inference. Raise it 

35 only temporarily, so importing Jedi does not change global interpreter 

36 state and a user-defined higher limit is never lowered.""" 

37 old_limit = sys.getrecursionlimit() 

38 if old_limit >= RECURSION_LIMIT: 

39 yield 

40 else: 

41 sys.setrecursionlimit(RECURSION_LIMIT) 

42 try: 

43 yield 

44 finally: 

45 sys.setrecursionlimit(old_limit) 

46 

47 

48def recursion_limit(func: Callable[P, T]) -> Callable[P, T]: 

49 @wraps(func) 

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

51 with higher_recursion_limit(): 

52 return func(*args, **kwargs) 

53 return wrapper 

54 

55 

56def recursion_limit_generator(func): 

57 @wraps(func) 

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

59 with higher_recursion_limit(): 

60 iterator = iter(func(*args, **kwargs)) 

61 while True: 

62 with higher_recursion_limit(): 

63 try: 

64 value = next(iterator) 

65 except StopIteration: 

66 return 

67 # Restore the caller's limit before yielding a result. 

68 yield value 

69 return wrapper 

70 

71 

72def _start_match(string, like_name): 

73 return string.startswith(like_name) 

74 

75 

76def _fuzzy_match(string, like_name): 

77 if len(like_name) <= 1: 

78 return like_name in string 

79 pos = string.find(like_name[0]) 

80 if pos >= 0: 

81 return _fuzzy_match(string[pos + 1:], like_name[1:]) 

82 return False 

83 

84 

85def match(string, like_name, fuzzy=False): 

86 if fuzzy: 

87 return _fuzzy_match(string, like_name) 

88 else: 

89 return _start_match(string, like_name) 

90 

91 

92def sorted_definitions(defs): 

93 # Note: `or ''` below is required because `module_path` could be 

94 return sorted(defs, key=lambda x: (str(x.module_path or ''), 

95 x.line or 0, 

96 x.column or 0, 

97 x.name)) 

98 

99 

100def get_on_completion_name(module_node, lines, position): 

101 leaf = module_node.get_leaf_for_position(position) 

102 if leaf is None or leaf.type in ('string', 'error_leaf'): 

103 # Completions inside strings are a bit special, we need to parse the 

104 # string. The same is true for comments and error_leafs. 

105 line = lines[position[0] - 1] 

106 # The first step of completions is to get the name 

107 return re.search(r'(?!\d)\w+$|$', line[:position[1]]).group(0) 

108 elif leaf.type not in ('name', 'keyword'): 

109 return '' 

110 

111 return leaf.value[:position[1] - leaf.start_pos[1]] 

112 

113 

114def _get_code(code_lines, start_pos, end_pos): 

115 # Get relevant lines. 

116 lines = code_lines[start_pos[0] - 1:end_pos[0]] 

117 # Remove the parts at the end of the line. 

118 lines[-1] = lines[-1][:end_pos[1]] 

119 # Remove first line indentation. 

120 lines[0] = lines[0][start_pos[1]:] 

121 return ''.join(lines) 

122 

123 

124class OnErrorLeaf(Exception): 

125 @property 

126 def error_leaf(self): 

127 return self.args[0] 

128 

129 

130def _get_code_for_stack(code_lines, leaf, position): 

131 # It might happen that we're on whitespace or on a comment. This means 

132 # that we would not get the right leaf. 

133 if leaf.start_pos >= position: 

134 # If we're not on a comment simply get the previous leaf and proceed. 

135 leaf = leaf.get_previous_leaf() 

136 if leaf is None: 

137 return '' # At the beginning of the file. 

138 

139 is_after_newline = leaf.type == 'newline' 

140 while leaf.type == 'newline': 

141 leaf = leaf.get_previous_leaf() 

142 if leaf is None: 

143 return '' 

144 

145 if leaf.type == 'error_leaf' or leaf.type == 'string': 

146 if leaf.start_pos[0] < position[0]: 

147 # On a different line, we just begin anew. 

148 return '' 

149 

150 # Error leafs cannot be parsed, completion in strings is also 

151 # impossible. 

152 raise OnErrorLeaf(leaf) 

153 else: 

154 user_stmt = leaf 

155 while True: 

156 if user_stmt.parent.type in ('file_input', 'suite', 'simple_stmt'): 

157 break 

158 user_stmt = user_stmt.parent 

159 

160 if is_after_newline: 

161 if user_stmt.start_pos[1] > position[1]: 

162 # This means that it's actually a dedent and that means that we 

163 # start without value (part of a suite). 

164 return '' 

165 

166 # This is basically getting the relevant lines. 

167 return _get_code(code_lines, user_stmt.get_start_pos_of_prefix(), position) 

168 

169 

170def get_stack_at_position(grammar, code_lines, leaf, pos): 

171 """ 

172 Returns the possible node names (e.g. import_from, xor_test or yield_stmt). 

173 """ 

174 class EndMarkerReached(Exception): 

175 pass 

176 

177 def tokenize_without_endmarker(code): 

178 # TODO This is for now not an official parso API that exists purely 

179 # for Jedi. 

180 tokens = grammar._tokenize(code) 

181 for token in tokens: 

182 if token.string == safeword: 

183 raise EndMarkerReached() 

184 elif token.prefix.endswith(safeword): 

185 # This happens with comments. 

186 raise EndMarkerReached() 

187 elif token.string.endswith(safeword): 

188 yield token # Probably an f-string literal that was not finished. 

189 raise EndMarkerReached() 

190 else: 

191 yield token 

192 

193 # The code might be indedented, just remove it. 

194 code = dedent(_get_code_for_stack(code_lines, leaf, pos)) 

195 # We use a word to tell Jedi when we have reached the start of the 

196 # completion. 

197 # Use Z as a prefix because it's not part of a number suffix. 

198 safeword = 'ZZZ_USER_WANTS_TO_COMPLETE_HERE_WITH_JEDI' 

199 code = code + ' ' + safeword 

200 

201 p = Parser(grammar._pgen_grammar, error_recovery=True) 

202 try: 

203 p.parse(tokens=tokenize_without_endmarker(code)) 

204 except EndMarkerReached: 

205 return p.stack 

206 raise SystemError( 

207 "This really shouldn't happen. There's a bug in Jedi:\n%s" 

208 % list(tokenize_without_endmarker(code)) 

209 ) 

210 

211 

212def infer(inference_state, context, leaf): 

213 if leaf.type == 'name': 

214 return inference_state.infer(context, leaf) 

215 

216 parent = leaf.parent 

217 definitions = NO_VALUES 

218 if parent.type == 'atom': 

219 # e.g. `(a + b)` 

220 definitions = context.infer_node(leaf.parent) 

221 elif parent.type == 'trailer': 

222 # e.g. `a()` 

223 definitions = infer_call_of_leaf(context, leaf) 

224 elif isinstance(leaf, tree.Literal): 

225 # e.g. `"foo"` or `1.0` 

226 return infer_atom(context, leaf) 

227 elif leaf.type in ('fstring_string', 'fstring_start', 'fstring_end'): 

228 return get_string_value_set(inference_state) 

229 return definitions 

230 

231 

232def filter_follow_imports(names, follow_builtin_imports=False): 

233 for name in names: 

234 if name.is_import(): 

235 new_names = list(filter_follow_imports( 

236 name.goto(), 

237 follow_builtin_imports=follow_builtin_imports, 

238 )) 

239 found_builtin = False 

240 if follow_builtin_imports: 

241 for new_name in new_names: 

242 if new_name.start_pos is None: 

243 found_builtin = True 

244 

245 if found_builtin: 

246 yield name 

247 else: 

248 yield from new_names 

249 else: 

250 yield name 

251 

252 

253class CallDetails: 

254 def __init__(self, bracket_leaf, children, position): 

255 self.bracket_leaf = bracket_leaf 

256 self._children = children 

257 self._position = position 

258 

259 @property 

260 def index(self): 

261 return _get_index_and_key(self._children, self._position)[0] 

262 

263 @property 

264 def keyword_name_str(self): 

265 return _get_index_and_key(self._children, self._position)[1] 

266 

267 @memoize_method 

268 def _list_arguments(self): 

269 return list(_iter_arguments(self._children, self._position)) 

270 

271 def calculate_index(self, param_names): 

272 positional_count = 0 

273 used_names = set() 

274 star_count = -1 

275 args = self._list_arguments() 

276 if not args: 

277 if param_names: 

278 return 0 

279 else: 

280 return None 

281 

282 is_kwarg = False 

283 for i, (star_count, key_start, had_equal) in enumerate(args): 

284 is_kwarg |= had_equal | (star_count == 2) 

285 if star_count: 

286 pass # For now do nothing, we don't know what's in there here. 

287 else: 

288 if i + 1 != len(args): # Not last 

289 if had_equal: 

290 used_names.add(key_start) 

291 else: 

292 positional_count += 1 

293 

294 for i, param_name in enumerate(param_names): 

295 kind = param_name.get_kind() 

296 

297 if not is_kwarg: 

298 if kind == Parameter.VAR_POSITIONAL: 

299 return i 

300 if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY): 

301 if i == positional_count: 

302 return i 

303 

304 if key_start is not None and not star_count == 1 or star_count == 2: 

305 if param_name.string_name not in used_names \ 

306 and (kind == Parameter.KEYWORD_ONLY 

307 or kind == Parameter.POSITIONAL_OR_KEYWORD 

308 and positional_count <= i): 

309 if star_count: 

310 return i 

311 if had_equal: 

312 if param_name.string_name == key_start: 

313 return i 

314 else: 

315 if param_name.string_name.startswith(key_start): 

316 return i 

317 

318 if kind == Parameter.VAR_KEYWORD: 

319 return i 

320 return None 

321 

322 def iter_used_keyword_arguments(self): 

323 for star_count, key_start, had_equal in list(self._list_arguments()): 

324 if had_equal and key_start: 

325 yield key_start 

326 

327 def count_positional_arguments(self): 

328 count = 0 

329 for star_count, key_start, had_equal in self._list_arguments()[:-1]: 

330 if star_count or key_start: 

331 break 

332 count += 1 

333 return count 

334 

335 

336def _iter_arguments(nodes, position): 

337 def remove_after_pos(name): 

338 if name.type != 'name': 

339 return None 

340 return name.value[:position[1] - name.start_pos[1]] 

341 

342 # Returns Generator[Tuple[star_count, Optional[key_start: str], had_equal]] 

343 nodes_before = [c for c in nodes if c.start_pos < position] 

344 if nodes_before[-1].type == 'arglist': 

345 yield from _iter_arguments(nodes_before[-1].children, position) 

346 return 

347 

348 previous_node_yielded = False 

349 stars_seen = 0 

350 for i, node in enumerate(nodes_before): 

351 if node.type == 'argument': 

352 previous_node_yielded = True 

353 first = node.children[0] 

354 second = node.children[1] 

355 if second == '=': 

356 if second.start_pos < position and first.type == 'name': 

357 yield 0, first.value, True 

358 else: 

359 yield 0, remove_after_pos(first), False 

360 elif first in ('*', '**'): 

361 yield len(first.value), remove_after_pos(second), False 

362 else: 

363 # Must be a Comprehension 

364 first_leaf = node.get_first_leaf() 

365 if first_leaf.type == 'name' and first_leaf.start_pos >= position: 

366 yield 0, remove_after_pos(first_leaf), False 

367 else: 

368 yield 0, None, False 

369 stars_seen = 0 

370 elif node.type == 'testlist_star_expr': 

371 for n in node.children[::2]: 

372 if n.type == 'star_expr': 

373 stars_seen = 1 

374 n = n.children[1] 

375 yield stars_seen, remove_after_pos(n), False 

376 stars_seen = 0 

377 # The count of children is even if there's a comma at the end. 

378 previous_node_yielded = bool(len(node.children) % 2) 

379 elif isinstance(node, tree.PythonLeaf) and node.value == ',': 

380 if not previous_node_yielded: 

381 yield stars_seen, '', False 

382 stars_seen = 0 

383 previous_node_yielded = False 

384 elif isinstance(node, tree.PythonLeaf) and node.value in ('*', '**'): 

385 stars_seen = len(node.value) 

386 elif node == '=' and nodes_before[-1]: 

387 previous_node_yielded = True 

388 before = nodes_before[i - 1] 

389 if before.type == 'name': 

390 yield 0, before.value, True 

391 else: 

392 yield 0, None, False 

393 # Just ignore the star that is probably a syntax error. 

394 stars_seen = 0 

395 

396 if not previous_node_yielded: 

397 if nodes_before[-1].type == 'name': 

398 yield stars_seen, remove_after_pos(nodes_before[-1]), False 

399 else: 

400 yield stars_seen, '', False 

401 

402 

403def _get_index_and_key(nodes, position): 

404 """ 

405 Returns the amount of commas and the keyword argument string. 

406 """ 

407 nodes_before = [c for c in nodes if c.start_pos < position] 

408 if nodes_before[-1].type == 'arglist': 

409 return _get_index_and_key(nodes_before[-1].children, position) 

410 

411 key_str = None 

412 

413 last = nodes_before[-1] 

414 if last.type == 'argument' and last.children[1] == '=' \ 

415 and last.children[1].end_pos <= position: 

416 # Checked if the argument 

417 key_str = last.children[0].value 

418 elif last == '=': 

419 key_str = nodes_before[-2].value 

420 

421 return nodes_before.count(','), key_str 

422 

423 

424def _get_signature_details_from_error_node(node, additional_children, position): 

425 for index, element in reversed(list(enumerate(node.children))): 

426 # `index > 0` means that it's a trailer and not an atom. 

427 if element == '(' and element.end_pos <= position and index > 0: 

428 # It's an error node, we don't want to match too much, just 

429 # until the parentheses is enough. 

430 children = node.children[index:] 

431 name = element.get_previous_leaf() 

432 if name is None: 

433 continue 

434 if name.type == 'name' or name.parent.type in ('trailer', 'atom'): 

435 return CallDetails(element, children + additional_children, position) 

436 

437 

438def get_signature_details(module, position): 

439 leaf = module.get_leaf_for_position(position, include_prefixes=True) 

440 # It's easier to deal with the previous token than the next one in this 

441 # case. 

442 if leaf.start_pos >= position: 

443 # Whitespace / comments after the leaf count towards the previous leaf. 

444 leaf = leaf.get_previous_leaf() 

445 if leaf is None: 

446 return None 

447 

448 # Now that we know where we are in the syntax tree, we start to look at 

449 # parents for possible function definitions. 

450 node = leaf.parent 

451 while node is not None: 

452 if node.type in ('funcdef', 'classdef', 'decorated', 'async_stmt'): 

453 # Don't show signatures if there's stuff before it that just 

454 # makes it feel strange to have a signature. 

455 return None 

456 

457 additional_children = [] 

458 for n in reversed(node.children): 

459 if n.start_pos < position: 

460 if n.type == 'error_node': 

461 result = _get_signature_details_from_error_node( 

462 n, additional_children, position 

463 ) 

464 if result is not None: 

465 return result 

466 

467 additional_children[0:0] = n.children 

468 continue 

469 additional_children.insert(0, n) 

470 

471 # Find a valid trailer 

472 if node.type == 'trailer' and node.children[0] == '(' \ 

473 or node.type == 'decorator' and node.children[2] == '(': 

474 # Additionally we have to check that an ending parenthesis isn't 

475 # interpreted wrong. There are two cases: 

476 # 1. Cursor before paren -> The current signature is good 

477 # 2. Cursor after paren -> We need to skip the current signature 

478 if not (leaf is node.children[-1] and position >= leaf.end_pos): 

479 leaf = node.get_previous_leaf() 

480 if leaf is None: 

481 return None 

482 return CallDetails( 

483 node.children[0] if node.type == 'trailer' else node.children[2], 

484 node.children, 

485 position 

486 ) 

487 

488 node = node.parent 

489 

490 return None 

491 

492 

493@signature_time_cache("call_signatures_validity") 

494def cache_signatures(inference_state, context, bracket_leaf, code_lines, user_pos): 

495 """This function calculates the cache key.""" 

496 line_index = user_pos[0] - 1 

497 

498 before_cursor = code_lines[line_index][:user_pos[1]] 

499 other_lines = code_lines[bracket_leaf.start_pos[0]:line_index] 

500 whole = ''.join(other_lines + [before_cursor]) 

501 before_bracket = re.match(r'.*\(', whole, re.DOTALL) 

502 

503 module_path = context.get_root_context().py__file__() 

504 if module_path is None: 

505 yield None # Don't cache! 

506 else: 

507 yield (module_path, before_bracket, bracket_leaf.start_pos) 

508 yield infer( 

509 inference_state, 

510 context, 

511 bracket_leaf.get_previous_leaf(), 

512 ) 

513 

514 

515def validate_line_column(func: Callable[P, T]) -> Callable[P, T]: 

516 @wraps(func) 

517 def wrapper(self, line=None, column=None, *args, **kwargs) -> Any: 

518 line = max(len(self._code_lines), 1) if line is None else line 

519 if not (0 < line <= len(self._code_lines)): 

520 raise ValueError('`line` parameter is not in a valid range.') 

521 

522 line_string = self._code_lines[line - 1] 

523 line_len = len(line_string) 

524 if line_string.endswith('\r\n'): 

525 line_len -= 2 

526 elif line_string.endswith('\n'): 

527 line_len -= 1 

528 

529 column = line_len if column is None else column 

530 if not (0 <= column <= line_len): 

531 raise ValueError('`column` parameter (%d) is not in a valid range ' 

532 '(0-%d) for line %d (%r).' % ( 

533 column, line_len, line, line_string)) 

534 return func(self, line, column, *args, **kwargs) # type: ignore[arg-type] 

535 return wrapper # type: ignore[return-value] 

536 

537 

538def get_module_names(module, all_scopes, definitions=True, references=False): 

539 """ 

540 Returns a dictionary with name parts as keys and their call paths as 

541 values. 

542 """ 

543 def def_ref_filter(name): 

544 is_def = name.is_definition() 

545 return definitions and is_def or references and not is_def 

546 

547 names = list(chain.from_iterable(module.get_used_names().values())) 

548 if not all_scopes: 

549 # We have to filter all the names that don't have the module as a 

550 # parent_scope. There's None as a parent, because nodes in the module 

551 # node have the parent module and not suite as all the others. 

552 # Therefore it's important to catch that case. 

553 

554 def is_module_scope_name(name): 

555 parent_scope = get_parent_scope(name) 

556 # async functions have an extra wrapper. Strip it. 

557 if parent_scope and parent_scope.type == 'async_stmt': 

558 parent_scope = parent_scope.parent 

559 return parent_scope in (module, None) 

560 

561 names = [n for n in names if is_module_scope_name(n)] 

562 return filter(def_ref_filter, names) 

563 

564 

565def split_search_string(name): 

566 type, _, dotted_names = name.rpartition(' ') 

567 if type == 'def': 

568 type = 'function' 

569 return type, dotted_names.split('.')