diff --git a/.travis.yml b/.travis.yml index b049499b..ba8fb069 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,12 @@ python: - 2.7 - 3.2 install: - - if [[ $TRAVIS_PYTHON_VERSION == '2.5' ]]; then pip install --use-mirrors simplejson; fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.5' ]]; then + pip install --use-mirrors simplejson unittest2; + fi + - if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then + pip install --use-mirrors unittest2; + fi - pip install --use-mirrors nose script: - cd test diff --git a/jedi/api.py b/jedi/api.py index 9f1fdeea..00a39691 100644 --- a/jedi/api.py +++ b/jedi/api.py @@ -231,13 +231,37 @@ class Script(object): goto_path = self._module.get_path_under_cursor() context = self._module.get_context() + scopes = set() + lower_priority_operators = ('()', '(', ',') + """Operators that could hide callee.""" if next(context) in ('class', 'def'): scopes = set([self._module.parser.user_scope]) elif not goto_path: op = self._module.get_operator_under_cursor() - scopes = set([keywords.get_operator(op, self.pos)] if op else []) - else: - scopes = set(self._prepare_goto(goto_path)) + if op and op not in lower_priority_operators: + scopes = set([keywords.get_operator(op, self.pos)]) + + # Fetch definition of callee + if not goto_path: + (call, _) = self._func_call_and_param_index() + if call is not None: + while call.next is not None: + call = call.next + # reset cursor position: + (row, col) = call.name.end_pos + self.pos = (row, max(col - 1, 0)) + self._module = modules.ModuleWithCursor( + self._source_path, + source=self.source, + position=self.pos) + # then try to find the path again + goto_path = self._module.get_path_under_cursor() + + if not scopes: + if goto_path: + scopes = set(self._prepare_goto(goto_path)) + elif op in lower_priority_operators: + scopes = set([keywords.get_operator(op, self.pos)]) scopes = resolve_import_paths(scopes) @@ -370,6 +394,25 @@ class Script(object): :rtype: :class:`api_classes.CallDef` """ + + (call, index) = self._func_call_and_param_index() + if call is None: + return None + + user_stmt = self._parser.user_stmt + with common.scale_speed_settings(settings.scale_function_definition): + _callable = lambda: evaluate.follow_call(call) + origins = cache.cache_function_definition(_callable, user_stmt) + debug.speed('func_call followed') + + if len(origins) == 0: + return None + # just take entry zero, because we need just one. + executable = origins[0] + + return api_classes.CallDef(executable, index, call) + + def _func_call_and_param_index(self): def check_user_stmt(user_stmt): if user_stmt is None \ or not isinstance(user_stmt, pr.Statement): @@ -404,37 +447,17 @@ class Script(object): if repr(old_call) == repr(call): # return the index of the part_parser return old_call, index - return None, 0 - else: - raise NotFoundError() + return None, 0 debug.speed('func_call start') call = None + index = 0 if settings.use_function_definition_cache: - try: - call, index = check_cache() - except NotFoundError: - return None - - user_stmt = self._parser.user_stmt + call, index = check_cache() if call is None: - # This is a backup, if the above is not successful. - call, index = check_user_stmt(user_stmt) - if call is None: - return None + call, index = check_user_stmt(self._parser.user_stmt) debug.speed('func_call parsed') - - with common.scale_speed_settings(settings.scale_function_definition): - _callable = lambda: evaluate.follow_call(call) - origins = cache.cache_function_definition(_callable, user_stmt) - debug.speed('func_call followed') - - if len(origins) == 0: - return None - # just take entry zero, because we need just one. - executable = origins[0] - - return api_classes.CallDef(executable, index, call) + return call, index def _get_on_import_stmt(self, is_like_search=False): """ Resolve the user statement, if it is an import. Only resolve the diff --git a/test/base.py b/test/base.py index 29cc966b..a9e2d9ab 100644 --- a/test/base.py +++ b/test/base.py @@ -1,7 +1,9 @@ -import unittest - import time import sys +if sys.hexversion < 0x02070000: + import unittest2 as unittest +else: + import unittest import os from os.path import abspath, dirname import functools diff --git a/test/completion/definition.py b/test/completion/definition.py new file mode 100644 index 00000000..3fdd88e5 --- /dev/null +++ b/test/completion/definition.py @@ -0,0 +1,65 @@ +""" +Fallback to callee definition when definition not found. +- https://github.com/davidhalter/jedi/issues/131 +- https://github.com/davidhalter/jedi/pull/149 +""" + +"""Parenthesis closed at next line.""" + +#? isinstance +isinstance( +) + +#? isinstance +isinstance( +) + +#? isinstance +isinstance(None, +) + +#? isinstance +isinstance(None, +) + +"""Parenthesis closed at same line.""" + +# Note: len('isinstance(') == 11 +#? 11 isinstance +isinstance() + +# Note: len('isinstance(None,') == 16 +##? 16 isinstance +isinstance(None,) + +# Note: len('isinstance(None,') == 16 +##? 16 isinstance +isinstance(None, ) + +# Note: len('isinstance(None, ') == 17 +##? 17 isinstance +isinstance(None, ) + +# Note: len('isinstance( ') == 12 +##? 12 isinstance +isinstance( ) + +"""Unclosed parenthesis.""" + +#? isinstance +isinstance( + +def x(): pass # acts like EOF + +##? isinstance +isinstance( + +def x(): pass # acts like EOF + +#? isinstance +isinstance(None, + +def x(): pass # acts like EOF + +#? isinstance +isinstance(None, diff --git a/test/regression.py b/test/regression.py index 6e78eccd..54dfe755 100755 --- a/test/regression.py +++ b/test/regression.py @@ -5,17 +5,18 @@ Unit tests to avoid errors of the past. Makes use of Python's ``unittest`` module. """ -import unittest import time import functools import itertools import os +import textwrap -from base import TestBase, cwd_at +from base import TestBase, unittest, cwd_at import jedi from jedi._compatibility import is_py25, utf8, unicode from jedi import api +from jedi import api_classes #jedi.set_debug_function(jedi.debug.print_to_stdout) @@ -145,10 +146,15 @@ class TestRegression(TestBase): assert self.complete("from datetime import")[0].word == 'import' assert self.complete("from datetime import ") + def assert_call_def(self, call_def, name, index): + self.assertEqual( + {'call_name': getattr(call_def, 'call_name', None), + 'index': getattr(call_def, 'index', None)}, + {'call_name': name, 'index': index}, + ) + def test_function_definition(self): - def check(call_def, name, index): - return call_def and call_def.call_name == name \ - and call_def.index == index + check = self.assert_call_def # simple s = "abs(a, str(" @@ -161,13 +167,13 @@ class TestRegression(TestBase): s7 = "str().upper().center(" s8 = "str(int[zip(" - assert check(self.function_definition(s, (1, 4)), 'abs', 0) - assert check(self.function_definition(s, (1, 6)), 'abs', 1) - assert check(self.function_definition(s, (1, 7)), 'abs', 1) - assert check(self.function_definition(s, (1, 8)), 'abs', 1) - assert check(self.function_definition(s, (1, 11)), 'str', 0) + check(self.function_definition(s, (1, 4)), 'abs', 0) + check(self.function_definition(s, (1, 6)), 'abs', 1) + check(self.function_definition(s, (1, 7)), 'abs', 1) + check(self.function_definition(s, (1, 8)), 'abs', 1) + check(self.function_definition(s, (1, 11)), 'str', 0) - assert check(self.function_definition(s2, (1, 4)), 'abs', 0) + check(self.function_definition(s2, (1, 4)), 'abs', 0) assert self.function_definition(s2, (1, 5)) is None assert self.function_definition(s2) is None @@ -175,43 +181,41 @@ class TestRegression(TestBase): assert self.function_definition(s3) is None assert self.function_definition(s4, (1, 3)) is None - assert check(self.function_definition(s4, (1, 4)), 'abs', 0) - assert check(self.function_definition(s4, (1, 8)), 'zip', 0) - assert check(self.function_definition(s4, (1, 9)), 'abs', 0) - #assert check(self.function_definition(s4, (1, 10)), 'abs', 1) + check(self.function_definition(s4, (1, 4)), 'abs', 0) + check(self.function_definition(s4, (1, 8)), 'zip', 0) + check(self.function_definition(s4, (1, 9)), 'abs', 0) + #check(self.function_definition(s4, (1, 10)), 'abs', 1) - assert check(self.function_definition(s5, (1, 4)), 'abs', 0) - assert check(self.function_definition(s5, (1, 6)), 'abs', 1) + check(self.function_definition(s5, (1, 4)), 'abs', 0) + check(self.function_definition(s5, (1, 6)), 'abs', 1) - assert check(self.function_definition(s6), 'center', 0) - assert check(self.function_definition(s6, (1, 4)), 'str', 0) + check(self.function_definition(s6), 'center', 0) + check(self.function_definition(s6, (1, 4)), 'str', 0) - assert check(self.function_definition(s7), 'center', 0) - assert check(self.function_definition(s8), 'zip', 0) - assert check(self.function_definition(s8, (1, 8)), 'str', 0) + check(self.function_definition(s7), 'center', 0) + check(self.function_definition(s8), 'zip', 0) + check(self.function_definition(s8, (1, 8)), 'str', 0) s = "import time; abc = time; abc.sleep(" - assert check(self.function_definition(s), 'sleep', 0) + check(self.function_definition(s), 'sleep', 0) # jedi-vim #9 s = "with open(" - assert check(self.function_definition(s), 'open', 0) + check(self.function_definition(s), 'open', 0) # jedi-vim #11 s1 = "for sorted(" - assert check(self.function_definition(s1), 'sorted', 0) + check(self.function_definition(s1), 'sorted', 0) s2 = "for s in sorted(" - assert check(self.function_definition(s2), 'sorted', 0) + check(self.function_definition(s2), 'sorted', 0) # jedi #57 s = "def func(alpha, beta): pass\n" \ "func(alpha='101'," - assert check(self.function_definition(s, (2, 13)), 'func', 0) + check(self.function_definition(s, (2, 13)), 'func', 0) def test_function_definition_complex(self): - def check(call_def, name, index): - return call_def and call_def.call_name == name \ - and call_def.index == index + check = self.assert_call_def s = """ def abc(a,b): @@ -223,18 +227,27 @@ class TestRegression(TestBase): if 1: pass """ - assert check(self.function_definition(s, (6, 24)), 'abc', 0) + check(self.function_definition(s, (6, 24)), 'abc', 0) s = """ import re def huhu(it): re.compile( return it * 2 """ - assert check(self.function_definition(s, (4, 31)), 'compile', 0) + check(self.function_definition(s, (4, 31)), 'compile', 0) # jedi-vim #70 s = """def foo(""" assert self.function_definition(s) is None + @unittest.skip("function_definition at ``f( |)`` does not work") + def test_function_definition_empty_paren_pre_space(self): + s = textwrap.dedent("""\ + def f(a, b): + pass + f( )""") + call_def = self.function_definition(s, (3, 3)) + self.assert_call_def(call_def, 'f', 0) + @cwd_at('jedi') def test_add_dynamic_mods(self): api.settings.additional_dynamic_modules = ['dynamic.py'] @@ -384,9 +397,14 @@ class TestFeature(TestBase): """ feature request #61""" assert self.complete('import os; os.path.join')[0].full_name \ == 'os.path.join' - # issue #94 - defs = self.definition("""import os; os.path.join(""") - assert defs[0].full_name is None + + def test_keyword_full_name_should_be_none(self): + """issue #94""" + # Using `from jedi.keywords import Keyword` here does NOT work + # in Python 3. This is due to the import hack jedi using. + Keyword = api_classes.keywords.Keyword + d = api_classes.Definition(Keyword('(', (0, 0))) + assert d.full_name is None def test_full_name_builtin(self): self.assertEqual(self.complete('type')[0].full_name, 'type') diff --git a/tox.ini b/tox.ini index db9d67dc..2a39e11c 100644 --- a/tox.ini +++ b/tox.ini @@ -14,4 +14,9 @@ changedir = test [testenv:py25] deps = simplejson + unittest2 + {[testenv]deps} +[testenv:py26] +deps = + unittest2 {[testenv]deps}