diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index b582cc75..4e737a33 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -8,6 +8,7 @@ debug messages to stdout, simply call :func:`set_debug_function` without arguments. """ from pathlib import Path +from typing import Any import parso from parso.python import tree @@ -338,7 +339,7 @@ class Script: fuzzy=fuzzy, ) - def complete_search(self, string, **kwargs): + def complete_search(self, string, **kwargs) -> list[Any]: """ Like :meth:`.Script.search`, but completes that string. If you want to have all possible definitions in a file you can also provide an empty diff --git a/jedi/inference/star_args.py b/jedi/inference/star_args.py index 0d8a132b..2a9dd6b1 100644 --- a/jedi/inference/star_args.py +++ b/jedi/inference/star_args.py @@ -11,9 +11,10 @@ This means for example in this case:: The signature here for bar should be `bar(b, c)` instead of bar(*args). """ from inspect import Parameter +from typing import Sequence from jedi.inference.utils import to_list -from jedi.inference.names import ParamNameWrapper +from jedi.inference.names import ParamNameWrapper, AnonymousParamName from jedi.inference.helpers import is_big_annoying_library @@ -94,7 +95,11 @@ def _remove_given_params(arguments, param_names): @to_list -def process_params(param_names, star_count=3): # default means both * and ** +def process_params(param_names: Sequence[AnonymousParamName | ParamNameFixedKind], star_count=3, already_used=None): + # start_count=3 means both * and ** + if already_used is None: + already_used = set() + if param_names: if is_big_annoying_library(param_names[0].parent_context): # At first this feature can look innocent, but it does a lot of @@ -112,14 +117,22 @@ def process_params(param_names, star_count=3): # default means both * and ** original_arg_name = None original_kwarg_name = None for p in param_names: + def is_recursion(): + is_recursion = p.tree_name in already_used + if not is_recursion: + already_used.add(p.tree_name) + return is_recursion + kind = p.get_kind() if kind == Parameter.VAR_POSITIONAL: if star_count & 1: - arg_callables = _iter_nodes_for_param(p) + if not is_recursion(): + 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 is_recursion(): + kwarg_callables = list(_iter_nodes_for_param(p)) original_kwarg_name = p elif kind == Parameter.KEYWORD_ONLY: if star_count & 2: @@ -157,7 +170,7 @@ def process_params(param_names, star_count=3): # default means both * and ** list(_remove_given_params( arguments, signature.get_param_names(resolve_stars=False) - )), new_star_count): + )), new_star_count, already_used): if p.get_kind() == Parameter.VAR_KEYWORD: kwarg_names.append(p) elif p.get_kind() == Parameter.VAR_POSITIONAL: @@ -190,7 +203,7 @@ def process_params(param_names, star_count=3): # default means both * and ** list(_remove_given_params( arguments, signature.get_param_names(resolve_stars=False) - )), star_count=2): + )), star_count=2, already_used=already_used): if p.get_kind() == Parameter.VAR_KEYWORD: kwarg_names.append(p) elif p.get_kind() == Parameter.KEYWORD_ONLY: diff --git a/jedi/inference/utils.py b/jedi/inference/utils.py index 2f8d41f1..161f0ba8 100644 --- a/jedi/inference/utils.py +++ b/jedi/inference/utils.py @@ -2,6 +2,7 @@ import functools import re import os +from typing import ParamSpec, TypeVar, Callable, Iterable _sep = os.path.sep @@ -10,8 +11,11 @@ if os.path.altsep is not None: _path_re = re.compile(r'(?:\.[^{0}]+|[{0}]__init__\.py)$'.format(re.escape(_sep))) del _sep +P = ParamSpec("P") +T = TypeVar("T") -def to_list(func): + +def to_list(func: Callable[P, Iterable[T]]) -> Callable[P, list[T]]: def wrapper(*args, **kwargs): return list(func(*args, **kwargs)) return wrapper diff --git a/test/test_api/test_signatures.py b/test/test_api/test_signatures.py index bf254185..e532baaf 100644 --- a/test/test_api/test_signatures.py +++ b/test/test_api/test_signatures.py @@ -1,3 +1,5 @@ +from textwrap import dedent + import pytest _tuple_code = 'from typing import Tuple\ndef f(x: Tuple[int]): ...\nf' @@ -69,3 +71,28 @@ def test_param_kind_and_name(code, index, param_code, kind, Script): def test_staticmethod(Script): s, = Script('staticmethod(').get_signatures() assert s.to_string() == 'staticmethod(f: Callable[_P, _R_co], /)' + + +def test_recursive_star_kwargs_do_not_recurse(Script): + # From #2085 + code = dedent(''' + def test(x, **kwargs): + if isinstance(x, int): + return test((x,), **kwargs) + test( + ''') + script = Script(code) + script.complete() + signature, = script.get_signatures() + assert signature.to_string() == 'test(x, **kwargs)' + + code = dedent(''' + def test(x, *args, **kwargs): + if isinstance(x, int): + return test((x,), *args, **kwargs) + test( + ''') + script = Script(code) + script.complete() + signature, = script.get_signatures() + assert signature.to_string() == 'test(x, *args, **kwargs)'