From c490d37c2debc1dce2c8e275044a5ddf287cfd9b Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 26 Jul 2019 02:54:50 +0200 Subject: [PATCH 01/14] Start getting signature inferring working --- jedi/evaluate/names.py | 29 ++++++- jedi/evaluate/signature.py | 109 +++++++++++++++++++++++++++ test/test_evaluate/test_signature.py | 33 ++++++++ 3 files changed, 169 insertions(+), 2 deletions(-) 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() From 0703a69369b8605b2eb417cd764b6d1e7eb5a271 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 26 Jul 2019 12:11:45 +0200 Subject: [PATCH 02/14] Some progress in signature understanding --- jedi/evaluate/signature.py | 17 +++++++++++------ test/test_evaluate/test_signature.py | 11 +++++++++-- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/jedi/evaluate/signature.py b/jedi/evaluate/signature.py index 2e215af8..55d50527 100644 --- a/jedi/evaluate/signature.py +++ b/jedi/evaluate/signature.py @@ -148,12 +148,15 @@ def _iter_nodes_for_param(param_name, star_count): trailer=trailer, ) for c in contexts: - yield _remove_given_params(args, c.get_param_names(), star_count) + yield _process_params( + _remove_given_params(args, c.get_param_names()), + star_count, + ) else: assert False -def _remove_given_params(arguments, param_names, star_count): +def _remove_given_params(arguments, param_names): count = 0 used_keys = set() for key, _ in arguments.unpack(): @@ -163,14 +166,16 @@ def _remove_given_params(arguments, param_names, star_count): 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): + if count and p.maybe_positional_argument(): count -= 1 continue - if p.string_name in used_keys \ - and kind in (Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY): + if p.string_name in used_keys and p.maybe_keyword_argument(): continue + yield p + +def _process_params(param_names, star_count): + for p in param_names: # TODO recurse on *args/**kwargs if star_count == 1 and p.maybe_positional_argument(): yield ParamNameFixedKind(p, Parameter.POSITIONAL_ONLY) diff --git a/test/test_evaluate/test_signature.py b/test/test_evaluate/test_signature.py index 946b5929..7e20fd58 100644 --- a/test/test_evaluate/test_signature.py +++ b/test/test_evaluate/test_signature.py @@ -98,6 +98,12 @@ def test_tree_signature(Script, environment, code, expected): ('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'), + + ('combined_redirect(simple, kw)', 'a, b, /, *, a, b, c, **kwargs'), + ('combined_redirect(kw, simple)', 'a, b, /, *, a, b, c, **kwargs'), + + ('combined_lot_of_args(kw, simple4)', '*, b'), + ('combined_lot_of_args(simple4, kw)', '*, b, c'), ] ) def test_nested_signatures(Script, environment, combination, expected): @@ -105,6 +111,7 @@ def test_nested_signatures(Script, environment, combination, expected): def simple(a, b, *, c): ... def simple2(x): ... def simple3(a, x: int): ... + def simple4(a, b, x: int): ... def a(a, b, *args): ... def kw(a, b, *, c, **kwargs): ... def akw(a, b, *args, **kwargs): ... @@ -113,10 +120,10 @@ def test_nested_signatures(Script, environment, combination, expected): 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) + def combined_lot_of_args(func1, func2): + return lambda *args, **kwargs: func1(1, 2, 3, 4, *args) + func2(a=3, x=1, y=1, **kwargs) ''') code += 'z = ' + combination + '\nz(' sig, = Script(code).call_signatures() From ba160e72abd316cda0e30ecf0eb34179b79b1ae8 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 26 Jul 2019 14:29:33 +0200 Subject: [PATCH 03/14] Some more signature progress --- jedi/evaluate/signature.py | 36 +++++++++++++++++++++------- test/test_evaluate/test_signature.py | 4 ++-- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/jedi/evaluate/signature.py b/jedi/evaluate/signature.py index 55d50527..702ed3ae 100644 --- a/jedi/evaluate/signature.py +++ b/jedi/evaluate/signature.py @@ -102,10 +102,9 @@ class TreeSignature(AbstractSignature): 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) + if p.string_name not in used_names or p.get_kind() == Parameter.VAR_KEYWORD: + used_names.add(p.string_name) yield p - print(p) def _iter_nodes_for_param(param_name, star_count): @@ -148,10 +147,10 @@ def _iter_nodes_for_param(param_name, star_count): trailer=trailer, ) for c in contexts: - yield _process_params( + yield list(_process_params( _remove_given_params(args, c.get_param_names()), star_count, - ) + )) else: assert False @@ -174,13 +173,32 @@ def _remove_given_params(arguments, param_names): yield p -def _process_params(param_names, star_count): +def _process_params(param_names, star_count=3): # default means both * and ** + used_names = set() + param_names = list(param_names) for p in param_names: - # TODO recurse on *args/**kwargs if star_count == 1 and p.maybe_positional_argument(): - yield ParamNameFixedKind(p, Parameter.POSITIONAL_ONLY) + if p.get_kind() == Parameter.VAR_POSITIONAL: + yield p + else: + yield ParamNameFixedKind(p, Parameter.POSITIONAL_ONLY) elif star_count == 2 and p.maybe_keyword_argument(): - yield ParamNameFixedKind(p, Parameter.KEYWORD_ONLY) + if p.get_kind() == Parameter.VAR_KEYWORD: + yield p + continue + 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: + yield ParamNameFixedKind(p, Parameter.KEYWORD_ONLY) + elif star_count == 3: + yield p class ParamNameFixedKind(ParamNameWrapper): diff --git a/test/test_evaluate/test_signature.py b/test/test_evaluate/test_signature.py index 7e20fd58..97103419 100644 --- a/test/test_evaluate/test_signature.py +++ b/test/test_evaluate/test_signature.py @@ -100,10 +100,10 @@ def test_tree_signature(Script, environment, code, expected): ('combined_redirect(simple3, simple)', 'a, x: int, /, *, a, b, c'), ('combined_redirect(simple, kw)', 'a, b, /, *, a, b, c, **kwargs'), - ('combined_redirect(kw, simple)', 'a, b, /, *, a, b, c, **kwargs'), + ('combined_redirect(kw, simple)', 'a, b, /, *, a, b, c'), ('combined_lot_of_args(kw, simple4)', '*, b'), - ('combined_lot_of_args(simple4, kw)', '*, b, c'), + ('combined_lot_of_args(simple4, kw)', '*, b, c, **kwargs'), ] ) def test_nested_signatures(Script, environment, combination, expected): From 41dc5382fa5e17a44635472e46c541c4e3de7da1 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 26 Jul 2019 14:42:20 +0200 Subject: [PATCH 04/14] Make nesting of *args/**kwargs possible to understand. --- jedi/evaluate/signature.py | 9 ++++++--- test/test_evaluate/test_signature.py | 6 ++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/jedi/evaluate/signature.py b/jedi/evaluate/signature.py index 702ed3ae..ad514bbc 100644 --- a/jedi/evaluate/signature.py +++ b/jedi/evaluate/signature.py @@ -179,13 +179,16 @@ def _process_params(param_names, star_count=3): # default means both * and ** for p in param_names: if star_count == 1 and p.maybe_positional_argument(): if p.get_kind() == Parameter.VAR_POSITIONAL: - yield p + for param_names in _iter_nodes_for_param(p, star_count=1): + for p in param_names: + yield p + break + else: + yield p else: yield ParamNameFixedKind(p, Parameter.POSITIONAL_ONLY) elif star_count == 2 and p.maybe_keyword_argument(): if p.get_kind() == Parameter.VAR_KEYWORD: - yield p - continue itered = list(_iter_nodes_for_param(p, star_count=2)) if not itered: # We were not able to resolve kwargs. diff --git a/test/test_evaluate/test_signature.py b/test/test_evaluate/test_signature.py index 97103419..c6589b57 100644 --- a/test/test_evaluate/test_signature.py +++ b/test/test_evaluate/test_signature.py @@ -104,6 +104,11 @@ def test_tree_signature(Script, environment, code, expected): ('combined_lot_of_args(kw, simple4)', '*, b'), ('combined_lot_of_args(simple4, kw)', '*, b, c, **kwargs'), + + ('combined_redirect(combined_redirect(simple2, simple4), combined_redirect(kw, simple5))', + 'x, /, *, y'), + ('combined_redirect(combined_redirect(simple4, simple2), combined_redirect(simple5, kw))', + 'a, b, x: int, /, *, a, b, c, **kwargs'), ] ) def test_nested_signatures(Script, environment, combination, expected): @@ -112,6 +117,7 @@ def test_nested_signatures(Script, environment, combination, expected): def simple2(x): ... def simple3(a, x: int): ... def simple4(a, b, x: int): ... + def simple5(y): ... def a(a, b, *args): ... def kw(a, b, *, c, **kwargs): ... def akw(a, b, *args, **kwargs): ... From 6a480780f8bb053f3532dbf8bde9ab7ee9f58c3c Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Fri, 26 Jul 2019 14:51:30 +0200 Subject: [PATCH 05/14] Some more tests --- jedi/evaluate/signature.py | 6 +++++- test/test_evaluate/test_signature.py | 6 ++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/jedi/evaluate/signature.py b/jedi/evaluate/signature.py index ad514bbc..b00e328d 100644 --- a/jedi/evaluate/signature.py +++ b/jedi/evaluate/signature.py @@ -100,7 +100,11 @@ class TreeSignature(AbstractSignature): yield param_name for param_name in kwarg_params: - for param_names in _iter_nodes_for_param(param_name, star_count=2): + 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) diff --git a/test/test_evaluate/test_signature.py b/test/test_evaluate/test_signature.py index c6589b57..0a2c6235 100644 --- a/test/test_evaluate/test_signature.py +++ b/test/test_evaluate/test_signature.py @@ -109,6 +109,12 @@ def test_tree_signature(Script, environment, code, expected): 'x, /, *, y'), ('combined_redirect(combined_redirect(simple4, simple2), combined_redirect(simple5, kw))', 'a, b, x: int, /, *, a, b, c, **kwargs'), + ('combined_redirect(combined_redirect(a, kw), combined_redirect(kw, simple5))', + 'a, b, /, *args, y'), + + ('no_redirect(kw)', '*args, **kwargs'), + ('no_redirect(akw)', '*args, **kwargs'), + ('no_redirect(simple)', '*args, **kwargs'), ] ) def test_nested_signatures(Script, environment, combination, expected): From b4f2d82867d1319a9f3398588c01f0a8c0103831 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 28 Jul 2019 17:31:17 +0200 Subject: [PATCH 06/14] A new approach of getting arguments --- jedi/evaluate/arguments.py | 6 + jedi/evaluate/names.py | 9 ++ jedi/evaluate/signature.py | 185 ++++++++++++++++----------- test/test_evaluate/test_signature.py | 2 + 4 files changed, 125 insertions(+), 77 deletions(-) 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'), From fae2c8c060a7650bf3ad537cf993b92c2d04e9a1 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 28 Jul 2019 17:41:28 +0200 Subject: [PATCH 07/14] Move args resolving to a different file --- jedi/evaluate/signature.py | 170 +--------------------------------- jedi/evaluate/star_args.py | 181 +++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 168 deletions(-) create mode 100644 jedi/evaluate/star_args.py diff --git a/jedi/evaluate/signature.py b/jedi/evaluate/signature.py index b9223fef..b85d9e06 100644 --- a/jedi/evaluate/signature.py +++ b/jedi/evaluate/signature.py @@ -1,6 +1,4 @@ from jedi._compatibility import Parameter -from jedi.evaluate.utils import to_list -from jedi.evaluate.names import ParamNameWrapper class _SignatureMixin(object): @@ -80,172 +78,8 @@ class TreeSignature(AbstractSignature): return a.get_code(include_prefix=False) def get_param_names(self): - return _process_params(super(TreeSignature, self).get_param_names()) - - -def _iter_nodes_for_param(param_name): - 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] == '*' * param_name.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: # 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 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() - for key, _ in arguments.unpack(): - if key is None: - count += 1 - else: - used_keys.add(key) - - for p in param_names: - if count and p.maybe_positional_argument(): - count -= 1 - continue - if p.string_name in used_keys and p.maybe_keyword_argument(): - continue - yield p - - -def _process_params(param_names, star_count=3): # default means both * and ** - used_names = set() - kw_only_params = [] - arg_funcs = [] - kwarg_funcs = [] - kwarg_names = [] - longest_param_names = () - for p in param_names: - 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 - 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: - yield ParamNameFixedKind(p, Parameter.KEYWORD_ONLY) - 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): - super(ParamNameFixedKind, self).__init__(param_name) - self._new_kind = new_kind - - def get_kind(self): - return self._new_kind + from jedi.evaluate.star_args import process_params + return process_params(super(TreeSignature, self).get_param_names()) class BuiltinSignature(AbstractSignature): diff --git a/jedi/evaluate/star_args.py b/jedi/evaluate/star_args.py new file mode 100644 index 00000000..c5a603e5 --- /dev/null +++ b/jedi/evaluate/star_args.py @@ -0,0 +1,181 @@ +""" +This module is responsible for evaluating *args and **kwargs for signatures. + +This means for example in this case:: + + def foo(a, b, c): ... + + def bar(*args): + return foo(1, *args) + +The signature here for bar should be `bar(b, c)` instead of bar(*args). +""" + +from jedi._compatibility import Parameter +from jedi.evaluate.names import ParamNameWrapper + + +def _iter_nodes_for_param(param_name): + 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] == '*' * param_name.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: # 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 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() + for key, _ in arguments.unpack(): + if key is None: + count += 1 + else: + used_keys.add(key) + + for p in param_names: + if count and p.maybe_positional_argument(): + count -= 1 + continue + if p.string_name in used_keys and p.maybe_keyword_argument(): + continue + yield p + + +def process_params(param_names, star_count=3): # default means both * and ** + used_names = set() + kw_only_params = [] + arg_funcs = [] + kwarg_funcs = [] + kwarg_names = [] + longest_param_names = () + for p in param_names: + 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 + 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: + yield ParamNameFixedKind(p, Parameter.KEYWORD_ONLY) + 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): + super(ParamNameFixedKind, self).__init__(param_name) + self._new_kind = new_kind + + def get_kind(self): + return self._new_kind + From 97e7f608dfc0c39f74bf7007bc05b0c22f2a4ba0 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 28 Jul 2019 17:51:40 +0200 Subject: [PATCH 08/14] Don't return multiple used names for signatures --- jedi/evaluate/star_args.py | 10 +++++++--- test/test_evaluate/test_signature.py | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/jedi/evaluate/star_args.py b/jedi/evaluate/star_args.py index c5a603e5..3bebb2e9 100644 --- a/jedi/evaluate/star_args.py +++ b/jedi/evaluate/star_args.py @@ -121,8 +121,9 @@ def process_params(param_names, star_count=3): # default means both * and ** if star_count == 1: yield ParamNameFixedKind(p, Parameter.POSITIONAL_ONLY) elif star_count == 2: - yield ParamNameFixedKind(p, Parameter.KEYWORD_ONLY) + kw_only_params.append(ParamNameFixedKind(p, Parameter.KEYWORD_ONLY)) else: + used_names.add(p.string_name) yield p for func_and_argument in arg_funcs: @@ -152,10 +153,15 @@ def process_params(param_names, star_count=3): # default means both * and ** if star_count == 1 and p.get_kind() != Parameter.VAR_POSITIONAL: yield ParamNameFixedKind(p, Parameter.POSITIONAL_ONLY) else: + if p.get_kind() == Parameter.POSITIONAL_OR_KEYWORD: + used_names.add(p.string_name) yield p for p in kw_only_params: + if p.string_name in used_names: + continue yield p + used_names.add(p.string_name) for func, arguments in kwarg_funcs: for p in process_params( @@ -168,7 +174,6 @@ def process_params(param_names, star_count=3): # default means both * and ** if kwarg_names: yield kwarg_names[0] - return class ParamNameFixedKind(ParamNameWrapper): @@ -178,4 +183,3 @@ class ParamNameFixedKind(ParamNameWrapper): def get_kind(self): return self._new_kind - diff --git a/test/test_evaluate/test_signature.py b/test/test_evaluate/test_signature.py index 2bb81126..e0d1bec6 100644 --- a/test/test_evaluate/test_signature.py +++ b/test/test_evaluate/test_signature.py @@ -96,6 +96,8 @@ def test_tree_signature(Script, environment, code, expected): 'combination, expected', [ ('full_redirect(simple)', 'b, *, c'), + ('two_redirects(simple, simple)', 'a, 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'), @@ -134,6 +136,8 @@ def test_nested_signatures(Script, environment, combination, expected): return lambda *args, **kwargs: func(1) def full_redirect(func): return lambda *args, **kwargs: func(1, *args, **kwargs) + def two_redirects(func1, func2): + return lambda *args, **kwargs: func1(*args, **kwargs) + func2(1, *args, **kwargs) def combined_redirect(func1, func2): return lambda *args, **kwargs: func1(*args) + func2(**kwargs) def combined_lot_of_args(func1, func2): From c8588191f9c1a47cb888f2e58a727c65d09070ae Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 28 Jul 2019 18:09:08 +0200 Subject: [PATCH 09/14] Some more small fixes --- jedi/evaluate/star_args.py | 21 ++++++++++++++------- test/test_evaluate/test_signature.py | 5 ++++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/jedi/evaluate/star_args.py b/jedi/evaluate/star_args.py index 3bebb2e9..4f688885 100644 --- a/jedi/evaluate/star_args.py +++ b/jedi/evaluate/star_args.py @@ -94,18 +94,19 @@ def _remove_given_params(arguments, param_names): def process_params(param_names, star_count=3): # default means both * and ** used_names = set() - kw_only_params = [] arg_funcs = [] kwarg_funcs = [] + + kw_only_names = [] kwarg_names = [] - longest_param_names = () + arg_names = [] for p in param_names: 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 + arg_names.append(p) elif p.get_kind() == Parameter.VAR_KEYWORD: if star_count & 2: kwarg_funcs = list(_iter_nodes_for_param(p)) @@ -113,7 +114,7 @@ def process_params(param_names, star_count=3): # default means both * and ** kwarg_names.append(p) elif kind == Parameter.KEYWORD_ONLY: if star_count & 2: - kw_only_params.append(p) + kw_only_names.append(p) elif kind == Parameter.POSITIONAL_ONLY: if star_count & 1: yield p @@ -121,11 +122,12 @@ def process_params(param_names, star_count=3): # default means both * and ** if star_count == 1: yield ParamNameFixedKind(p, Parameter.POSITIONAL_ONLY) elif star_count == 2: - kw_only_params.append(ParamNameFixedKind(p, Parameter.KEYWORD_ONLY)) + kw_only_names.append(ParamNameFixedKind(p, Parameter.KEYWORD_ONLY)) else: used_names.add(p.string_name) yield p + longest_param_names = () for func_and_argument in arg_funcs: func, arguments = func_and_argument new_star_count = star_count @@ -142,8 +144,10 @@ def process_params(param_names, star_count=3): # default means both * and ** )), new_star_count): if p.get_kind() == Parameter.VAR_KEYWORD: kwarg_names.append(p) + elif p.get_kind() == Parameter.VAR_POSITIONAL: + arg_names.append(p) elif p.get_kind() == Parameter.KEYWORD_ONLY: - kw_only_params.append(p) + kw_only_names.append(p) else: args_for_this_func.append(p) if len(args_for_this_func) > len(longest_param_names): @@ -157,7 +161,10 @@ def process_params(param_names, star_count=3): # default means both * and ** used_names.add(p.string_name) yield p - for p in kw_only_params: + if arg_names: + yield arg_names[0] + + for p in kw_only_names: if p.string_name in used_names: continue yield p diff --git a/test/test_evaluate/test_signature.py b/test/test_evaluate/test_signature.py index e0d1bec6..eec334d2 100644 --- a/test/test_evaluate/test_signature.py +++ b/test/test_evaluate/test_signature.py @@ -97,6 +97,9 @@ def test_tree_signature(Script, environment, code, expected): ('full_redirect(simple)', 'b, *, c'), ('two_redirects(simple, simple)', 'a, b, *, c'), + ('two_redirects(simple2, simple2)', 'x'), + ('two_redirects(akw, kw)', 'a, c, *args, **kwargs'), + ('two_redirects(kw, akw)', 'a, b, *args, c, **kwargs'), ('combined_redirect(simple, simple2)', 'a, b, /, *, x'), ('combined_redirect(simple, simple3)', 'a, b, /, *, a, x: int'), @@ -130,7 +133,7 @@ def test_nested_signatures(Script, environment, combination, expected): def simple5(y): ... def a(a, b, *args): ... def kw(a, b, *, c, **kwargs): ... - def akw(a, b, *args, **kwargs): ... + def akw(a, c, *args, **kwargs): ... def no_redirect(func): return lambda *args, **kwargs: func(1) From e8e3e8c1118e358eca8706907e08e236e5f524d0 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 28 Jul 2019 19:52:48 +0200 Subject: [PATCH 10/14] Deal better with non-functions --- jedi/evaluate/signature.py | 11 +++-- jedi/evaluate/star_args.py | 68 ++++++++++++++-------------- test/test_evaluate/test_signature.py | 17 +++++++ 3 files changed, 59 insertions(+), 37 deletions(-) diff --git a/jedi/evaluate/signature.py b/jedi/evaluate/signature.py index b85d9e06..20aa4c65 100644 --- a/jedi/evaluate/signature.py +++ b/jedi/evaluate/signature.py @@ -30,7 +30,7 @@ class _SignatureMixin(object): s += ' -> ' + annotation return s - def get_param_names(self): + def get_param_names(self, resolve_stars=True): param_names = self._function_context.get_param_names() if self.is_bound: return param_names[1:] @@ -77,9 +77,12 @@ class TreeSignature(AbstractSignature): return '' return a.get_code(include_prefix=False) - def get_param_names(self): - from jedi.evaluate.star_args import process_params - return process_params(super(TreeSignature, self).get_param_names()) + def get_param_names(self, resolve_stars=True): + params = super(TreeSignature, self).get_param_names() + if resolve_stars: + from jedi.evaluate.star_args import process_params + params = process_params(params) + return params class BuiltinSignature(AbstractSignature): diff --git a/jedi/evaluate/star_args.py b/jedi/evaluate/star_args.py index 4f688885..05709d83 100644 --- a/jedi/evaluate/star_args.py +++ b/jedi/evaluate/star_args.py @@ -94,8 +94,8 @@ def _remove_given_params(arguments, param_names): def process_params(param_names, star_count=3): # default means both * and ** used_names = set() - arg_funcs = [] - kwarg_funcs = [] + arg_callables = [] + kwarg_callables = [] kw_only_names = [] kwarg_names = [] @@ -104,13 +104,13 @@ def process_params(param_names, star_count=3): # default means both * and ** 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: + arg_callables = list(_iter_nodes_for_param(p)) + if not arg_callables: arg_names.append(p) elif p.get_kind() == Parameter.VAR_KEYWORD: if star_count & 2: - kwarg_funcs = list(_iter_nodes_for_param(p)) - if not kwarg_funcs: + kwarg_callables = list(_iter_nodes_for_param(p)) + if not kwarg_callables: kwarg_names.append(p) elif kind == Parameter.KEYWORD_ONLY: if star_count & 2: @@ -128,30 +128,31 @@ def process_params(param_names, star_count=3): # default means both * and ** yield p longest_param_names = () - for func_and_argument in arg_funcs: + for func_and_argument in arg_callables: func, arguments = func_and_argument new_star_count = star_count - if func_and_argument in kwarg_funcs: - kwarg_funcs.remove(func_and_argument) + if func_and_argument in kwarg_callables: + kwarg_callables.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.VAR_POSITIONAL: - arg_names.append(p) - elif p.get_kind() == Parameter.KEYWORD_ONLY: - kw_only_names.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 signature in func.get_signatures(): + args_for_this_func = [] + for p in process_params( + list(_remove_given_params( + arguments, + signature.get_param_names(resolve_stars=False) + )), new_star_count): + if p.get_kind() == Parameter.VAR_KEYWORD: + kwarg_names.append(p) + elif p.get_kind() == Parameter.VAR_POSITIONAL: + arg_names.append(p) + elif p.get_kind() == Parameter.KEYWORD_ONLY: + kw_only_names.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: @@ -170,14 +171,15 @@ def process_params(param_names, star_count=3): # default means both * and ** yield p used_names.add(p.string_name) - 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 + for func, arguments in kwarg_callables: + for signature in func.get_signatures(): + for p in process_params( + list(_remove_given_params( + arguments, + signature.get_param_names(resolve_stars=False) + )), star_count=2): + if p.get_kind() != Parameter.KEYWORD_ONLY or not kwarg_names: + yield p if kwarg_names: yield kwarg_names[0] diff --git a/test/test_evaluate/test_signature.py b/test/test_evaluate/test_signature.py index eec334d2..101bb6d3 100644 --- a/test/test_evaluate/test_signature.py +++ b/test/test_evaluate/test_signature.py @@ -94,8 +94,21 @@ def test_tree_signature(Script, environment, code, expected): @pytest.mark.parametrize( 'combination, expected', [ + # Functions ('full_redirect(simple)', 'b, *, c'), + ('full_redirect(simple4)', 'b, x: int'), + ('full_redirect(a)', 'b, *args'), + ('full_redirect(kw)', 'b, *, c, **kwargs'), + ('full_redirect(akw)', 'c, *args, **kwargs'), + # Non functions + ('full_redirect(lambda x, y: ...)', 'y'), + ('full_redirect(C)', 'z, *c'), + ('full_redirect(C())', 'y'), + ('full_redirect()', '*args, **kwargs'), + ('full_redirect(1)', '*args, **kwargs'), + + # Merging ('two_redirects(simple, simple)', 'a, b, *, c'), ('two_redirects(simple2, simple2)', 'x'), ('two_redirects(akw, kw)', 'a, c, *args, **kwargs'), @@ -145,6 +158,10 @@ def test_nested_signatures(Script, environment, combination, expected): return lambda *args, **kwargs: func1(*args) + func2(**kwargs) def combined_lot_of_args(func1, func2): return lambda *args, **kwargs: func1(1, 2, 3, 4, *args) + func2(a=3, x=1, y=1, **kwargs) + + class C: + def __init__(self, a, z, *c): ... + def __call__(self, x, y): ... ''') code += 'z = ' + combination + '\nz(' sig, = Script(code).call_signatures() From 02bd7e5bc70e1ae67ec3205ad158a427aa37dd3d Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 28 Jul 2019 20:22:28 +0200 Subject: [PATCH 11/14] Some small args adaptions --- jedi/evaluate/star_args.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/jedi/evaluate/star_args.py b/jedi/evaluate/star_args.py index 05709d83..4fc6cc8e 100644 --- a/jedi/evaluate/star_args.py +++ b/jedi/evaluate/star_args.py @@ -100,18 +100,18 @@ def process_params(param_names, star_count=3): # default means both * and ** kw_only_names = [] kwarg_names = [] arg_names = [] + original_arg_name = None + original_kwarg_name = None for p in param_names: kind = p.get_kind() if kind == Parameter.VAR_POSITIONAL: if star_count & 1: - arg_callables = list(_iter_nodes_for_param(p)) - if not arg_callables: - arg_names.append(p) + arg_callables = _iter_nodes_for_param(p) + original_arg_name = p elif p.get_kind() == Parameter.VAR_KEYWORD: if star_count & 2: kwarg_callables = list(_iter_nodes_for_param(p)) - if not kwarg_callables: - kwarg_names.append(p) + original_kwarg_name = p elif kind == Parameter.KEYWORD_ONLY: if star_count & 2: kw_only_names.append(p) @@ -128,6 +128,8 @@ def process_params(param_names, star_count=3): # default means both * and ** yield p longest_param_names = () + found_arg_signature = False + found_kwarg_signature = False for func_and_argument in arg_callables: func, arguments = func_and_argument new_star_count = star_count @@ -137,6 +139,9 @@ def process_params(param_names, star_count=3): # default means both * and ** new_star_count = 1 for signature in func.get_signatures(): + found_arg_signature = True + if new_star_count == 3: + found_kwarg_signature = True args_for_this_func = [] for p in process_params( list(_remove_given_params( @@ -162,7 +167,9 @@ def process_params(param_names, star_count=3): # default means both * and ** used_names.add(p.string_name) yield p - if arg_names: + if not found_arg_signature and original_arg_name is not None: + yield original_arg_name + elif arg_names: yield arg_names[0] for p in kw_only_names: @@ -173,6 +180,7 @@ def process_params(param_names, star_count=3): # default means both * and ** for func, arguments in kwarg_callables: for signature in func.get_signatures(): + found_kwarg_signature = True for p in process_params( list(_remove_given_params( arguments, @@ -181,7 +189,9 @@ def process_params(param_names, star_count=3): # default means both * and ** if p.get_kind() != Parameter.KEYWORD_ONLY or not kwarg_names: yield p - if kwarg_names: + if not found_kwarg_signature and original_kwarg_name is not None: + yield original_kwarg_name + elif kwarg_names: yield kwarg_names[0] From f6808a96e0294c5eaa051d0ae33916ac56193761 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Sun, 28 Jul 2019 20:40:32 +0200 Subject: [PATCH 12/14] Skip pre python 3.5 --- conftest.py | 8 ++++++++ test/test_evaluate/test_signature.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/conftest.py b/conftest.py index 3c1b1d79..765c865c 100644 --- a/conftest.py +++ b/conftest.py @@ -148,3 +148,11 @@ def skip_pre_python37(environment): # This if is just needed to avoid that tests ever skip way more than # they should for all Python versions. pytest.skip() + + +@pytest.fixture() +def skip_pre_python35(environment): + if environment.version_info < (3, 5): + # This if is just needed to avoid that tests ever skip way more than + # they should for all Python versions. + pytest.skip() diff --git a/test/test_evaluate/test_signature.py b/test/test_evaluate/test_signature.py index 101bb6d3..49a17dd3 100644 --- a/test/test_evaluate/test_signature.py +++ b/test/test_evaluate/test_signature.py @@ -137,7 +137,7 @@ def test_tree_signature(Script, environment, code, expected): ('no_redirect(simple)', '*args, **kwargs'), ] ) -def test_nested_signatures(Script, environment, combination, expected): +def test_nested_signatures(Script, environment, combination, expected, skip_pre_python35): code = dedent(''' def simple(a, b, *, c): ... def simple2(x): ... From fa0424cfd682cda10ece5ce4c180d96c1bcdac6c Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 29 Jul 2019 00:12:58 +0200 Subject: [PATCH 13/14] Fix signatures for wraps, see #1058 --- jedi/evaluate/signature.py | 12 ++++----- jedi/plugins/stdlib.py | 40 +++++++++++++++++++++++++++- test/test_evaluate/test_signature.py | 35 ++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 7 deletions(-) diff --git a/jedi/evaluate/signature.py b/jedi/evaluate/signature.py index 20aa4c65..1b4d60dc 100644 --- a/jedi/evaluate/signature.py +++ b/jedi/evaluate/signature.py @@ -30,12 +30,6 @@ class _SignatureMixin(object): s += ' -> ' + annotation return s - def get_param_names(self, resolve_stars=True): - param_names = self._function_context.get_param_names() - if self.is_bound: - return param_names[1:] - return param_names - class AbstractSignature(_SignatureMixin): def __init__(self, context, is_bound=False): @@ -50,6 +44,12 @@ class AbstractSignature(_SignatureMixin): def annotation_string(self): return '' + def get_param_names(self, resolve_stars=True): + param_names = self._function_context.get_param_names() + if self.is_bound: + return param_names[1:] + return param_names + def bind(self, context): raise NotImplementedError diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index fc138cd2..0d25c394 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -642,6 +642,43 @@ class ItemGetterCallable(ContextWrapper): return context_set +@argument_clinic('func, /') +def _functools_wraps(funcs): + return ContextSet(WrapsCallable(func) for func in funcs) + + +class WrapsCallable(ContextWrapper): + @repack_with_argument_clinic('func, /') + def py__call__(self, funcs): + return ContextSet({Wrapped(func, self._wrapped_context) for func in funcs}) + + +class Wrapped(ContextWrapper): + def __init__(self, func, original_function): + super(Wrapped, self).__init__(func) + self._original_function = original_function + + @property + def name(self): + return self._original_function.name + + def get_signatures(self): + return [ + ReplacedNameSignature(sig, self._original_function.name) + for sig in self._wrapped_context.get_signatures() + ] + + +class ReplacedNameSignature(SignatureWrapper): + def __init__(self, signature, name): + super(ReplacedNameSignature, self).__init__(signature) + self._name = name + + @property + def name(self): + return self._name + + @argument_clinic('*args, /', want_obj=True, want_arguments=True) def _operator_itemgetter(args_context_set, obj, arguments): return ContextSet([ @@ -675,7 +712,8 @@ _implemented = { }, 'functools': { 'partial': functools_partial, - 'wraps': _return_first_param, + 'wraps': _functools_wraps, + #'wraps': _return_first_param, }, '_weakref': { 'proxy': _return_first_param, diff --git a/test/test_evaluate/test_signature.py b/test/test_evaluate/test_signature.py index 49a17dd3..5a4e2423 100644 --- a/test/test_evaluate/test_signature.py +++ b/test/test_evaluate/test_signature.py @@ -179,6 +179,41 @@ def test_pow_signature(Script): 'pow(x: int, y: int, /) -> Any'} +@pytest.mark.parametrize( + 'code, signature', [ + [dedent(''' + import functools + def f(x): + pass + def x(f): + @functools.wraps(f) + def wrapper(*args): + # Have no arguments here, but because of wraps, the signature + # should still be f's. + return f(*args) + return wrapper + + x(f)('''), 'f(x, /)'], + [dedent(''' + import functools + def f(x): + pass + def x(f): + @functools.wraps(f) + def wrapper(): + # Have no arguments here, but because of wraps, the signature + # should still be f's. + return 1 + return wrapper + + x(f)('''), 'f()'], + ] +) +def test_wraps_signature(Script, code, signature, skip_pre_python35): + sigs = Script(code).call_signatures() + assert {sig._signature.to_string() for sig in sigs} == {signature} + + @pytest.mark.parametrize( 'start, start_params', [ ['@dataclass\nclass X:', []], From 9b338f69a621fbc92a21b67157b0d615a7ae1ab7 Mon Sep 17 00:00:00 2001 From: Dave Halter Date: Mon, 29 Jul 2019 00:28:12 +0200 Subject: [PATCH 14/14] Add a comment about wraps --- jedi/plugins/stdlib.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jedi/plugins/stdlib.py b/jedi/plugins/stdlib.py index 0d25c394..d76edcaf 100644 --- a/jedi/plugins/stdlib.py +++ b/jedi/plugins/stdlib.py @@ -648,6 +648,9 @@ def _functools_wraps(funcs): class WrapsCallable(ContextWrapper): + # XXX this is not the correct wrapped context, it should be a weird + # partials object, but it doesn't matter, because it's always used as a + # decorator anyway. @repack_with_argument_clinic('func, /') def py__call__(self, funcs): return ContextSet({Wrapped(func, self._wrapped_context) for func in funcs})