diff --git a/IPython/core/completer.py b/IPython/core/completer.py index bee864f..8e3ecc7 100644 --- a/IPython/core/completer.py +++ b/IPython/core/completer.py @@ -53,6 +53,8 @@ Notes: # Some of this code originated from rlcompleter in the Python standard library # Copyright (C) 2001 Python Software Foundation, www.python.org +from __future__ import print_function + import __main__ import glob import inspect @@ -75,6 +77,14 @@ from IPython.utils.process import arg_split from IPython.utils.py3compat import builtin_mod, string_types, PY3, cast_unicode_py2 from traitlets import CBool, Enum +try: + import jedi + import jedi.api.helpers + import jedi.parser.user_context + JEDI_INSTALLED = True +except ImportError: + JEDI_INSTALLED = False + #----------------------------------------------------------------------------- # Globals #----------------------------------------------------------------------------- @@ -269,6 +279,7 @@ class Completer(Configurable): greedy = CBool(False, config=True, help="""Activate greedy completion + PENDING DEPRECTION. this is now mostly taken care of with Jedi. This will enable completion on elements of lists, results of function calls, etc., but can be unsafe because the code is actually evaluated on TAB. @@ -279,7 +290,7 @@ class Completer(Configurable): def __init__(self, namespace=None, global_namespace=None, **kwargs): """Create a new completer for the command line. - Completer(namespace=ns,global_namespace=ns2) -> completer instance. + Completer(namespace=ns, global_namespace=ns2) -> completer instance. If unspecified, the default namespace where completions are performed is __main__ (technically, __main__.__dict__). Namespaces should be @@ -339,7 +350,6 @@ class Completer(Configurable): defined in self.namespace or self.global_namespace that match. """ - #print 'Completer->global_matches, txt=%r' % text # dbg matches = [] match_append = matches.append n = len(text) @@ -366,7 +376,6 @@ class Completer(Configurable): """ - #io.rprint('Completer->attr_matches, txt=%r' % text) # dbg # Another option, seems to work great. Catches things like ''. m = re.match(r"(\S+(\.\w+)*)\.(\w*)$", text) @@ -565,7 +574,10 @@ class IPCompleter(Completer): """ ) limit_to__all__ = CBool(default_value=False, config=True, - help="""Instruct the completer to use __all__ for the completion + help=""" + DEPRECATED as of version 5.0. + + Instruct the completer to use __all__ for the completion Specifically, when completing on ``object.``. @@ -574,6 +586,9 @@ class IPCompleter(Completer): When False [default]: the __all__ attribute is ignored """ ) + use_jedi_completions = CBool(default_value=JEDI_INSTALLED, config=True, + help="""Use Jedi to generate autocompletions. + """) def __init__(self, shell=None, namespace=None, global_namespace=None, use_readline=True, config=None, **kwargs): @@ -640,7 +655,7 @@ class IPCompleter(Completer): #= re.compile(r'[\s|\[]*(\w+)(?:\s*=?\s*.*)') # All active matcher routines for completion - self.matchers = [self.python_matches, + self.matchers = [ self.file_matches, self.magic_matches, self.python_func_kw_matches, @@ -654,7 +669,7 @@ class IPCompleter(Completer): """ return self.complete(text)[1] - def _clean_glob(self,text): + def _clean_glob(self, text): return self.glob("%s*" % text) def _clean_glob_win32(self,text): @@ -675,8 +690,6 @@ class IPCompleter(Completer): current (as of Python 2.3) Python readline it's possible to do better.""" - #io.rprint('Completer->file_matches: <%r>' % text) # dbg - # chars that require escaping with backslash - i.e. chars # that readline treats incorrectly as delimiters, but we # don't want to treat as delimiters in filename matching @@ -743,7 +756,6 @@ class IPCompleter(Completer): def magic_matches(self, text): """Match magics""" - #print 'Completer->magic_matches:',text,'lb',self.text_until_cursor # dbg # Get all shell magics now rather than statically, so magics loaded at # runtime show up too. lsm = self.shell.magics_manager.lsmagic() @@ -763,10 +775,62 @@ class IPCompleter(Completer): comp += [ pre+m for m in line_magics if m.startswith(bare_text)] return [cast_unicode_py2(c) for c in comp] + def python_jedi_matches(self, text, line_buffer, cursor_pos): + """Match attributes or global Python names using Jedi.""" + if line_buffer.startswith('aimport ') or line_buffer.startswith('%aimport '): + return () + namespaces = [] + if self.namespace is None: + import __main__ + namespaces.append(__main__.__dict__) + else: + namespaces.append(self.namespace) + if self.global_namespace is not None: + namespaces.append(self.global_namespace) + + # cursor_pos is an it, jedi wants line and column + + interpreter = jedi.Interpreter(line_buffer, namespaces, column=cursor_pos) + path = jedi.parser.user_context.UserContext(line_buffer, \ + (1, len(line_buffer))).get_path_until_cursor() + path, dot, like = jedi.api.helpers.completion_parts(path) + if text.startswith('.'): + # text will be `.` on completions like `a[0].` + before = dot + else: + before = line_buffer[:len(line_buffer) - len(like)] + + + def trim_start(completion): + """completions need to start with `text`, trim the beginning until it does""" + if text in completion and not (completion.startswith(text)): + start_index = completion.index(text) + if cursor_pos: + assert start_index < cursor_pos + return completion[start_index:] + return completion + + completions = interpreter.completions() + + completion_text = [c.name_with_symbols for c in completions] + + if self.omit__names: + if self.omit__names == 1: + # true if txt is _not_ a __ name, false otherwise: + no__name = lambda txt: not txt.startswith('__') + else: + # true if txt is _not_ a _ name, false otherwise: + no__name = lambda txt: not txt.startswith('_') + completion_text = filter(no__name, completion_text) + + + return [trim_start(before + c_text) for c_text in completion_text] + + def python_matches(self, text): """Match attributes or global python names""" - - #io.rprint('Completer->python_matches, txt=%r' % text) # dbg + # Jedi completion + if "." in text: try: matches = self.attr_matches(text) @@ -1085,8 +1149,6 @@ class IPCompleter(Completer): event.command = cmd event.text_until_cursor = self.text_until_cursor - #print "\ncustom:{%s]\n" % event # dbg - # for foo etc, try also to find completer for %foo if not cmd.startswith(self.magic_escape): try_magic = self.custom_completers.s_matches( @@ -1142,8 +1204,6 @@ class IPCompleter(Completer): matches : list A list of completion matches. """ - # io.rprint('\nCOMP1 %r %r %r' % (text, line_buffer, cursor_pos)) # dbg - # if the cursor position isn't given, the only sane assumption we can # make is that it's at the end of the line (the common case) if cursor_pos is None: @@ -1172,7 +1232,6 @@ class IPCompleter(Completer): self.line_buffer = line_buffer self.text_until_cursor = self.line_buffer[:cursor_pos] - # io.rprint('COMP2 %r %r %r' % (text, line_buffer, cursor_pos)) # dbg # Start with a clean slate of completions self.matches[:] = [] @@ -1202,10 +1261,11 @@ class IPCompleter(Completer): # different types of objects. The rlcomplete() method could then # simply collapse the dict into a list for readline, but we'd have # richer completion semantics in other evironments. + if self.use_jedi_completions: + self.matches.extend(self.python_jedi_matches(text, line_buffer, cursor_pos)) self.matches = sorted(set(self.matches), key=completions_sorting_key) - #io.rprint('COMP TEXT, MATCHES: %r, %r' % (text, self.matches)) # dbg return text, self.matches def rlcomplete(self, text, state): diff --git a/IPython/core/tests/test_completer.py b/IPython/core/tests/test_completer.py index 777cf1e..b0f90ec 100644 --- a/IPython/core/tests/test_completer.py +++ b/IPython/core/tests/test_completer.py @@ -181,7 +181,7 @@ def test_no_ascii_back_completion(): ip = get_ipython() with TemporaryWorkingDirectory(): # Avoid any filename completions # single ascii letter that don't have yet completions - for letter in 'fjqyJMQVWY' : + for letter in 'jJ' : name, matches = ip.complete('\\'+letter) nt.assert_equal(matches, []) @@ -264,19 +264,36 @@ def test_local_file_completions(): # Now check with a function call cmd = 'a = f("%s' % prefix c = ip.complete(prefix, cmd)[1] - comp = [prefix+s for s in suffixes] - nt.assert_equal(c, comp) + comp = set(prefix+s for s in suffixes) + nt.assert_true(comp.issubset(set(c))) def test_greedy_completions(): ip = get_ipython() ip.ex('a=list(range(5))') _,c = ip.complete('.',line='a[0].') - nt.assert_false('a[0].real' in c, + nt.assert_false('.real' in c, "Shouldn't have completed on a[0]: %s"%c) with greedy_completion(): - _,c = ip.complete('.',line='a[0].') - nt.assert_true('a[0].real' in c, "Should have completed on a[0]: %s"%c) + def _(line, cursor_pos, expect, message): + _,c = ip.complete('.', line=line, cursor_pos=cursor_pos) + nt.assert_in(expect, c, message%c) + + yield _, 'a[0].', 5, '.real', "Should have completed on a[0].: %s" + yield _, 'a[0].r', 6, '.real', "Should have completed on a[0].r: %s" + + if sys.version_info > (3,4): + yield _, 'a[0].from_', 10, '.from_bytes', "Should have completed on a[0].from_: %s" + + + def _2(): + # jedi bug, this will be empty, makeitfail for now, + # once jedi is fixed, switch to assert_in + # https://github.com/davidhalter/jedi/issues/718 + _,c = ip.complete('.',line='a[0].from', cursor_pos=9) + nt.assert_not_in('.from_bytes', c, "Should not have completed on a[0].from (jedi bug), if fails, update test to assert_in: %s"%c) + yield _2 + def test_omit__names(): @@ -321,20 +338,6 @@ def test_limit_to__all__False_ok(): nt.assert_in('d.x', matches) -def test_limit_to__all__True_ok(): - ip = get_ipython() - c = ip.Completer - ip.ex('class D: x=24') - ip.ex('d=D()') - ip.ex("d.__all__=['z']") - cfg = Config() - cfg.IPCompleter.limit_to__all__ = True - c.update_config(cfg) - s, matches = c.complete('d.') - nt.assert_in('d.z', matches) - nt.assert_not_in('d.x', matches) - - def test_get__all__entries_ok(): class A(object): __all__ = ['x', 1] @@ -366,7 +369,6 @@ def test_func_kw_completions(): def test_default_arguments_from_docstring(): - doc = min.__doc__ ip = get_ipython() c = ip.Completer kwd = c._default_arguments_from_docstring( diff --git a/IPython/terminal/ptshell.py b/IPython/terminal/ptshell.py index ba4fa66..90b7a50 100644 --- a/IPython/terminal/ptshell.py +++ b/IPython/terminal/ptshell.py @@ -68,9 +68,13 @@ class IPythonPTCompleter(Completer): yield Completion(m, start_position=start_pos - 1) continue + # TODO: Use Jedi to determine meta_text + # (Jedi currently has a bug that results in incorrect information.) + # meta_text = '' + # yield Completion(m, start_position=start_pos, + # display_meta=meta_text) yield Completion(m, start_position=start_pos) - class IPythonPTLexer(Lexer): """ Wrapper around PythonLexer and BashLexer. diff --git a/setup.py b/setup.py index 7e129a1..e4d0c03 100755 --- a/setup.py +++ b/setup.py @@ -182,7 +182,7 @@ extras_require = dict( parallel = ['ipyparallel'], qtconsole = ['qtconsole'], doc = ['Sphinx>=1.3'], - test = ['nose>=0.10.1', 'requests', 'testpath', 'pygments'], + test = ['nose>=0.10.1', 'requests', 'testpath', 'pygments', 'path.py'], terminal = [], kernel = ['ipykernel'], nbformat = ['nbformat'], @@ -191,6 +191,7 @@ extras_require = dict( ) install_requires = [ 'setuptools>=18.5', + 'jedi', 'decorator', 'pickleshare', 'simplegeneric>0.8',