1""" 2For internal use only. 3""" 4import re 5from typing import Callable, Iterable, Type, TypeVar, cast 6 7from prompt_toolkit.formatted_text import to_formatted_text 8from prompt_toolkit.formatted_text.utils import fragment_list_to_text 9from prompt_toolkit.mouse_events import MouseEvent, MouseEventType 10 11__all__ = [ 12 "has_unclosed_brackets", 13 "get_jedi_script_from_document", 14 "document_is_multiline_python", 15 "unindent_code", 16] 17 18 19def has_unclosed_brackets(text: str) -> bool: 20 """ 21 Starting at the end of the string. If we find an opening bracket 22 for which we didn't had a closing one yet, return True. 23 """ 24 stack = [] 25 26 # Ignore braces inside strings 27 text = re.sub(r"""('[^']*'|"[^"]*")""", "", text) # XXX: handle escaped quotes.! 28 29 for c in reversed(text): 30 if c in "])}": 31 stack.append(c) 32 33 elif c in "[({": 34 if stack: 35 if ( 36 (c == "[" and stack[-1] == "]") 37 or (c == "{" and stack[-1] == "}") 38 or (c == "(" and stack[-1] == ")") 39 ): 40 stack.pop() 41 else: 42 # Opening bracket for which we didn't had a closing one. 43 return True 44 45 return False 46 47 48def get_jedi_script_from_document(document, locals, globals): 49 import jedi # We keep this import in-line, to improve start-up time. 50 51 # Importing Jedi is 'slow'. 52 53 try: 54 return jedi.Interpreter( 55 document.text, 56 path="input-text", 57 namespaces=[locals, globals], 58 ) 59 except ValueError: 60 # Invalid cursor position. 61 # ValueError('`column` parameter is not in a valid range.') 62 return None 63 except AttributeError: 64 # Workaround for #65: https://github.com/jonathanslenders/python-prompt-toolkit/issues/65 65 # See also: https://github.com/davidhalter/jedi/issues/508 66 return None 67 except IndexError: 68 # Workaround Jedi issue #514: for https://github.com/davidhalter/jedi/issues/514 69 return None 70 except KeyError: 71 # Workaroud for a crash when the input is "u'", the start of a unicode string. 72 return None 73 except Exception: 74 # Workaround for: https://github.com/jonathanslenders/ptpython/issues/91 75 return None 76 77 78_multiline_string_delims = re.compile("""[']{3}|["]{3}""") 79 80 81def document_is_multiline_python(document): 82 """ 83 Determine whether this is a multiline Python document. 84 """ 85 86 def ends_in_multiline_string() -> bool: 87 """ 88 ``True`` if we're inside a multiline string at the end of the text. 89 """ 90 delims = _multiline_string_delims.findall(document.text) 91 opening = None 92 for delim in delims: 93 if opening is None: 94 opening = delim 95 elif delim == opening: 96 opening = None 97 return bool(opening) 98 99 if "\n" in document.text or ends_in_multiline_string(): 100 return True 101 102 def line_ends_with_colon() -> bool: 103 return document.current_line.rstrip()[-1:] == ":" 104 105 # If we just typed a colon, or still have open brackets, always insert a real newline. 106 if ( 107 line_ends_with_colon() 108 or ( 109 document.is_cursor_at_the_end 110 and has_unclosed_brackets(document.text_before_cursor) 111 ) 112 or document.text.startswith("@") 113 ): 114 return True 115 116 # If the character before the cursor is a backslash (line continuation 117 # char), insert a new line. 118 elif document.text_before_cursor[-1:] == "\\": 119 return True 120 121 return False 122 123 124_T = TypeVar("_T", bound=Callable[[MouseEvent], None]) 125 126 127def if_mousedown(handler: _T) -> _T: 128 """ 129 Decorator for mouse handlers. 130 Only handle event when the user pressed mouse down. 131 132 (When applied to a token list. Scroll events will bubble up and are handled 133 by the Window.) 134 """ 135 136 def handle_if_mouse_down(mouse_event: MouseEvent): 137 if mouse_event.event_type == MouseEventType.MOUSE_DOWN: 138 return handler(mouse_event) 139 else: 140 return NotImplemented 141 142 return cast(_T, handle_if_mouse_down) 143 144 145_T_type = TypeVar("_T_type", bound=Type) 146 147 148def ptrepr_to_repr(cls: _T_type) -> _T_type: 149 """ 150 Generate a normal `__repr__` method for classes that have a `__pt_repr__`. 151 """ 152 if not hasattr(cls, "__pt_repr__"): 153 raise TypeError( 154 "@ptrepr_to_repr can only be applied to classes that have a `__pt_repr__` method." 155 ) 156 157 def __repr__(self) -> str: 158 return fragment_list_to_text(to_formatted_text(cls.__pt_repr__(self))) 159 160 cls.__repr__ = __repr__ # type:ignore 161 return cls 162 163 164def unindent_code(text: str) -> str: 165 """ 166 Remove common leading whitespace when all lines are indented. 167 """ 168 lines = text.splitlines(keepends=True) 169 170 # Look for common prefix. 171 common_prefix = _common_whitespace_prefix(lines) 172 173 # Remove indentation. 174 lines = [line[len(common_prefix) :] for line in lines] 175 176 return "".join(lines) 177 178 179def _common_whitespace_prefix(strings: Iterable[str]) -> str: 180 """ 181 Return common prefix for a list of lines. 182 This will ignore lines that contain whitespace only. 183 """ 184 # Ignore empty lines and lines that have whitespace only. 185 strings = [s for s in strings if not s.isspace() and not len(s) == 0] 186 187 if not strings: 188 return "" 189 190 else: 191 s1 = min(strings) 192 s2 = max(strings) 193 194 for i, c in enumerate(s1): 195 if c != s2[i] or c not in " \t": 196 return s1[:i] 197 198 return s1 199