from __future__ import unicode_literals from prompt_toolkit.completion import Completer, Completion, PathCompleter from prompt_toolkit.contrib.regular_languages.compiler import compile as compile_grammar from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter from ptpython.utils import get_jedi_script_from_document import re __all__ = ( 'PythonCompleter', ) class PythonCompleter(Completer): """ Completer for Python code. """ def __init__(self, get_globals, get_locals): super(PythonCompleter, self).__init__() self.get_globals = get_globals self.get_locals = get_locals self._path_completer_cache = None self._path_completer_grammar_cache = None @property def _path_completer(self): if self._path_completer_cache is None: self._path_completer_cache = GrammarCompleter( self._path_completer_grammar, { 'var1': PathCompleter(expanduser=True), 'var2': PathCompleter(expanduser=True), }) return self._path_completer_cache @property def _path_completer_grammar(self): """ Return the grammar for matching paths inside strings inside Python code. """ # We make this lazy, because it delays startup time a little bit. # This way, the grammar is build during the first completion. if self._path_completer_grammar_cache is None: self._path_completer_grammar_cache = self._create_path_completer_grammar() return self._path_completer_grammar_cache def _create_path_completer_grammar(self): def unwrapper(text): return re.sub(r'\\(.)', r'\1', text) def single_quoted_wrapper(text): return text.replace('\\', '\\\\').replace("'", "\\'") def double_quoted_wrapper(text): return text.replace('\\', '\\\\').replace('"', '\\"') grammar = r""" # Text before the current string. ( [^'"#] | # Not quoted characters. ''' ([^'\\]|'(?!')|''(?!')|\\.])* ''' | # Inside single quoted triple strings "" " ([^"\\]|"(?!")|""(?!^)|\\.])* "" " | # Inside double quoted triple strings \#[^\n]*(\n|$) | # Comment. "(?!"") ([^"\\]|\\.)*" | # Inside double quoted strings. '(?!'') ([^'\\]|\\.)*' # Inside single quoted strings. # Warning: The negative lookahead in the above two # statements is important. If we drop that, # then the regex will try to interpret every # triple quoted string also as a single quoted # string, making this exponentially expensive to # execute! )* # The current string that we're completing. ( ' (?P([^\n'\\]|\\.)*) | # Inside a single quoted string. " (?P([^\n"\\]|\\.)*) # Inside a double quoted string. ) """ return compile_grammar( grammar, escape_funcs={ 'var1': single_quoted_wrapper, 'var2': double_quoted_wrapper, }, unescape_funcs={ 'var1': unwrapper, 'var2': unwrapper, }) def _complete_path_while_typing(self, document): char_before_cursor = document.char_before_cursor return document.text and ( char_before_cursor.isalnum() or char_before_cursor in '/.~') def _complete_python_while_typing(self, document): char_before_cursor = document.char_before_cursor return document.text and ( char_before_cursor.isalnum() or char_before_cursor in '_.') def get_completions(self, document, complete_event): """ Get Python completions. """ # Do Path completions if complete_event.completion_requested or self._complete_path_while_typing(document): for c in self._path_completer.get_completions(document, complete_event): yield c # If we are inside a string, Don't do Jedi completion. if self._path_completer_grammar.match(document.text_before_cursor): return # Do Jedi Python completions. if complete_event.completion_requested or self._complete_python_while_typing(document): script = get_jedi_script_from_document(document, self.get_locals(), self.get_globals()) if script: try: completions = script.completions() except TypeError: # Issue #9: bad syntax causes completions() to fail in jedi. # https://github.com/jonathanslenders/python-prompt-toolkit/issues/9 pass except UnicodeDecodeError: # Issue #43: UnicodeDecodeError on OpenBSD # https://github.com/jonathanslenders/python-prompt-toolkit/issues/43 pass except AttributeError: # Jedi issue #513: https://github.com/davidhalter/jedi/issues/513 pass except ValueError: # Jedi issue: "ValueError: invalid \x escape" pass except KeyError: # Jedi issue: "KeyError: u'a_lambda'." # https://github.com/jonathanslenders/ptpython/issues/89 pass except IOError: # Jedi issue: "IOError: No such file or directory." # https://github.com/jonathanslenders/ptpython/issues/71 pass except AssertionError: # In jedi.parser.__init__.py: 227, in remove_last_newline, # the assertion "newline.value.endswith('\n')" can fail. pass except SystemError: # In jedi.api.helpers.py: 144, in get_stack_at_position # raise SystemError("This really shouldn't happen. There's a bug in Jedi.") pass except NotImplementedError: # See: https://github.com/jonathanslenders/ptpython/issues/223 pass except Exception: # Supress all other Jedi exceptions. pass else: for c in completions: yield Completion(c.name_with_symbols, len(c.complete) - len(c.name_with_symbols), display=c.name_with_symbols)