diff --git a/jedi/api/__init__.py b/jedi/api/__init__.py index aca670de..bc965f82 100644 --- a/jedi/api/__init__.py +++ b/jedi/api/__init__.py @@ -26,7 +26,7 @@ from jedi.api import classes from jedi.api import interpreter from jedi.api import helpers from jedi.api.helpers import validate_line_column -from jedi.api.completion import Completion +from jedi.api.completion import Completion, complete_trailer from jedi.api.keywords import KeywordName from jedi.api.environment import InterpreterEnvironment from jedi.api.project import get_default_project, Project @@ -45,6 +45,7 @@ from jedi.inference.base_value import ValueSet from jedi.inference.value.iterable import unpack_tuple_to_dict from jedi.inference.gradual.conversion import convert_names, convert_values from jedi.inference.gradual.utils import load_proper_stub_module +from jedi.inference.utils import to_list # Jedi uses lots and lots of recursion. By setting this a little bit higher, we # can remove some "maximum recursion depth" errors. @@ -336,6 +337,43 @@ class Script(object): defs = [classes.Definition(self._inference_state, d) for d in set(names)] return helpers.sorted_definitions(defs) + def search(self, name, **kwargs): + """ + Searches a symbol in the current file. + + :param all_scopes: If True lists the symbols of all scopes instead of + only the module. + :param definitions: If True lists the names that have been defined by a + class, function or a statement (``a = b`` returns ``a``). + :param references: If True lists all the names that are not listed by + ``definitions=True``. E.g. ``a = b`` returns ``b``. + """ + return self._search(name, **kwargs) # Python 2 ... + + @to_list + def _search(self, line, column, name, complete=False, all_scopes=False, + fuzzy=False): + wanted_type, wanted_names = helpers.split_search_string(name) + + names = [d._name for d in self._names(all_scopes=all_scopes)] + for s in wanted_names[:-1]: + new_names = [] + for n in names: + if s == n.string_name: + new_names += complete_trailer( + self._get_module_context(), + n.infer() + ) + names = new_names + + last_name = wanted_names[-1] + for n in names: + if complete and helpers.match(n.string_name, last_name, fuzzy=fuzzy) \ + or not complete and n.string_name == last_name: + def_ = classes.Definition(self._inference_state, n) + if wanted_type is None or wanted_type == def_.api_type: + yield def_ + @validate_line_column def help(self, line=None, column=None): """ diff --git a/jedi/api/completion.py b/jedi/api/completion.py index 86636db1..b80c0e01 100644 --- a/jedi/api/completion.py +++ b/jedi/api/completion.py @@ -357,80 +357,7 @@ class Completion: def _complete_trailer_for_values(self, values): user_context = get_user_context(self._module_context, self._position) - completion_names = [] - for value in values: - for filter in value.get_filters(origin_scope=user_context.tree_node): - completion_names += filter.values() - - if not value.is_stub() and isinstance(value, TreeInstance): - completion_names += self._complete_getattr(value) - - python_values = convert_values(values) - for c in python_values: - if c not in values: - for filter in c.get_filters(origin_scope=user_context.tree_node): - completion_names += filter.values() - return completion_names - - def _complete_getattr(self, instance): - """ - A heuristic to make completion for proxy objects work. This is not - intended to work in all cases. It works exactly in this case: - - def __getattr__(self, name): - ... - return getattr(any_object, name) - - It is important that the return contains getattr directly, otherwise it - won't work anymore. It's really just a stupid heuristic. It will not - work if you write e.g. `return (getatr(o, name))`, because of the - additional parentheses. It will also not work if you move the getattr - to some other place that is not the return statement itself. - - It is intentional that it doesn't work in all cases. Generally it's - really hard to do even this case (as you can see below). Most people - will write it like this anyway and the other ones, well they are just - out of luck I guess :) ~dave. - """ - names = (instance.get_function_slot_names(u'__getattr__') - or instance.get_function_slot_names(u'__getattribute__')) - functions = ValueSet.from_sets( - name.infer() - for name in names - ) - for func in functions: - tree_node = func.tree_node - for return_stmt in tree_node.iter_return_stmts(): - # Basically until the next comment we just try to find out if a - # return statement looks exactly like `return getattr(x, name)`. - if return_stmt.type != 'return_stmt': - continue - atom_expr = return_stmt.children[1] - if atom_expr.type != 'atom_expr': - continue - atom = atom_expr.children[0] - trailer = atom_expr.children[1] - if len(atom_expr.children) != 2 or atom.type != 'name' \ - or atom.value != 'getattr': - continue - arglist = trailer.children[1] - if arglist.type != 'arglist' or len(arglist.children) < 3: - continue - context = func.as_context() - object_node = arglist.children[0] - - # Make sure it's a param: foo in __getattr__(self, foo) - name_node = arglist.children[2] - name_list = context.goto(name_node, name_node.start_pos) - if not any(n.api_type == 'param' for n in name_list): - continue - - # Now that we know that these are most probably completion - # objects, we just infer the object and return them as - # completions. - objects = context.infer_node(object_node) - return self._complete_trailer_for_values(objects) - return [] + return complete_trailer(user_context, values) def _get_importer_names(self, names, level=0, only_modules=True): names = [n.value for n in names] @@ -568,3 +495,81 @@ def _extract_string_while_in_string(leaf, position): leaves.insert(0, leaf) leaf = leaf.get_previous_leaf() return None, None, None + + +def complete_trailer(user_context, values): + completion_names = [] + for value in values: + for filter in value.get_filters(origin_scope=user_context.tree_node): + completion_names += filter.values() + + if not value.is_stub() and isinstance(value, TreeInstance): + completion_names += _complete_getattr(user_context, value) + + python_values = convert_values(values) + for c in python_values: + if c not in values: + for filter in c.get_filters(origin_scope=user_context.tree_node): + completion_names += filter.values() + return completion_names + + +def _complete_getattr(user_context, instance): + """ + A heuristic to make completion for proxy objects work. This is not + intended to work in all cases. It works exactly in this case: + + def __getattr__(self, name): + ... + return getattr(any_object, name) + + It is important that the return contains getattr directly, otherwise it + won't work anymore. It's really just a stupid heuristic. It will not + work if you write e.g. `return (getatr(o, name))`, because of the + additional parentheses. It will also not work if you move the getattr + to some other place that is not the return statement itself. + + It is intentional that it doesn't work in all cases. Generally it's + really hard to do even this case (as you can see below). Most people + will write it like this anyway and the other ones, well they are just + out of luck I guess :) ~dave. + """ + names = (instance.get_function_slot_names(u'__getattr__') + or instance.get_function_slot_names(u'__getattribute__')) + functions = ValueSet.from_sets( + name.infer() + for name in names + ) + for func in functions: + tree_node = func.tree_node + for return_stmt in tree_node.iter_return_stmts(): + # Basically until the next comment we just try to find out if a + # return statement looks exactly like `return getattr(x, name)`. + if return_stmt.type != 'return_stmt': + continue + atom_expr = return_stmt.children[1] + if atom_expr.type != 'atom_expr': + continue + atom = atom_expr.children[0] + trailer = atom_expr.children[1] + if len(atom_expr.children) != 2 or atom.type != 'name' \ + or atom.value != 'getattr': + continue + arglist = trailer.children[1] + if arglist.type != 'arglist' or len(arglist.children) < 3: + continue + context = func.as_context() + object_node = arglist.children[0] + + # Make sure it's a param: foo in __getattr__(self, foo) + name_node = arglist.children[2] + name_list = context.goto(name_node, name_node.start_pos) + if not any(n.api_type == 'param' for n in name_list): + continue + + # Now that we know that these are most probably completion + # objects, we just infer the object and return them as + # completions. + objects = context.infer_node(object_node) + return complete_trailer(user_context, objects) + return [] diff --git a/jedi/api/helpers.py b/jedi/api/helpers.py index cd6c70c4..24b0db6a 100644 --- a/jedi/api/helpers.py +++ b/jedi/api/helpers.py @@ -462,3 +462,8 @@ def validate_line_column(func): column, line_len, line, line_string)) return func(self, line, column, *args, **kwargs) return wrapper + + +def split_search_string(name): + type, _, dotted_names = name.rpartition(' ') + return type, dotted_names.split('.')