mirror of
https://github.com/davidhalter/jedi.git
synced 2025-12-06 14:04:26 +08:00
Merge the signature changes
Fixes include - Better @wraps(func) understanding - *args, **kwargs in call signatures is now resolved as well as possible Fixes #503, fixes #1058 Also look at #906, #634, #1163
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -173,6 +187,15 @@ class ParamNameInterface(object):
|
||||
# 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
|
||||
@@ -247,6 +270,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
|
||||
|
||||
@@ -30,12 +30,6 @@ class _SignatureMixin(object):
|
||||
s += ' -> ' + annotation
|
||||
return s
|
||||
|
||||
def get_param_names(self):
|
||||
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
|
||||
|
||||
@@ -77,6 +77,13 @@ class TreeSignature(AbstractSignature):
|
||||
return ''
|
||||
return a.get_code(include_prefix=False)
|
||||
|
||||
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):
|
||||
def __init__(self, context, return_string, is_bound=False):
|
||||
|
||||
204
jedi/evaluate/star_args.py
Normal file
204
jedi/evaluate/star_args.py
Normal file
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
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()
|
||||
arg_callables = []
|
||||
kwarg_callables = []
|
||||
|
||||
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 = _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))
|
||||
original_kwarg_name = p
|
||||
elif kind == Parameter.KEYWORD_ONLY:
|
||||
if star_count & 2:
|
||||
kw_only_names.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:
|
||||
kw_only_names.append(ParamNameFixedKind(p, Parameter.KEYWORD_ONLY))
|
||||
else:
|
||||
used_names.add(p.string_name)
|
||||
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
|
||||
if func_and_argument in kwarg_callables:
|
||||
kwarg_callables.remove(func_and_argument)
|
||||
else:
|
||||
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(
|
||||
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:
|
||||
yield ParamNameFixedKind(p, Parameter.POSITIONAL_ONLY)
|
||||
else:
|
||||
if p.get_kind() == Parameter.POSITIONAL_OR_KEYWORD:
|
||||
used_names.add(p.string_name)
|
||||
yield p
|
||||
|
||||
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:
|
||||
if p.string_name in used_names:
|
||||
continue
|
||||
yield p
|
||||
used_names.add(p.string_name)
|
||||
|
||||
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,
|
||||
signature.get_param_names(resolve_stars=False)
|
||||
)), star_count=2):
|
||||
if p.get_kind() != Parameter.KEYWORD_ONLY or not kwarg_names:
|
||||
yield p
|
||||
|
||||
if not found_kwarg_signature and original_kwarg_name is not None:
|
||||
yield original_kwarg_name
|
||||
elif kwarg_names:
|
||||
yield kwarg_names[0]
|
||||
|
||||
|
||||
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
|
||||
@@ -642,6 +642,46 @@ class ItemGetterCallable(ContextWrapper):
|
||||
return context_set
|
||||
|
||||
|
||||
@argument_clinic('func, /')
|
||||
def _functools_wraps(funcs):
|
||||
return ContextSet(WrapsCallable(func) for func in 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})
|
||||
|
||||
|
||||
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 +715,8 @@ _implemented = {
|
||||
},
|
||||
'functools': {
|
||||
'partial': functools_partial,
|
||||
'wraps': _return_first_param,
|
||||
'wraps': _functools_wraps,
|
||||
#'wraps': _return_first_param,
|
||||
},
|
||||
'_weakref': {
|
||||
'proxy': _return_first_param,
|
||||
|
||||
@@ -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,83 @@ def test_tree_signature(Script, environment, code, expected):
|
||||
assert expected == sig._signature.to_string()
|
||||
|
||||
|
||||
@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'),
|
||||
('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'),
|
||||
('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'),
|
||||
|
||||
('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'),
|
||||
('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, skip_pre_python35):
|
||||
code = dedent('''
|
||||
def simple(a, b, *, c): ...
|
||||
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, c, *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 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):
|
||||
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()
|
||||
computed = sig._signature.to_string()
|
||||
assert '<lambda>(' + expected + ')' == computed
|
||||
|
||||
|
||||
def test_pow_signature(Script):
|
||||
# See github #1357
|
||||
sigs = Script('pow(').call_signatures()
|
||||
@@ -101,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:', []],
|
||||
|
||||
Reference in New Issue
Block a user