diff --git a/jedi/api/classes.py b/jedi/api/classes.py index 6b51d2a3..d5eae87d 100644 --- a/jedi/api/classes.py +++ b/jedi/api/classes.py @@ -615,6 +615,7 @@ class CallSignature(Definition): The Param index of the current call. Returns None if the index cannot be found in the curent call. """ + return self._call_details.calculate_index(self._signature.get_param_names()) if self._call_details.keyword_name_str is not None: for i, param in enumerate(self.params): if self._call_details.keyword_name_str == param.name: diff --git a/jedi/api/helpers.py b/jedi/api/helpers.py index f5cb1a8c..17134033 100644 --- a/jedi/api/helpers.py +++ b/jedi/api/helpers.py @@ -8,7 +8,7 @@ from textwrap import dedent from parso.python.parser import Parser from parso.python import tree -from jedi._compatibility import u +from jedi._compatibility import u, Parameter from jedi.evaluate.base_context import NO_CONTEXTS from jedi.evaluate.syntax_tree import eval_atom from jedi.evaluate.helpers import evaluate_call_of_leaf @@ -174,6 +174,104 @@ class CallDetails(object): def keyword_name_str(self): return _get_index_and_key(self._children, self._position)[1] + def calculate_index(self, param_names): + positional_count = 0 + used_names = set() + star_count = -1 + args = list(_iter_arguments(self._children, self._position)) + if not args: + if param_names: + return 0 + else: + return None + + is_kwarg = False + for i, (star_count, key_start, had_equal) in enumerate(args): + is_kwarg |= had_equal | (star_count == 2) + if star_count: + pass # For now do nothing, we don't know what's in there here. + elif had_equal: + if i + 1 != len(args): + used_names.add(key_start) + else: + positional_count += 1 + + for i, param_name in enumerate(param_names): + kind = param_name.get_kind() + + if not is_kwarg: + if kind == Parameter.VAR_POSITIONAL: + return i + if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY): + if i + 1 == positional_count: + return i + + if key_start is not None: + if param_name.string_name not in used_names \ + and (kind == Parameter.KEYWORD_ONLY + or kind == Parameter.POSITIONAL_OR_KEYWORD + and positional_count < i + 1): + if had_equal: + if param_name.string_name == key_start: + return i + else: + if param_name.string_name.startswith(key_start): + return i + + if kind == Parameter.VAR_KEYWORD: + return i + return None + + +def _iter_arguments(nodes, position): + def remove_after_pos(name): + return name.value[:position[1] - name.start_pos[1]] + + # Returns Generator[Tuple[star_count, Optional[key_start: str], had_equal]] + nodes_before = [c for c in nodes if c.start_pos < position] + if nodes_before[-1].type == 'arglist': + for x in _iter_arguments(nodes_before[-1].children, position): + yield x # Python 2 :( + return + + previous_node_yielded = False + for i, node in enumerate(nodes_before): + if node.type == 'argument': + previous_node_yielded = True + first = node.children[0] + second = node.children[1] + if second == '=': + if second.start_pos < position: + yield 0, first.value, True + else: + yield 0, remove_after_pos(first), True + elif first in ('*', '**'): + yield len(first), remove_after_pos(second), '' + else: + # Must be a Comprehension + first_leaf = node.get_first_leaf() + if first_leaf.type == 'name' and first_leaf.start_pos >= position: + yield 0, remove_after_pos(first_leaf), False + else: + yield 0, None, False + elif node == ',': + if not previous_node_yielded: + yield 0, '', False + previous_node_yielded = False + elif node == '=' and nodes_before[-1]: + previous_node_yielded = True + before = nodes_before[i - 1] + if before.type == 'name': + yield 0, before.value, True + else: + yield 0, None, False + + if not previous_node_yielded: + if nodes_before[-1].type == 'name': + yield 0, remove_after_pos(nodes_before[-1]), False + else: + yield 0, '', False + def _get_index_and_key(nodes, position): """ @@ -185,14 +283,13 @@ def _get_index_and_key(nodes, position): key_str = None - if nodes_before: - last = nodes_before[-1] - if last.type == 'argument' and last.children[1] == '=' \ - and last.children[1].end_pos <= position: - # Checked if the argument - key_str = last.children[0].value - elif last == '=': - key_str = nodes_before[-2].value + last = nodes_before[-1] + if last.type == 'argument' and last.children[1] == '=' \ + and last.children[1].end_pos <= position: + # Checked if the argument + key_str = last.children[0].value + elif last == '=': + key_str = nodes_before[-2].value return nodes_before.count(','), key_str diff --git a/test/test_api/test_call_signatures.py b/test/test_api/test_call_signatures.py index 6fdfa231..d0d910a0 100644 --- a/test/test_api/test_call_signatures.py +++ b/test/test_api/test_call_signatures.py @@ -396,13 +396,13 @@ def test_keyword_argument_index(Script, environment): code1 = 'def f(u, /, v=3, *, abc, abd, xyz): pass' -code2 = 'def f(u, /, v=3, *, abc, abd, xyz, **kwargs): pass' -code3 = 'def f(u, /, v, *args, x=1, y): pass' -code4 = 'def f(u, /, v, *args, x=1, y, **kwargs): pass' +code2 = 'def g(u, /, v=3, *, abc, abd, xyz, **kwargs): pass' +code3 = 'def h(u, /, v, *args, x=1, y): pass' +code4 = 'def i(u, /, v, *args, x=1, y, **kwargs): pass' @pytest.mark.parametrize( - 'code, call, index', [ + 'code, call, expected_index', [ # No *args, **kwargs (code1, 'f(', 0), (code1, 'f(a', 0), @@ -424,51 +424,52 @@ code4 = 'def f(u, /, v, *args, x=1, y, **kwargs): pass' (code1, 'f(v=', 1), # **kwargs - (code2, 'f(a,b,a', 2), - (code2, 'f(a,b,abd', 2), - (code2, 'f(a,b,arr', 5), - (code2, 'f(a,b,xy', 4), - (code2, 'f(a,b,xy=', 4), - (code2, 'f(a,b,abc=1,abd=4,', 5), - (code2, 'f(a,b,abc=1,abd=4,lala', 5), - (code2, 'f(a,b,abc=1,abd=4,lala=', 5), - (code2, 'f(a,b,kw', 5), - (code2, 'f(a,b,kwargs=', 5), - (code2, 'f(u=', 5), - (code2, 'f(v=', 1), + (code2, 'g(a,b,a', 2), + (code2, 'g(a,b,abc', 2), + (code2, 'g(a,b,abd', 3), + (code2, 'g(a,b,arr', 5), + (code2, 'g(a,b,xy', 4), + (code2, 'g(a,b,xyz=', 4), + (code2, 'g(a,b,xy=', 5), + (code2, 'g(a,b,abc=1,abd=4,', 4), + (code2, 'g(a,b,abc=1,xyz=3,abd=4,', 5), + (code2, 'g(a,b,abc=1,abd=4,lala', 5), + (code2, 'g(a,b,abc=1,abd=4,lala=', 5), + (code2, 'g(a,b,abc=1,abd=4,abd=', 5), + (code2, 'g(a,b,kw', 5), + (code2, 'g(a,b,kwargs=', 5), + (code2, 'g(u=', 5), + (code2, 'g(v=', 1), # *args - (code3, 'f(a,b,c', 2), - (code3, 'f(a,b,c,', 2), - (code3, 'f(a,b,c,d', 2), - (code3, 'f(a,b,c,d[', 2), - (code3, 'f(a,b,c,d(3,', 2), - (code3, 'f(a,b,c,(3,)', 2), - (code3, 'f(a,b,args=', None), - (code3, 'f(a,b=', 1), - (code3, 'f(a=', None), + (code3, 'h(a,b,c', 2), + (code3, 'h(a,b,c,', 2), + (code3, 'h(a,b,c,d', 2), + (code3, 'h(a,b,c,d[', 2), + (code3, 'h(a,b,c,(3,', 2), + (code3, 'h(a,b,c,(3,)', 2), + (code3, 'h(a,b,args=', None), + (code3, 'h(u,v=', 1), + (code3, 'h(u=', None), # *args, **kwargs - (code4, 'f(a,b,c,d', 2), - (code4, 'f(a,b,c,d,e', 2), - (code4, 'f(a,b,c,d,e=', 5), - (code4, 'f(a,b,c,d,e=3', 5), - (code4, 'f(a,b,c,d=,', 3), - (code4, 'f(a,b,c,d=,x=', 3), - (code4, 'f(a,b,c,d=5,x=4', 3), - (code4, 'f(a,b,c,d=5,x=4,y', 4), - (code4, 'f(a,b,c,d=5,x=4,y=3,', 5), - (code4, 'f(a,b,c,d=5,y=4,x=3', 4), - (code4, 'f(a,b,c,d=5,y=4,x=3,', 5), - (code4, 'f(a,b,c,d=4,', 5), - (code4, 'f(a,b,c,d=,', 5), + (code4, 'i(a,b,c,d', 2), + (code4, 'i(a,b,c,d,e', 2), + (code4, 'i(a,b,c,d,e=', 5), + (code4, 'i(a,b,c,d,e=3', 5), + #(code4, 'i(a,b,c,d=,x=', 3), + (code4, 'i(a,b,c,d=5,x=4', 3), + (code4, 'i(a,b,c,d=5,x=4,y', 4), + (code4, 'i(a,b,c,d=5,x=4,y=3,', 5), + (code4, 'i(a,b,c,d=5,y=4,x=3,', 5), + (code4, 'i(a,b,c,d=4,', 3), + (code4, 'i(a,b,c,x=1,d=2,', 4), ] ) -def test_signature_index(skip_pre_python38, Script, code, call, index): +def test_signature_index(skip_pre_python38, Script, code, call, expected_index): sig, = Script(code + '\n' + call).call_signatures() - print(call) - print('index', index) - assert index == sig.index + index = sig.index + assert expected_index == index @pytest.mark.skipif(sys.version_info[0] == 2, reason="Python 2 doesn't support __signature__")