From 227038a454943a075c51b60988c53f77a605fa10 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 26 Feb 2021 05:10:02 +0100 Subject: Adding upstream version 3.0.16. Signed-off-by: Daniel Baumann --- ptpython/utils.py | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 198 insertions(+) create mode 100644 ptpython/utils.py (limited to 'ptpython/utils.py') diff --git a/ptpython/utils.py b/ptpython/utils.py new file mode 100644 index 0000000..2fb24a4 --- /dev/null +++ b/ptpython/utils.py @@ -0,0 +1,198 @@ +""" +For internal use only. +""" +import re +from typing import Callable, Iterable, Type, TypeVar, cast + +from prompt_toolkit.formatted_text import to_formatted_text +from prompt_toolkit.formatted_text.utils import fragment_list_to_text +from prompt_toolkit.mouse_events import MouseEvent, MouseEventType + +__all__ = [ + "has_unclosed_brackets", + "get_jedi_script_from_document", + "document_is_multiline_python", + "unindent_code", +] + + +def has_unclosed_brackets(text: str) -> bool: + """ + Starting at the end of the string. If we find an opening bracket + for which we didn't had a closing one yet, return True. + """ + stack = [] + + # Ignore braces inside strings + text = re.sub(r"""('[^']*'|"[^"]*")""", "", text) # XXX: handle escaped quotes.! + + for c in reversed(text): + if c in "])}": + stack.append(c) + + elif c in "[({": + if stack: + if ( + (c == "[" and stack[-1] == "]") + or (c == "{" and stack[-1] == "}") + or (c == "(" and stack[-1] == ")") + ): + stack.pop() + else: + # Opening bracket for which we didn't had a closing one. + return True + + return False + + +def get_jedi_script_from_document(document, locals, globals): + import jedi # We keep this import in-line, to improve start-up time. + + # Importing Jedi is 'slow'. + + try: + return jedi.Interpreter( + document.text, + path="input-text", + namespaces=[locals, globals], + ) + except ValueError: + # Invalid cursor position. + # ValueError('`column` parameter is not in a valid range.') + return None + except AttributeError: + # Workaround for #65: https://github.com/jonathanslenders/python-prompt-toolkit/issues/65 + # See also: https://github.com/davidhalter/jedi/issues/508 + return None + except IndexError: + # Workaround Jedi issue #514: for https://github.com/davidhalter/jedi/issues/514 + return None + except KeyError: + # Workaroud for a crash when the input is "u'", the start of a unicode string. + return None + except Exception: + # Workaround for: https://github.com/jonathanslenders/ptpython/issues/91 + return None + + +_multiline_string_delims = re.compile("""[']{3}|["]{3}""") + + +def document_is_multiline_python(document): + """ + Determine whether this is a multiline Python document. + """ + + def ends_in_multiline_string() -> bool: + """ + ``True`` if we're inside a multiline string at the end of the text. + """ + delims = _multiline_string_delims.findall(document.text) + opening = None + for delim in delims: + if opening is None: + opening = delim + elif delim == opening: + opening = None + return bool(opening) + + if "\n" in document.text or ends_in_multiline_string(): + return True + + def line_ends_with_colon() -> bool: + return document.current_line.rstrip()[-1:] == ":" + + # If we just typed a colon, or still have open brackets, always insert a real newline. + if ( + line_ends_with_colon() + or ( + document.is_cursor_at_the_end + and has_unclosed_brackets(document.text_before_cursor) + ) + or document.text.startswith("@") + ): + return True + + # If the character before the cursor is a backslash (line continuation + # char), insert a new line. + elif document.text_before_cursor[-1:] == "\\": + return True + + return False + + +_T = TypeVar("_T", bound=Callable[[MouseEvent], None]) + + +def if_mousedown(handler: _T) -> _T: + """ + Decorator for mouse handlers. + Only handle event when the user pressed mouse down. + + (When applied to a token list. Scroll events will bubble up and are handled + by the Window.) + """ + + def handle_if_mouse_down(mouse_event: MouseEvent): + if mouse_event.event_type == MouseEventType.MOUSE_DOWN: + return handler(mouse_event) + else: + return NotImplemented + + return cast(_T, handle_if_mouse_down) + + +_T_type = TypeVar("_T_type", bound=Type) + + +def ptrepr_to_repr(cls: _T_type) -> _T_type: + """ + Generate a normal `__repr__` method for classes that have a `__pt_repr__`. + """ + if not hasattr(cls, "__pt_repr__"): + raise TypeError( + "@ptrepr_to_repr can only be applied to classes that have a `__pt_repr__` method." + ) + + def __repr__(self) -> str: + return fragment_list_to_text(to_formatted_text(cls.__pt_repr__(self))) + + cls.__repr__ = __repr__ # type:ignore + return cls + + +def unindent_code(text: str) -> str: + """ + Remove common leading whitespace when all lines are indented. + """ + lines = text.splitlines(keepends=True) + + # Look for common prefix. + common_prefix = _common_whitespace_prefix(lines) + + # Remove indentation. + lines = [line[len(common_prefix) :] for line in lines] + + return "".join(lines) + + +def _common_whitespace_prefix(strings: Iterable[str]) -> str: + """ + Return common prefix for a list of lines. + This will ignore lines that contain whitespace only. + """ + # Ignore empty lines and lines that have whitespace only. + strings = [s for s in strings if not s.isspace() and not len(s) == 0] + + if not strings: + return "" + + else: + s1 = min(strings) + s2 = max(strings) + + for i, c in enumerate(s1): + if c != s2[i] or c not in " \t": + return s1[:i] + + return s1 -- cgit v1.2.3