diff --git a/jedi/evaluate/__init__.py b/jedi/evaluate/__init__.py index f3df8469..823730b5 100644 --- a/jedi/evaluate/__init__.py +++ b/jedi/evaluate/__init__.py @@ -111,8 +111,8 @@ class Evaluator(object): self.recursion_detector = recursion.RecursionDetector(self) self.execution_recursion_detector = recursion.ExecutionRecursionDetector(self) - def find_types(self, context, name_str, position=None, search_global=False, - is_goto=False): + def find_types(self, context, name_or_str, name_context, position=None, + search_global=False, is_goto=False): """ This is the search function. The most important part to debug. `remove_statements` and `filter_statements` really are the core part of @@ -121,7 +121,7 @@ class Evaluator(object): :param position: Position of the last statement -> tuple of line, column :return: List of Names. Their parents are the types. """ - f = finder.NameFinder(self, context, name_str, position) + f = finder.NameFinder(self, context, name_context, name_or_str, position) filters = f.get_filters(search_global) if is_goto: return f.filter_name(filters) @@ -152,7 +152,8 @@ class Evaluator(object): operator = copy.copy(first_operation) operator.value = operator.value[:-1] name = str(stmt.get_defined_names()[0]) - left = self.find_types(context, name, stmt.start_pos, search_global=True) + left = context.py__getattribute__( + name, position=stmt.start_pos, search_global=True) for_stmt = stmt.get_parent_until(tree.ForStmt) if isinstance(for_stmt, tree.ForStmt) and types \ @@ -294,8 +295,10 @@ class Evaluator(object): types = self._eval_atom(context, element.children[0]) for next_name in element.children[2::2]: # TODO add search_global=True? - types = set(chain.from_iterable(self.find_types(typ, next_name) - for typ in types)) + types = unite( + typ.py__getattribute__(next_name, name_context=context) + for typ in types + ) types = types elif element.type == 'eval_input': types = self._eval_element_not_cached(context, element.children[0]) @@ -326,7 +329,11 @@ class Evaluator(object): # We only need to adjust the start_pos for statements, because # there the name cannot be used. stmt = atom - return self.find_types(context, atom, stmt.start_pos, search_global=True) + return context.py__getattribute__( + name_or_str=atom, + position=stmt.start_pos, + search_global=True + ) elif isinstance(atom, tree.Literal): return set([compiled.create(self, atom.eval())]) else: @@ -372,7 +379,10 @@ class Evaluator(object): for typ in types: debug.dbg('eval_trailer: %s in scope %s', trailer, typ) if trailer_op == '.': - new_types |= self.find_types(typ, node) + new_types |= typ.py__getattribute__( + name_context=context, + name_or_str=node + ) elif trailer_op == '(': arguments = param.TreeArguments(self, context, node, trailer) new_types |= self.execute(typ, arguments) @@ -477,27 +487,32 @@ class Evaluator(object): new_dotted.children[index - 1:] = [] values = self.eval_element(context, new_dotted) return unite( - self.find_types(value, name, is_goto=True) for value in values + value.py__getattribute__(name, name_context=context, is_goto=True) + for value in values ) - return resolve_implicit_imports(iterable.unite( - self.find_types(typ, name, is_goto=True) for typ in types - )) + #return resolve_implicit_imports(iterable.unite( + #self.find_types(typ, name, is_goto=True) for typ in types + #)) if tree.is_node(par, 'trailer') and par.children[0] == '.': values = helpers.evaluate_call_of_leaf(context, name, cut_own_trailer=True) return unite( - self.find_types(value, name, is_goto=True) for value in values + value.py__getattribute__(name, name_context=context, is_goto=True) + for value in values ) - return resolve_implicit_imports(iterable.unite( - self.find_types(typ, name, is_goto=True) for typ in types - )) + #return resolve_implicit_imports(iterable.unite( + #self.find_types(typ, name, is_goto=True) for typ in types + #)) else: if stmt.type != 'expr_stmt': # We only need to adjust the start_pos for statements, because # there the name cannot be used. stmt = name - return self.find_types(context, name, stmt.start_pos, - search_global=True, is_goto=True) + return context.py__getattribute__( + name, + position=stmt.start_pos, + search_global=True, is_goto=True + ) def wrap(self, element, parent_context): raise DeprecationWarning diff --git a/jedi/evaluate/context.py b/jedi/evaluate/context.py index 2f68a195..ed870233 100644 --- a/jedi/evaluate/context.py +++ b/jedi/evaluate/context.py @@ -46,9 +46,12 @@ class Context(object): def eval_trailer(self, types, trailer): return self.evaluator.eval_trailer(self, types, trailer) - def py__getattribute__(self, name_or_str, position=None, + def py__getattribute__(self, name_or_str, name_context=None, position=None, search_global=False, is_goto=False): - return self.evaluator.find_types(self, name_or_str, position, search_global, is_goto) + if name_context is None: + name_context = self + return self.evaluator.find_types( + self, name_or_str, name_context, position, search_global, is_goto) def create_context(self, node): return self.evaluator.create_context(self, node) diff --git a/jedi/evaluate/finder.py b/jedi/evaluate/finder.py index ae7555f8..c567c7b5 100644 --- a/jedi/evaluate/finder.py +++ b/jedi/evaluate/finder.py @@ -33,6 +33,7 @@ from jedi.evaluate import analysis from jedi.evaluate import flow_analysis from jedi.evaluate import param from jedi.evaluate import helpers +from jedi.evaluate.context import TreeContext from jedi.evaluate.cache import memoize_default from jedi.evaluate.filters import get_global_filters, ContextName @@ -95,10 +96,11 @@ def filter_definition_names(names, origin, position=None): class NameFinder(object): - def __init__(self, evaluator, context, name_or_str, position=None): + def __init__(self, evaluator, context, name_context, name_or_str, position=None): self._evaluator = evaluator # Make sure that it's not just a syntax tree node. self._context = context + self._name_context = name_context self._name = name_or_str if isinstance(name_or_str, tree.Name): self._string_name = name_or_str.value @@ -308,19 +310,17 @@ class NameFinder(object): types = set() # Add isinstance and other if/assert knowledge. - #if isinstance(self._name, tree.Name): - ## Ignore FunctionExecution parents for now. - #flow_scope = self._name - #until = flow_scope.get_parent_until(er.FunctionExecution) - #while not isinstance(until, er.FunctionExecution): - #flow_scope = flow_scope.get_parent_scope(include_flows=True) - #if flow_scope is None: - #break - ## TODO check if result is in scope -> no evaluation necessary - #n = check_flow_information(self._evaluator, flow_scope, - #self._name, self._position) - #if n: - #return n + if isinstance(self._name, tree.Name) and \ + not isinstance(self._name_context, AbstractInstanceContext): + # Ignore FunctionExecution parents for now. + flow_scope = self._name + while flow_scope != self._name_context.get_node(): + flow_scope = flow_scope.get_parent_scope(include_flows=True) + # TODO check if result is in scope -> no evaluation necessary + n = check_flow_information(self._name_context, flow_scope, + self._name, self._position) + if n: + return n for name in names: new_types = name.infer() @@ -515,14 +515,14 @@ def _eval_param(evaluator, context, param, scope): if not res_new: if param.stars: t = 'tuple' if param.stars == 1 else 'dict' - typ = list(evaluator.find_types(evaluator.BUILTINS, t))[0] + typ = list(evaluator.BUILTINS.py__getattribute__(t))[0] res_new = evaluator.execute(typ) if param.default: res_new |= evaluator.eval_element(context, param.default) return res_new -def check_flow_information(evaluator, flow, search_name, pos): +def check_flow_information(context, flow, search_name, pos): """ Try to find out the type of a variable just with the information that is given by the flows: e.g. It is also responsible for assert checks.:: @@ -545,7 +545,7 @@ def check_flow_information(evaluator, flow, search_name, pos): for name in names: ass = name.get_parent_until(tree.AssertStmt) if isinstance(ass, tree.AssertStmt) and pos is not None and ass.start_pos < pos: - result = _check_isinstance_type(evaluator, ass.assertion(), search_name) + result = _check_isinstance_type(context, ass.assertion(), search_name) if result: break @@ -553,11 +553,11 @@ def check_flow_information(evaluator, flow, search_name, pos): potential_ifs = [c for c in flow.children[1::4] if c != ':'] for if_test in reversed(potential_ifs): if search_name.start_pos > if_test.end_pos: - return _check_isinstance_type(evaluator, if_test, search_name) + return _check_isinstance_type(context, if_test, search_name) return result -def _check_isinstance_type(evaluator, element, search_name): +def _check_isinstance_type(context, element, search_name): try: assert element.type in ('power', 'atom_expr') # this might be removed if we analyze and, etc @@ -569,27 +569,29 @@ def _check_isinstance_type(evaluator, element, search_name): # arglist stuff arglist = trailer.children[1] - args = param.Arguments(evaluator, arglist, trailer) - lst = list(args.unpack()) + args = param.TreeArguments(context.evaluator, context, arglist, trailer) + param_list = list(args.unpack()) # Disallow keyword arguments - assert len(lst) == 2 and lst[0][0] is None and lst[1][0] is None - name = lst[0][1][0] # first argument, values, first value + assert len(param_list) == 2 + (key1, lazy_context_object), (key2, lazy_context_cls) = param_list + assert key1 is None and key2 is None + call = helpers.call_of_leaf(search_name) + is_instance_call = helpers.call_of_leaf(lazy_context_object.data) # Do a simple get_code comparison. They should just have the same code, # and everything will be all right. - classes = lst[1][1][0] - call = helpers.call_of_leaf(search_name) - assert name.get_code(normalized=True) == call.get_code(normalized=True) + assert is_instance_call.get_code(normalized=True) == call.get_code(normalized=True) except AssertionError: return set() result = set() - for cls_or_tup in evaluator.eval_element(classes): - if isinstance(cls_or_tup, iterable.Array) and cls_or_tup.type == 'tuple': + for cls_or_tup in lazy_context_cls.infer(): + if isinstance(cls_or_tup, iterable.AbstractSequence) and \ + cls_or_tup.array_type == 'tuple': for lazy_context in cls_or_tup.py__iter__(): for context in lazy_context.infer(): result |= context.execute_evaluated() else: - result |= evaluator.execute(cls_or_tup) + result |= cls_or_tup.execute_evaluated() return result diff --git a/jedi/evaluate/helpers.py b/jedi/evaluate/helpers.py index 970f924a..7a81935b 100644 --- a/jedi/evaluate/helpers.py +++ b/jedi/evaluate/helpers.py @@ -121,6 +121,49 @@ def evaluate_call_of_leaf(context, leaf, cut_own_trailer=False): ''' +def call_of_leaf(leaf): + """ + Creates a "call" node that consist of all ``trailer`` and ``power`` + objects. E.g. if you call it with ``append``:: + + list([]).append(3) or None + + You would get a node with the content ``list([]).append`` back. + + This generates a copy of the original ast node. + + If you're using the leaf, e.g. the bracket `)` it will return ``list([])``. + """ + # TODO this is the old version of this call. Try to remove it. + trailer = leaf.parent + # The leaf may not be the last or first child, because there exist three + # different trailers: `( x )`, `[ x ]` and `.x`. In the first two examples + # we should not match anything more than x. + if trailer.type != 'trailer' or leaf not in (trailer.children[0], trailer.children[-1]): + if trailer.type == 'atom': + return trailer + return leaf + + power = trailer.parent + index = power.children.index(trailer) + + new_power = copy.copy(power) + new_power.children = list(new_power.children) + new_power.children[index + 1:] = [] + + if power.type == 'error_node': + start = index + while True: + start -= 1 + if power.children[start].type != 'trailer': + break + transformed = tree.Node('power', power.children[start:]) + transformed.parent = power.parent + return transformed + + return power + + def get_names_of_node(node): try: children = node.children diff --git a/jedi/evaluate/imports.py b/jedi/evaluate/imports.py index 2f8545c3..b0864fc8 100644 --- a/jedi/evaluate/imports.py +++ b/jedi/evaluate/imports.py @@ -92,9 +92,11 @@ class ImportWrapper(object): if from_import_name is not None: types = unite( - evaluator.find_types(t, unicode(from_import_name), - is_goto=is_goto) - for t in types + t.py__getattribute__( + unicode(from_import_name), + name_context=self._context, + is_goto=is_goto + ) for t in types ) if not types: @@ -316,7 +318,7 @@ class Importer(object): # that is being achieved by messing with ``sys.modules`` in # ``os``. if [str(i) for i in import_path] == ['os', 'path']: - return self._evaluator.find_types(parent_module, 'path') + return parent_module.py__getattribute__('path') try: method = parent_module.py__path__ diff --git a/jedi/evaluate/pep0484.py b/jedi/evaluate/pep0484.py index 4a0dcee7..28b344eb 100644 --- a/jedi/evaluate/pep0484.py +++ b/jedi/evaluate/pep0484.py @@ -71,7 +71,7 @@ def _fix_forward_reference(context, node): else: module = node.get_parent_until() new_node.move(module.end_pos[0]) - new_node.parent = module + new_node.parent = context.get_node() return new_node else: return node diff --git a/jedi/evaluate/stdlib.py b/jedi/evaluate/stdlib.py index 29aad1af..c942cd01 100644 --- a/jedi/evaluate/stdlib.py +++ b/jedi/evaluate/stdlib.py @@ -112,7 +112,7 @@ def builtins_getattr(evaluator, objects, names, defaults=None): for obj in objects: for name in names: if precedence.is_string(name): - return evaluator.find_types(obj, name.obj) + return obj.py__getattribute__(name.obj) else: debug.warning('getattr called without str') continue @@ -185,12 +185,12 @@ def builtins_isinstance(evaluator, objects, types, arguments): classes = unite(cls_or_tup.py__iter__()) bool_results.add(any(cls in mro for cls in classes)) else: - _, nodes = list(arguments.unpack())[1] - for node in nodes: - message = 'TypeError: isinstance() arg 2 must be a ' \ - 'class, type, or tuple of classes and types, ' \ - 'not %s.' % cls_or_tup - analysis.add(evaluator, 'type-error-isinstance', node, message) + _, lazy_context = list(arguments.unpack())[1] + node = lazy_context.data + message = 'TypeError: isinstance() arg 2 must be a ' \ + 'class, type, or tuple of classes and types, ' \ + 'not %s.' % cls_or_tup + analysis.add(evaluator, 'type-error-isinstance', node, message) return set(compiled.create(evaluator, x) for x in bool_results) diff --git a/test/run.py b/test/run.py index 43526220..4946404e 100755 --- a/test/run.py +++ b/test/run.py @@ -123,7 +123,6 @@ from jedi._compatibility import unicode, is_py3 from jedi.parser import Parser, load_grammar from jedi.api.classes import Definition from jedi.api.completion import get_user_scope -from jedi.evaluate.representation import ModuleContext TEST_COMPLETIONS = 0 @@ -193,8 +192,8 @@ class IntegrationTestCase(object): element = parser.get_parsed_node() module_context = script._get_module() # The context shouldn't matter for the test results. - element.parent = module_context.module_node user_context = get_user_scope(module_context, (self.line_nr, 0)) + element.parent = user_context.get_node() results = evaluator.eval_element(user_context, element) if not results: raise Exception('Could not resolve %s on line %s'