diff --git a/jedi/evaluate/arguments.py b/jedi/evaluate/arguments.py index b96a6a01..cfa09c2b 100644 --- a/jedi/evaluate/arguments.py +++ b/jedi/evaluate/arguments.py @@ -11,6 +11,7 @@ from jedi.evaluate.lazy_context import LazyKnownContext, LazyKnownContexts, \ from jedi.evaluate.names import ParamName, TreeNameDefinition from jedi.evaluate.base_context import NO_CONTEXTS, ContextSet, ContextualizedNode from jedi.evaluate.context import iterable +from jedi.evaluate.cache import evaluator_as_method_param_cache from jedi.evaluate.param import get_executed_params_and_issues, ExecutedParam @@ -208,6 +209,11 @@ class TreeArguments(AbstractArguments): self._evaluator = evaluator self.trailer = trailer # Can be None, e.g. in a class definition. + @classmethod + @evaluator_as_method_param_cache() + def create_cached(cls, *args, **kwargs): + return cls(*args, **kwargs) + def unpack(self, funcdef=None): named_args = [] for star_count, el in unpack_arglist(self.argument_node): diff --git a/jedi/evaluate/names.py b/jedi/evaluate/names.py index eab00bbc..563506b3 100644 --- a/jedi/evaluate/names.py +++ b/jedi/evaluate/names.py @@ -187,6 +187,15 @@ class ParamNameInterface(_ParamMixin): # clear what values would be allowed. return None + @property + def star_count(self): + kind = self.get_kind() + if kind == Parameter.VAR_POSITIONAL: + return 1 + if kind == Parameter.VAR_KEYWORD: + return 2 + return 0 + class BaseTreeParamName(ParamNameInterface, AbstractTreeName): annotation_node = None diff --git a/jedi/evaluate/signature.py b/jedi/evaluate/signature.py index b00e328d..b9223fef 100644 --- a/jedi/evaluate/signature.py +++ b/jedi/evaluate/signature.py @@ -79,41 +79,11 @@ class TreeSignature(AbstractSignature): return '' return a.get_code(include_prefix=False) - @to_list def get_param_names(self): - used_names = set() - kwarg_params = [] - for param_name in super(TreeSignature, self).get_param_names(): - kind = param_name.get_kind() - if kind == Parameter.VAR_POSITIONAL: - for param_names in _iter_nodes_for_param(param_name, star_count=1): - for p in param_names: - yield p - break - else: - yield param_name - elif kind == Parameter.VAR_KEYWORD: - kwarg_params.append(param_name) - else: - if param_name.maybe_keyword_argument(): - used_names.add(param_name.string_name) - yield param_name - - for param_name in kwarg_params: - itered = list(_iter_nodes_for_param(param_name, star_count=2)) - if not itered: - yield param_name - - for param_names in itered: - for p in param_names: - if p.string_name not in used_names or p.get_kind() == Parameter.VAR_KEYWORD: - used_names.add(p.string_name) - yield p + return _process_params(super(TreeSignature, self).get_param_names()) -def _iter_nodes_for_param(param_name, star_count): - from jedi.evaluate.syntax_tree import eval_trailer - from jedi.evaluate.names import TreeNameDefinition +def _iter_nodes_for_param(param_name): from parso.python.tree import search_ancestor from jedi.evaluate.arguments import TreeArguments @@ -124,41 +94,54 @@ def _iter_nodes_for_param(param_name, star_count): end = function_node.children[-1].end_pos for name in module_node.get_used_names().get(param_name.string_name): if start <= name.start_pos < end: - # Is used in the function. + # Is used in the function argument = name.parent - if argument.type == 'argument' and argument.children[0] == '*' * star_count: + if argument.type == 'argument' \ + and argument.children[0] == '*' * param_name.star_count: # No support for Python <= 3.4 here, but they are end-of-life - # anyway. + # anyway trailer = search_ancestor(argument, 'trailer') - if trailer is not None: - atom_expr = trailer.parent - context = execution_context.create_context(atom_expr) - found = TreeNameDefinition(context, name).goto() - if any(param_name.parent_context == p.parent_context - and param_name.start_pos == p.start_pos - for p in found): - index = atom_expr.children[0] == 'await' - # Eval atom first - contexts = context.eval_node(atom_expr.children[index]) - for trailer2 in atom_expr.children[index + 1:]: - if trailer == trailer2: - break - contexts = eval_trailer(context, contexts, trailer2) - args = TreeArguments( - evaluator=execution_context.evaluator, + if trailer is not None: # Make sure we're in a function + context = execution_context.create_context(trailer) + if _goes_to_param_name(param_name, context, name): + contexts = _to_callables(context, trailer) + + args = TreeArguments.create_cached( + execution_context.evaluator, context=context, argument_node=trailer.children[1], trailer=trailer, ) for c in contexts: - yield list(_process_params( - _remove_given_params(args, c.get_param_names()), - star_count, - )) + yield c, args else: assert False +def _goes_to_param_name(param_name, context, potential_name): + if potential_name.type != 'name': + return False + from jedi.evaluate.names import TreeNameDefinition + found = TreeNameDefinition(context, potential_name).goto() + return any(param_name.parent_context == p.parent_context + and param_name.start_pos == p.start_pos + for p in found) + + +def _to_callables(context, trailer): + from jedi.evaluate.syntax_tree import eval_trailer + + atom_expr = trailer.parent + index = atom_expr.children[0] == 'await' + # Eval atom first + contexts = context.eval_node(atom_expr.children[index]) + for trailer2 in atom_expr.children[index + 1:]: + if trailer == trailer2: + break + contexts = eval_trailer(context, contexts, trailer2) + return contexts + + def _remove_given_params(arguments, param_names): count = 0 used_keys = set() @@ -179,34 +162,82 @@ def _remove_given_params(arguments, param_names): def _process_params(param_names, star_count=3): # default means both * and ** used_names = set() - param_names = list(param_names) + kw_only_params = [] + arg_funcs = [] + kwarg_funcs = [] + kwarg_names = [] + longest_param_names = () for p in param_names: - if star_count == 1 and p.maybe_positional_argument(): - if p.get_kind() == Parameter.VAR_POSITIONAL: - for param_names in _iter_nodes_for_param(p, star_count=1): - for p in param_names: - yield p - break - else: + kind = p.get_kind() + if kind == Parameter.VAR_POSITIONAL: + if star_count & 1: + arg_funcs = list(_iter_nodes_for_param(p)) + if not arg_funcs: yield p - else: + elif p.get_kind() == Parameter.VAR_KEYWORD: + if star_count & 2: + kwarg_funcs = list(_iter_nodes_for_param(p)) + if not kwarg_funcs: + kwarg_names.append(p) + elif kind == Parameter.KEYWORD_ONLY: + if star_count & 2: + kw_only_params.append(p) + elif kind == Parameter.POSITIONAL_ONLY: + if star_count & 1: + yield p + else: + if star_count == 1: yield ParamNameFixedKind(p, Parameter.POSITIONAL_ONLY) - elif star_count == 2 and p.maybe_keyword_argument(): - if p.get_kind() == Parameter.VAR_KEYWORD: - itered = list(_iter_nodes_for_param(p, star_count=2)) - if not itered: - # We were not able to resolve kwargs. - yield p - for param_names in itered: - for p in param_names: - if p.string_name not in used_names: - used_names.add(p.string_name) - yield p - else: + elif star_count == 2: yield ParamNameFixedKind(p, Parameter.KEYWORD_ONLY) - elif star_count == 3: + else: + yield p + + for func_and_argument in arg_funcs: + func, arguments = func_and_argument + new_star_count = star_count + if func_and_argument in kwarg_funcs: + kwarg_funcs.remove(func_and_argument) + else: + new_star_count = 1 + + args_for_this_func = [] + for p in _process_params( + list(_remove_given_params( + arguments, + func.get_param_names() + )), new_star_count): + if p.get_kind() == Parameter.VAR_KEYWORD: + kwarg_names.append(p) + elif p.get_kind() == Parameter.KEYWORD_ONLY: + kw_only_params.append(p) + else: + args_for_this_func.append(p) + if len(args_for_this_func) > len(longest_param_names): + longest_param_names = args_for_this_func + + for p in longest_param_names: + if star_count == 1 and p.get_kind() != Parameter.VAR_POSITIONAL: + yield ParamNameFixedKind(p, Parameter.POSITIONAL_ONLY) + else: yield p + for p in kw_only_params: + yield p + + for func, arguments in kwarg_funcs: + for p in _process_params( + list(_remove_given_params( + arguments, + func.get_param_names() + )), star_count=2): + if p.get_kind() != Parameter.KEYWORD_ONLY or not kwarg_names: + yield p + + if kwarg_names: + yield kwarg_names[0] + return + class ParamNameFixedKind(ParamNameWrapper): def __init__(self, param_name, new_kind): diff --git a/test/test_evaluate/test_signature.py b/test/test_evaluate/test_signature.py index 0a2c6235..2bb81126 100644 --- a/test/test_evaluate/test_signature.py +++ b/test/test_evaluate/test_signature.py @@ -94,6 +94,8 @@ def test_tree_signature(Script, environment, code, expected): @pytest.mark.parametrize( 'combination, expected', [ + ('full_redirect(simple)', 'b, *, c'), + ('combined_redirect(simple, simple2)', 'a, b, /, *, x'), ('combined_redirect(simple, simple3)', 'a, b, /, *, a, x: int'), ('combined_redirect(simple2, simple)', 'x, /, *, a, b, c'),