diff --git a/jedi/evaluate/names.py b/jedi/evaluate/names.py index eed5e5a6..eab00bbc 100644 --- a/jedi/evaluate/names.py +++ b/jedi/evaluate/names.py @@ -150,8 +150,18 @@ class TreeNameDefinition(AbstractTreeName): return self._API_TYPES.get(definition.type, 'statement') -class ParamNameInterface(object): - api_type = u'param' +class _ParamMixin(object): + def maybe_positional_argument(self, include_star=True): + options = [Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD] + if include_star: + options.append(Parameter.VAR_POSITIONAL) + return self.get_kind() in options + + def maybe_keyword_argument(self, include_stars=True): + options = [Parameter.KEYWORD_ONLY, Parameter.POSITIONAL_OR_KEYWORD] + if include_stars: + options.append(Parameter.VAR_KEYWORD) + return self.get_kind() in options def _kind_string(self): kind = self.get_kind() @@ -161,6 +171,10 @@ class ParamNameInterface(object): return '**' return '' + +class ParamNameInterface(_ParamMixin): + api_type = u'param' + def get_kind(self): raise NotImplementedError @@ -247,6 +261,17 @@ class ParamName(BaseTreeParamName): return params[param_node.position_index] +class ParamNameWrapper(_ParamMixin): + def __init__(self, param_name): + self._wrapped_param_name = param_name + + def __getattr__(self, name): + return getattr(self._wrapped_param_name, name) + + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self._wrapped_param_name) + + class ImportName(AbstractNameDefinition): start_pos = (1, 0) _level = 0 diff --git a/jedi/evaluate/signature.py b/jedi/evaluate/signature.py index 1f4cefbe..2e215af8 100644 --- a/jedi/evaluate/signature.py +++ b/jedi/evaluate/signature.py @@ -1,4 +1,6 @@ from jedi._compatibility import Parameter +from jedi.evaluate.utils import to_list +from jedi.evaluate.names import ParamNameWrapper class _SignatureMixin(object): @@ -77,6 +79,113 @@ 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: + for param_names in _iter_nodes_for_param(param_name, star_count=2): + for p in param_names: + if p.string_name not in used_names: + used_names.add(param_name.string_name) + yield p + print(p) + + +def _iter_nodes_for_param(param_name, star_count): + from jedi.evaluate.syntax_tree import eval_trailer + from jedi.evaluate.names import TreeNameDefinition + from parso.python.tree import search_ancestor + from jedi.evaluate.arguments import TreeArguments + + execution_context = param_name.parent_context + function_node = execution_context.tree_node + module_node = function_node.get_root_node() + start = function_node.children[-1].start_pos + 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. + argument = name.parent + if argument.type == 'argument' and argument.children[0] == '*' * star_count: + # No support for Python <= 3.4 here, but they are end-of-life + # 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, + context=context, + argument_node=trailer.children[1], + trailer=trailer, + ) + for c in contexts: + yield _remove_given_params(args, c.get_param_names(), star_count) + else: + assert False + + +def _remove_given_params(arguments, param_names, star_count): + count = 0 + used_keys = set() + for key, _ in arguments.unpack(): + if key is None: + count += 1 + else: + used_keys.add(key) + + for p in param_names: + kind = p.get_kind() + if count and kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.POSITIONAL_ONLY): + count -= 1 + continue + if p.string_name in used_keys \ + and kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY): + continue + + # TODO recurse on *args/**kwargs + if star_count == 1 and p.maybe_positional_argument(): + yield ParamNameFixedKind(p, Parameter.POSITIONAL_ONLY) + elif star_count == 2 and p.maybe_keyword_argument(): + yield ParamNameFixedKind(p, Parameter.KEYWORD_ONLY) + + +class ParamNameFixedKind(ParamNameWrapper): + def __init__(self, param_name, new_kind): + super(ParamNameFixedKind, self).__init__(param_name) + self._new_kind = new_kind + + def get_kind(self): + return self._new_kind + class BuiltinSignature(AbstractSignature): def __init__(self, context, return_string, is_bound=False): diff --git a/test/test_evaluate/test_signature.py b/test/test_evaluate/test_signature.py index 8cc05109..946b5929 100644 --- a/test/test_evaluate/test_signature.py +++ b/test/test_evaluate/test_signature.py @@ -60,6 +60,7 @@ c = functools.partial(func, 1, c=2) d = functools.partial() ''' + @pytest.mark.parametrize( 'code, expected', [ ('def f(a, * args, x): pass\n f(', 'f(a, *args, x)'), @@ -91,6 +92,38 @@ def test_tree_signature(Script, environment, code, expected): assert expected == sig._signature.to_string() +@pytest.mark.parametrize( + 'combination, expected', [ + ('combined_redirect(simple, simple2)', 'a, b, /, *, x'), + ('combined_redirect(simple, simple3)', 'a, b, /, *, a, x: int'), + ('combined_redirect(simple2, simple)', 'x, /, *, a, b, c'), + ('combined_redirect(simple3, simple)', 'a, x: int, /, *, a, b, c'), + ] +) +def test_nested_signatures(Script, environment, combination, expected): + code = dedent(''' + def simple(a, b, *, c): ... + def simple2(x): ... + def simple3(a, x: int): ... + def a(a, b, *args): ... + def kw(a, b, *, c, **kwargs): ... + def akw(a, b, *args, **kwargs): ... + + def no_redirect(func): + return lambda *args, **kwargs: func(1) + def full_redirect(func): + return lambda *args, **kwargs: func(1, *args, **kwargs) + def full_redirect(func): + return lambda *args, **kwargs: func(, *args, **kwargs) + def combined_redirect(func1, func2): + return lambda *args, **kwargs: func1(*args) + func2(**kwargs) + ''') + code += 'z = ' + combination + '\nz(' + sig, = Script(code).call_signatures() + computed = sig._signature.to_string() + assert '(' + expected + ')' == computed + + def test_pow_signature(Script): # See github #1357 sigs = Script('pow(').call_signatures()