| | """Token-related utilities""" |
| |
|
| | |
| | |
| |
|
| | from collections import namedtuple |
| | from io import StringIO |
| | from keyword import iskeyword |
| |
|
| | import tokenize |
| | from tokenize import TokenInfo |
| | from typing import List, Optional |
| |
|
| |
|
| | Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line']) |
| |
|
| | def generate_tokens(readline): |
| | """wrap generate_tkens to catch EOF errors""" |
| | try: |
| | for token in tokenize.generate_tokens(readline): |
| | yield token |
| | except tokenize.TokenError: |
| | |
| | return |
| |
|
| |
|
| | def generate_tokens_catch_errors( |
| | readline, extra_errors_to_catch: Optional[List[str]] = None |
| | ): |
| | default_errors_to_catch = [ |
| | "unterminated string literal", |
| | "invalid non-printable character", |
| | "after line continuation character", |
| | ] |
| | assert extra_errors_to_catch is None or isinstance(extra_errors_to_catch, list) |
| | errors_to_catch = default_errors_to_catch + (extra_errors_to_catch or []) |
| |
|
| | tokens: List[TokenInfo] = [] |
| | try: |
| | for token in tokenize.generate_tokens(readline): |
| | tokens.append(token) |
| | yield token |
| | except tokenize.TokenError as exc: |
| | if any(error in exc.args[0] for error in errors_to_catch): |
| | if tokens: |
| | start = tokens[-1].start[0], tokens[-1].end[0] |
| | end = start |
| | line = tokens[-1].line |
| | else: |
| | start = end = (1, 0) |
| | line = "" |
| | yield tokenize.TokenInfo(tokenize.ERRORTOKEN, "", start, end, line) |
| | else: |
| | |
| | raise |
| |
|
| |
|
| | def line_at_cursor(cell, cursor_pos=0): |
| | """Return the line in a cell at a given cursor position |
| | |
| | Used for calling line-based APIs that don't support multi-line input, yet. |
| | |
| | Parameters |
| | ---------- |
| | cell : str |
| | multiline block of text |
| | cursor_pos : integer |
| | the cursor position |
| | |
| | Returns |
| | ------- |
| | (line, offset): (string, integer) |
| | The line with the current cursor, and the character offset of the start of the line. |
| | """ |
| | offset = 0 |
| | lines = cell.splitlines(True) |
| | for line in lines: |
| | next_offset = offset + len(line) |
| | if not line.endswith('\n'): |
| | |
| | |
| | |
| | next_offset += 1 |
| | if next_offset > cursor_pos: |
| | break |
| | offset = next_offset |
| | else: |
| | line = "" |
| | return (line, offset) |
| |
|
| |
|
| | def token_at_cursor(cell: str, cursor_pos: int = 0): |
| | """Get the token at a given cursor |
| | |
| | Used for introspection. |
| | |
| | Function calls are prioritized, so the token for the callable will be returned |
| | if the cursor is anywhere inside the call. |
| | |
| | Parameters |
| | ---------- |
| | cell : str |
| | A block of Python code |
| | cursor_pos : int |
| | The location of the cursor in the block where the token should be found |
| | """ |
| | names: List[str] = [] |
| | tokens: List[Token] = [] |
| | call_names = [] |
| | |
| | offsets = {1: 0} |
| | for tup in generate_tokens(StringIO(cell).readline): |
| | |
| | tok = Token(*tup) |
| | |
| | |
| | start_line, start_col = tok.start |
| | end_line, end_col = tok.end |
| | if end_line + 1 not in offsets: |
| | |
| | lines = tok.line.splitlines(True) |
| | for lineno, line in enumerate(lines, start_line + 1): |
| | if lineno not in offsets: |
| | offsets[lineno] = offsets[lineno-1] + len(line) |
| | |
| | offset = offsets[start_line] |
| | |
| | boundary = cursor_pos + 1 if start_col == 0 else cursor_pos |
| | if offset + start_col >= boundary: |
| | |
| | |
| | break |
| | |
| | if tok.token == tokenize.NAME and not iskeyword(tok.text): |
| | if names and tokens and tokens[-1].token == tokenize.OP and tokens[-1].text == '.': |
| | names[-1] = "%s.%s" % (names[-1], tok.text) |
| | else: |
| | names.append(tok.text) |
| | elif tok.token == tokenize.OP: |
| | if tok.text == '=' and names: |
| | |
| | names.pop(-1) |
| | if tok.text == '(' and names: |
| | |
| | call_names.append(names[-1]) |
| | elif tok.text == ')' and call_names: |
| | call_names.pop(-1) |
| | |
| | tokens.append(tok) |
| | |
| | if offsets[end_line] + end_col > cursor_pos: |
| | |
| | break |
| | |
| | if call_names: |
| | return call_names[-1] |
| | elif names: |
| | return names[-1] |
| | else: |
| | return '' |
| |
|