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