303 lines
11 KiB
Python
303 lines
11 KiB
Python
import ast
|
|
from typing import Tuple, List
|
|
|
|
from rope.base import pyobjects, worder
|
|
from rope.base.builtins import Lambda
|
|
from rope.base.codeanalyze import SourceLinesAdapter
|
|
|
|
|
|
class DefinitionInfo:
|
|
def __init__(
|
|
self, function_name, is_method, args_with_defaults, args_arg, keywords_arg
|
|
):
|
|
self.function_name = function_name
|
|
self.is_method = is_method
|
|
self.args_with_defaults = args_with_defaults
|
|
self.args_arg = args_arg
|
|
self.keywords_arg = keywords_arg
|
|
|
|
def to_string(self):
|
|
return f"{self.function_name}({self.arguments_to_string()})"
|
|
|
|
def arguments_to_string(self, from_index=0):
|
|
params = []
|
|
for arg, default in self.args_with_defaults:
|
|
if default is not None:
|
|
params.append(f"{arg}={default}")
|
|
else:
|
|
params.append(arg)
|
|
if self.args_arg is not None:
|
|
params.append("*" + self.args_arg)
|
|
if self.keywords_arg:
|
|
params.append("**" + self.keywords_arg)
|
|
return ", ".join(params[from_index:])
|
|
|
|
@staticmethod
|
|
def _read(pyfunction, code):
|
|
kind = pyfunction.get_kind()
|
|
is_method = kind == "method"
|
|
is_lambda = kind == "lambda"
|
|
info = _FunctionDefParser(code, is_method, is_lambda)
|
|
args, keywords = info.get_parameters()
|
|
args_arg = None
|
|
keywords_arg = None
|
|
if args and args[-1].startswith("**"):
|
|
keywords_arg = args[-1][2:]
|
|
del args[-1]
|
|
if args and args[-1].startswith("*"):
|
|
args_arg = args[-1][1:]
|
|
del args[-1]
|
|
args_with_defaults = [(name, None) for name in args]
|
|
args_with_defaults.extend(keywords)
|
|
return DefinitionInfo(
|
|
info.get_function_name(),
|
|
is_method,
|
|
args_with_defaults,
|
|
args_arg,
|
|
keywords_arg,
|
|
)
|
|
|
|
@staticmethod
|
|
def read(pyfunction):
|
|
pymodule = pyfunction.get_module()
|
|
word_finder = worder.Worder(pymodule.source_code)
|
|
lineno = pyfunction.get_ast().lineno
|
|
start = pymodule.lines.get_line_start(lineno)
|
|
if isinstance(pyfunction, Lambda):
|
|
call = word_finder.get_lambda_and_args(start)
|
|
else:
|
|
call = word_finder.get_function_and_args_in_header(start)
|
|
return DefinitionInfo._read(pyfunction, call)
|
|
|
|
|
|
class CallInfo:
|
|
def __init__(
|
|
self,
|
|
function_name,
|
|
args,
|
|
keywords,
|
|
args_arg,
|
|
keywords_arg,
|
|
implicit_arg,
|
|
constructor,
|
|
):
|
|
self.function_name = function_name
|
|
self.args = args
|
|
self.keywords = keywords
|
|
self.args_arg = args_arg
|
|
self.keywords_arg = keywords_arg
|
|
self.implicit_arg = implicit_arg
|
|
self.constructor = constructor
|
|
|
|
def to_string(self):
|
|
function = self.function_name
|
|
if self.implicit_arg:
|
|
function = self.args[0] + "." + self.function_name
|
|
params = []
|
|
start = 0
|
|
if self.implicit_arg or self.constructor:
|
|
start = 1
|
|
if self.args[start:]:
|
|
params.extend(self.args[start:])
|
|
if self.keywords:
|
|
params.extend([f"{name}={value}" for name, value in self.keywords])
|
|
if self.args_arg is not None:
|
|
params.append("*" + self.args_arg)
|
|
if self.keywords_arg:
|
|
params.append("**" + self.keywords_arg)
|
|
return "{}({})".format(function, ", ".join(params))
|
|
|
|
@staticmethod
|
|
def read(primary, pyname, definition_info, code):
|
|
is_method_call = CallInfo._is_method_call(primary, pyname)
|
|
is_constructor = CallInfo._is_class(pyname)
|
|
is_classmethod = CallInfo._is_classmethod(pyname)
|
|
info = _FunctionCallParser(code, is_method_call or is_classmethod)
|
|
args, keywords = info.get_parameters()
|
|
args_arg = None
|
|
keywords_arg = None
|
|
if args and args[-1].startswith("**"):
|
|
keywords_arg = args[-1][2:]
|
|
del args[-1]
|
|
if args and args[-1].startswith("*"):
|
|
args_arg = args[-1][1:]
|
|
del args[-1]
|
|
if is_constructor:
|
|
args.insert(0, definition_info.args_with_defaults[0][0])
|
|
return CallInfo(
|
|
info.get_function_name(),
|
|
args,
|
|
keywords,
|
|
args_arg,
|
|
keywords_arg,
|
|
is_method_call or is_classmethod,
|
|
is_constructor,
|
|
)
|
|
|
|
@staticmethod
|
|
def _is_method_call(primary, pyname):
|
|
return (
|
|
primary is not None
|
|
and isinstance(primary.get_object().get_type(), pyobjects.PyClass)
|
|
and CallInfo._is_method(pyname)
|
|
)
|
|
|
|
@staticmethod
|
|
def _is_class(pyname):
|
|
return pyname is not None and isinstance(pyname.get_object(), pyobjects.PyClass)
|
|
|
|
@staticmethod
|
|
def _is_method(pyname):
|
|
if pyname is not None and isinstance(pyname.get_object(), pyobjects.PyFunction):
|
|
return pyname.get_object().get_kind() == "method"
|
|
return False
|
|
|
|
@staticmethod
|
|
def _is_classmethod(pyname):
|
|
if pyname is not None and isinstance(pyname.get_object(), pyobjects.PyFunction):
|
|
return pyname.get_object().get_kind() == "classmethod"
|
|
return False
|
|
|
|
|
|
class ArgumentMapping:
|
|
def __init__(self, definition_info, call_info):
|
|
self.call_info = call_info
|
|
self.param_dict = {}
|
|
self.keyword_args = []
|
|
self.args_arg = []
|
|
for index, value in enumerate(call_info.args):
|
|
if index < len(definition_info.args_with_defaults):
|
|
name = definition_info.args_with_defaults[index][0]
|
|
self.param_dict[name] = value
|
|
else:
|
|
self.args_arg.append(value)
|
|
for name, value in call_info.keywords:
|
|
index = -1
|
|
for pair in definition_info.args_with_defaults:
|
|
if pair[0] == name:
|
|
self.param_dict[name] = value
|
|
break
|
|
else:
|
|
self.keyword_args.append((name, value))
|
|
|
|
def to_call_info(self, definition_info):
|
|
args = []
|
|
keywords = []
|
|
for index in range(len(definition_info.args_with_defaults)):
|
|
name = definition_info.args_with_defaults[index][0]
|
|
if name in self.param_dict:
|
|
args.append(self.param_dict[name])
|
|
else:
|
|
for i in range(index, len(definition_info.args_with_defaults)):
|
|
name = definition_info.args_with_defaults[i][0]
|
|
if name in self.param_dict:
|
|
keywords.append((name, self.param_dict[name]))
|
|
break
|
|
args.extend(self.args_arg)
|
|
keywords.extend(self.keyword_args)
|
|
return CallInfo(
|
|
self.call_info.function_name,
|
|
args,
|
|
keywords,
|
|
self.call_info.args_arg,
|
|
self.call_info.keywords_arg,
|
|
self.call_info.implicit_arg,
|
|
self.call_info.constructor,
|
|
)
|
|
|
|
|
|
class _BaseFunctionParser:
|
|
call: str
|
|
implicit_arg: bool
|
|
|
|
def __init__(self, call: str, implicit_arg: bool, is_lambda: bool = False):
|
|
self.call = call
|
|
self.implicit_arg = implicit_arg
|
|
self.word_finder = worder.Worder(self.call)
|
|
if is_lambda:
|
|
self.last_parens = self.call.rindex(":")
|
|
else:
|
|
self.last_parens = self.call.rindex(")")
|
|
self.first_parens = self.word_finder._find_parens_start(self.last_parens)
|
|
|
|
def get_function_name(self):
|
|
if self.is_called_as_a_method():
|
|
return self.word_finder.get_word_at(self.first_parens - 1)
|
|
else:
|
|
return self.word_finder.get_primary_at(self.first_parens - 1)
|
|
|
|
def is_called_as_a_method(self):
|
|
return self.implicit_arg and "." in self.call[: self.first_parens]
|
|
|
|
def _get_source_range(self, tree):
|
|
start = self._lines.get_line_start(tree.lineno) + tree.col_offset
|
|
end = self._lines.get_line_start(tree.end_lineno) + tree.end_col_offset
|
|
return self._lines.code[start:end]
|
|
|
|
|
|
class _FunctionDefParser(_BaseFunctionParser):
|
|
_lines: SourceLinesAdapter
|
|
|
|
def __init__(self, call, implicit_arg, is_lambda=False):
|
|
super().__init__(call, implicit_arg, is_lambda=False)
|
|
_modified_call = "def " + call.rstrip(":") + ": pass"
|
|
self._lines = SourceLinesAdapter(_modified_call)
|
|
self.ast = ast.parse(_modified_call).body[0]
|
|
assert isinstance(self.ast, ast.FunctionDef)
|
|
|
|
def get_parameters(self) -> Tuple[List[str], List[Tuple[str, str]]]:
|
|
# FIXME: the weird parsing here is because we're replicating what
|
|
# _FunctionParser originally did, which was designed before the
|
|
# existence of posonlyargs and kwonlyargs, we'll want to rewrite this
|
|
# properly to handle them properly at some point
|
|
args = []
|
|
kwargs = []
|
|
args += [arg.arg for arg in self.ast.args.posonlyargs]
|
|
args += [arg.arg for arg in self.ast.args.args]
|
|
if self.ast.args.vararg is not None:
|
|
args += ["*" + self.ast.args.vararg.arg]
|
|
if len(self.ast.args.defaults) > 0:
|
|
defaults = self.ast.args.defaults
|
|
kwargs += [
|
|
(name, self._get_source_range(value))
|
|
for name, value in zip(args[-len(defaults) :], defaults)
|
|
]
|
|
del args[-len(self.ast.args.defaults) :]
|
|
if self.ast.args.kwarg is not None:
|
|
args += ["**" + self.ast.args.kwarg.arg]
|
|
if self.ast.args.kwonlyargs:
|
|
args += ["*"] + [arg.arg for arg in self.ast.args.kwonlyargs]
|
|
if len(self.ast.args.kw_defaults) > 0:
|
|
kw_defaults = self.ast.args.kw_defaults
|
|
kwargs += [
|
|
(name, self._get_source_range(value))
|
|
for name, value in zip(kwargs[-len(kw_defaults) :], kw_defaults)
|
|
]
|
|
del args[-len(self.ast.args.kw_defaults) :]
|
|
return args, kwargs
|
|
|
|
|
|
class _FunctionCallParser(_BaseFunctionParser):
|
|
_lines: SourceLinesAdapter
|
|
ast: ast.Call
|
|
|
|
def __init__(self, call, implicit_arg, is_lambda=False):
|
|
super().__init__(call, implicit_arg, is_lambda=False)
|
|
self._lines = SourceLinesAdapter(call)
|
|
self.ast = ast.parse(call).body[0].value
|
|
assert isinstance(self.ast, ast.Call)
|
|
|
|
def get_parameters(self) -> Tuple[List[str], List[Tuple[str, str]]]:
|
|
args = []
|
|
for arg in self.ast.args:
|
|
arg_value = self._get_source_range(arg)
|
|
args.append(arg_value)
|
|
kwargs = []
|
|
for kw in self.ast.keywords:
|
|
kw_value = self._get_source_range(kw.value)
|
|
assert kw.arg
|
|
kwargs.append((kw.arg, kw_value))
|
|
if self.is_called_as_a_method():
|
|
instance = self.call[: self.call.rindex(".", 0, self.first_parens)]
|
|
args.insert(0, instance.strip())
|
|
return args, kwargs
|