From f07dee3564e93a2161e061acd170ccf968494972 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 17 Apr 2020 22:58:55 +0200 Subject: [PATCH] Completion: Don't suggest variables when only kwargs are legal, fixes #1541 --- jedi/api/completion.py | 77 ++++++++++++++++++++++++++-------- jedi/api/helpers.py | 21 +++++++++- test/completion/named_param.py | 49 ++++++++++++++++++++++ 3 files changed, 127 insertions(+), 20 deletions(-) diff --git a/jedi/api/completion.py b/jedi/api/completion.py index f5f81df0..e17f49cc 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -30,16 +30,43 @@ class ParamNameWithEquals(ParamNameWrapper): return self.string_name + '=' -def get_signature_param_names(signatures): - # add named params +def _get_signature_param_names(signatures, positional_count, used_kwargs): + # Add named params for call_sig in signatures: - for p in call_sig.params: + for i, p in enumerate(call_sig.params): # Allow protected access, because it's a public API. - if p._name.get_kind() in (Parameter.POSITIONAL_OR_KEYWORD, - Parameter.KEYWORD_ONLY): + # TODO reconsider with Python 2 drop + kind = p._name.get_kind() + if i < positional_count and kind == Parameter.POSITIONAL_OR_KEYWORD: + continue + if kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY) \ + and p.name not in used_kwargs: yield ParamNameWithEquals(p._name) +def _must_be_kwarg(signatures, positional_count, used_kwargs): + if used_kwargs: + return True + + must_be_kwarg = True + for signature in signatures: + for i, p in enumerate(signature.params): + # TODO reconsider with Python 2 drop + kind = p._name.get_kind() + if kind is Parameter.VAR_POSITIONAL: + # In case there were not already kwargs, the next param can + # always be a normal argument. + return False + + if i >= positional_count and kind in (Parameter.POSITIONAL_OR_KEYWORD, + Parameter.POSITIONAL_ONLY): + must_be_kwarg = False + break + if not must_be_kwarg: + break + return must_be_kwarg + + def filter_names(inference_state, completion_names, stack, like_name, fuzzy, cached_name): comp_dct = set() if settings.case_insensitive_completion: @@ -264,20 +291,34 @@ class Completion: elif self._is_parameter_completion(): completion_names += self._complete_params(leaf) else: - completion_names += self._complete_global_scope() - completion_names += self._complete_inherited(is_function=False) + # Apparently this looks like it's good enough to filter most cases + # so that signature completions don't randomly appear. + # To understand why this works, three things are important: + # 1. trailer with a `,` in it is either a subscript or an arglist. + # 2. If there's no `,`, it's at the start and only signatures start + # with `(`. Other trailers could start with `.` or `[`. + # 3. Decorators are very primitive and have an optional `(` with + # optional arglist in them. + kwargs_only = False + if nodes[-1] in ['(', ','] \ + and nonterminals[-1] in ('trailer', 'arglist', 'decorator'): + signatures = self._signatures_callback(*self._position) + if signatures: + call_details = signatures[0]._call_details + used_kwargs = list(call_details.iter_used_keyword_arguments()) + positional_count = call_details.count_positional_arguments() - # Apparently this looks like it's good enough to filter most cases - # so that signature completions don't randomly appear. - # To understand why this works, three things are important: - # 1. trailer with a `,` in it is either a subscript or an arglist. - # 2. If there's no `,`, it's at the start and only signatures start - # with `(`. Other trailers could start with `.` or `[`. - # 3. Decorators are very primitive and have an optional `(` with - # optional arglist in them. - if nodes[-1] in ['(', ','] and nonterminals[-1] in ('trailer', 'arglist', 'decorator'): - signatures = self._signatures_callback(*self._position) - completion_names += get_signature_param_names(signatures) + completion_names += _get_signature_param_names( + signatures, + positional_count, + used_kwargs, + ) + + kwargs_only = _must_be_kwarg(signatures, positional_count, used_kwargs) + + if not kwargs_only: + completion_names += self._complete_global_scope() + completion_names += self._complete_inherited(is_function=False) return cached_name, completion_names diff --git a/jedi/api/helpers.py b/jedi/api/helpers.py index f06ab694..91ec2806 100644 --- a/jedi/api/helpers.py +++ b/jedi/api/helpers.py @@ -15,7 +15,7 @@ from jedi.inference.base_value import NO_VALUES from jedi.inference.syntax_tree import infer_atom from jedi.inference.helpers import infer_call_of_leaf from jedi.inference.compiled import get_string_value_set -from jedi.cache import signature_time_cache +from jedi.cache import signature_time_cache, memoize_method from jedi.parser_utils import get_parent_scope @@ -216,11 +216,15 @@ class CallDetails(object): def keyword_name_str(self): return _get_index_and_key(self._children, self._position)[1] + @memoize_method + def _list_arguments(self): + return list(_iter_arguments(self._children, self._position)) + def calculate_index(self, param_names): positional_count = 0 used_names = set() star_count = -1 - args = list(_iter_arguments(self._children, self._position)) + args = self._list_arguments() if not args: if param_names: return 0 @@ -267,6 +271,19 @@ class CallDetails(object): return i return None + def iter_used_keyword_arguments(self): + for star_count, key_start, had_equal in list(self._list_arguments()): + if had_equal and key_start: + yield key_start + + def count_positional_arguments(self): + count = 0 + for star_count, key_start, had_equal in self._list_arguments()[:-1]: + if star_count: + break + count += 1 + return count + def _iter_arguments(nodes, position): def remove_after_pos(name): diff --git a/test/completion/named_param.py b/test/completion/named_param.py index 141ffe47..72ddcc0e 100644 --- a/test/completion/named_param.py +++ b/test/completion/named_param.py @@ -89,3 +89,52 @@ def x(): pass #? 8 ['xyz='] @foo(xyz) def x(): pass + +# ----------------- +# Only keyword arguments are valid +# ----------------- +# python >= 3.5 + +def x(bam, *, bar, baz): + pass +def y(bam, *bal, bar, baz, **bag): + pass +def z(bam, bar=2, *, bas=1): + pass + +#? 7 ['bar=', 'baz='] +x(1, ba) + +#? 14 ['baz='] +x(1, bar=2, ba) +#? 7 ['bar=', 'baz='] +x(1, ba, baz=3) +#? 14 ['baz='] +x(1, bar=2, baz=3) +#? 7 ['BaseException'] +x(basee) +#? 22 ['bar=', 'baz='] +x(1, 2, 3, 4, 5, 6, bar=2) + +#? 14 ['baz='] +y(1, bar=2, ba) +#? 7 ['bar=', 'BaseException', 'baz='] +y(1, ba, baz=3) +#? 14 ['baz='] +y(1, bar=2, baz=3) +#? 7 ['BaseException'] +y(basee) +#? 22 ['bar=', 'BaseException', 'baz='] +y(1, 2, 3, 4, 5, 6, bar=2) + +#? 11 ['bar=', 'bas='] +z(bam=1, bar=2, bas=3) +#? 8 ['BaseException', 'bas='] +z(1, bas=2) +#? 12 ['BaseException'] +z(1, bas=bas) + +#? 19 ['dict'] +z(1, bas=bas, **dic) +#? 18 ['dict'] +z(1, bas=bas, *dic)