Beispiel #1
0
class PythonCompleter(Completer):
    """
    Completer for Python code.
    """

    def __init__(
        self,
        get_globals: Callable[[], dict],
        get_locals: Callable[[], dict],
        enable_dictionary_completion: Callable[[], bool],
    ) -> None:
        super().__init__()

        self.get_globals = get_globals
        self.get_locals = get_locals
        self.enable_dictionary_completion = enable_dictionary_completion

        self._system_completer = SystemCompleter()
        self._jedi_completer = JediCompleter(get_globals, get_locals)
        self._dictionary_completer = DictionaryCompleter(get_globals, get_locals)

        self._path_completer_cache: Optional[GrammarCompleter] = None
        self._path_completer_grammar_cache: Optional["_CompiledGrammar"] = None

    @property
    def _path_completer(self) -> GrammarCompleter:
        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) -> "_CompiledGrammar":
        """
        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) -> "_CompiledGrammar":
        def unwrapper(text: str) -> str:
            return re.sub(r"\\(.)", r"\1", text)

        def single_quoted_wrapper(text: str) -> str:
            return text.replace("\\", "\\\\").replace("'", "\\'")

        def double_quoted_wrapper(text: str) -> str:
            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<var1>([^\n'\\]|\\.)*) |  # Inside a single quoted string.
                    " (?P<var2>([^\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: Document) -> bool:
        char_before_cursor = document.char_before_cursor
        return bool(
            document.text
            and (char_before_cursor.isalnum() or char_before_cursor in "/.~")
        )

    def _complete_python_while_typing(self, document: Document) -> bool:
        """
        When `complete_while_typing` is set, only return completions when this
        returns `True`.
        """
        text = document.text_before_cursor  # .rstrip()
        char_before_cursor = text[-1:]
        return bool(
            text and (char_before_cursor.isalnum() or char_before_cursor in "_.([,")
        )

    def get_completions(
        self, document: Document, complete_event: CompleteEvent
    ) -> Iterable[Completion]:
        """
        Get Python completions.
        """
        # If the input starts with an exclamation mark. Use the system completer.
        if document.text.lstrip().startswith("!"):
            yield from self._system_completer.get_completions(
                Document(
                    text=document.text[1:], cursor_position=document.cursor_position - 1
                ),
                complete_event,
            )
            return

        # Do dictionary key completions.
        if complete_event.completion_requested or self._complete_python_while_typing(
            document
        ):
            if self.enable_dictionary_completion():
                has_dict_completions = False
                for c in self._dictionary_completer.get_completions(
                    document, complete_event
                ):
                    if c.text not in "[.":
                        # If we get the [ or . completion, still include the other
                        # completions.
                        has_dict_completions = True
                    yield c
                if has_dict_completions:
                    return

        # Do Path completions (if there were no dictionary completions).
        if complete_event.completion_requested or self._complete_path_while_typing(
            document
        ):
            yield from self._path_completer.get_completions(document, complete_event)

        # Do Jedi completions.
        if complete_event.completion_requested or self._complete_python_while_typing(
            document
        ):
            # If we are inside a string, Don't do Jedi completion.
            if not self._path_completer_grammar.match(document.text_before_cursor):

                # Do Jedi Python completions.
                yield from self._jedi_completer.get_completions(
                    document, complete_event
                )
Beispiel #2
0
class PythonCompleter(Completer):
    """
    Completer for Python code.
    """

    def __init__(
        self, get_globals, get_locals, get_enable_dictionary_completion
    ) -> None:
        super().__init__()

        self.get_globals = get_globals
        self.get_locals = get_locals
        self.get_enable_dictionary_completion = get_enable_dictionary_completion

        self._system_completer = SystemCompleter()
        self._dictionary_completer = DictionaryCompleter(get_globals, get_locals)

        self._path_completer_cache: Optional[GrammarCompleter] = None
        self._path_completer_grammar_cache: Optional["_CompiledGrammar"] = None

    @property
    def _path_completer(self) -> GrammarCompleter:
        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) -> "_CompiledGrammar":
        """
        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) -> "_CompiledGrammar":
        def unwrapper(text: str) -> str:
            return re.sub(r"\\(.)", r"\1", text)

        def single_quoted_wrapper(text: str) -> str:
            return text.replace("\\", "\\\\").replace("'", "\\'")

        def double_quoted_wrapper(text: str) -> str:
            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<var1>([^\n'\\]|\\.)*) |  # Inside a single quoted string.
                    " (?P<var2>([^\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: Document) -> bool:
        char_before_cursor = document.char_before_cursor
        return bool(
            document.text
            and (char_before_cursor.isalnum() or char_before_cursor in "/.~")
        )

    def _complete_python_while_typing(self, document: Document) -> bool:
        char_before_cursor = document.char_before_cursor
        return bool(
            document.text
            and (char_before_cursor.isalnum() or char_before_cursor in "_.")
        )

    def get_completions(
        self, document: Document, complete_event: CompleteEvent
    ) -> Iterable[Completion]:
        """
        Get Python completions.
        """
        # If the input starts with an exclamation mark. Use the system completer.
        if document.text.lstrip().startswith("!"):
            yield from self._system_completer.get_completions(
                Document(
                    text=document.text[1:], cursor_position=document.cursor_position - 1
                ),
                complete_event,
            )
            return

        # Do dictionary key completions.
        if self.get_enable_dictionary_completion():
            has_dict_completions = False
            for c in self._dictionary_completer.get_completions(
                document, complete_event
            ):
                if c.text not in "[.":
                    # If we get the [ or . completion, still include the other
                    # completions.
                    has_dict_completions = True
                yield c
            if has_dict_completions:
                return

        # Do Path completions (if there were no dictionary completions).
        if complete_event.completion_requested or self._complete_path_while_typing(
            document
        ):
            yield from self._path_completer.get_completions(document, complete_event)

        # 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:
                    jedi_completions = script.complete(
                        column=document.cursor_position_col,
                        line=document.cursor_position_row + 1,
                    )
                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 jc in jedi_completions:
                        if jc.type == "function":
                            suffix = "()"
                        else:
                            suffix = ""

                        yield Completion(
                            jc.name_with_symbols,
                            len(jc.complete) - len(jc.name_with_symbols),
                            display=jc.name_with_symbols + suffix,
                            display_meta=jc.type,
                            style=_get_style_for_name(jc.name_with_symbols),
                        )