Completion: Don't suggest variables when only kwargs are legal, fixes #1541

This commit is contained in:
Dave Halter
2020-04-17 22:58:55 +02:00
parent f871f5e726
commit f07dee3564
3 changed files with 127 additions and 20 deletions

View File

@@ -30,16 +30,43 @@ class ParamNameWithEquals(ParamNameWrapper):
return self.string_name + '=' return self.string_name + '='
def get_signature_param_names(signatures): def _get_signature_param_names(signatures, positional_count, used_kwargs):
# add named params # Add named params
for call_sig in signatures: 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. # Allow protected access, because it's a public API.
if p._name.get_kind() in (Parameter.POSITIONAL_OR_KEYWORD, # TODO reconsider with Python 2 drop
Parameter.KEYWORD_ONLY): 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) 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): def filter_names(inference_state, completion_names, stack, like_name, fuzzy, cached_name):
comp_dct = set() comp_dct = set()
if settings.case_insensitive_completion: if settings.case_insensitive_completion:
@@ -264,20 +291,34 @@ class Completion:
elif self._is_parameter_completion(): elif self._is_parameter_completion():
completion_names += self._complete_params(leaf) completion_names += self._complete_params(leaf)
else: else:
completion_names += self._complete_global_scope() # Apparently this looks like it's good enough to filter most cases
completion_names += self._complete_inherited(is_function=False) # 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 completion_names += _get_signature_param_names(
# so that signature completions don't randomly appear. signatures,
# To understand why this works, three things are important: positional_count,
# 1. trailer with a `,` in it is either a subscript or an arglist. used_kwargs,
# 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 kwargs_only = _must_be_kwarg(signatures, positional_count, used_kwargs)
# optional arglist in them.
if nodes[-1] in ['(', ','] and nonterminals[-1] in ('trailer', 'arglist', 'decorator'): if not kwargs_only:
signatures = self._signatures_callback(*self._position) completion_names += self._complete_global_scope()
completion_names += get_signature_param_names(signatures) completion_names += self._complete_inherited(is_function=False)
return cached_name, completion_names return cached_name, completion_names

View File

@@ -15,7 +15,7 @@ from jedi.inference.base_value import NO_VALUES
from jedi.inference.syntax_tree import infer_atom from jedi.inference.syntax_tree import infer_atom
from jedi.inference.helpers import infer_call_of_leaf from jedi.inference.helpers import infer_call_of_leaf
from jedi.inference.compiled import get_string_value_set 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 from jedi.parser_utils import get_parent_scope
@@ -216,11 +216,15 @@ class CallDetails(object):
def keyword_name_str(self): def keyword_name_str(self):
return _get_index_and_key(self._children, self._position)[1] 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): def calculate_index(self, param_names):
positional_count = 0 positional_count = 0
used_names = set() used_names = set()
star_count = -1 star_count = -1
args = list(_iter_arguments(self._children, self._position)) args = self._list_arguments()
if not args: if not args:
if param_names: if param_names:
return 0 return 0
@@ -267,6 +271,19 @@ class CallDetails(object):
return i return i
return None 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 _iter_arguments(nodes, position):
def remove_after_pos(name): def remove_after_pos(name):

View File

@@ -89,3 +89,52 @@ def x(): pass
#? 8 ['xyz='] #? 8 ['xyz=']
@foo(xyz) @foo(xyz)
def x(): pass 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)