Format: text/x-python
⬇ Download File (5.0 KB)
   1  """Token-related utilities"""
   2  
   3  # Copyright (c) IPython Development Team.
   4  # Distributed under the terms of the Modified BSD License.
   5  
   6  from collections import namedtuple
   7  from io import StringIO
   8  from keyword import iskeyword
   9  
  10  import tokenize
  11  from tokenize import TokenInfo
  12  from typing import List, Optional
  13  
  14  
  15  Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line'])
  16  
  17  def generate_tokens(readline):
  18      """wrap generate_tkens to catch EOF errors"""
  19      try:
  20          for token in tokenize.generate_tokens(readline):
  21              yield token
  22      except tokenize.TokenError:
  23          # catch EOF error
  24          return
  25  
  26  
  27  def generate_tokens_catch_errors(
  28      readline, extra_errors_to_catch: Optional[List[str]] = None
  29  ):
  30      default_errors_to_catch = [
  31          "unterminated string literal",
  32          "invalid non-printable character",
  33          "after line continuation character",
  34      ]
  35      assert extra_errors_to_catch is None or isinstance(extra_errors_to_catch, list)
  36      errors_to_catch = default_errors_to_catch + (extra_errors_to_catch or [])
  37  
  38      tokens: List[TokenInfo] = []
  39      try:
  40          for token in tokenize.generate_tokens(readline):
  41              tokens.append(token)
  42              yield token
  43      except tokenize.TokenError as exc:
  44          if any(error in exc.args[0] for error in errors_to_catch):
  45              if tokens:
  46                  start = tokens[-1].start[0], tokens[-1].end[0]
  47                  end = start
  48                  line = tokens[-1].line
  49              else:
  50                  start = end = (1, 0)
  51                  line = ""
  52              yield tokenize.TokenInfo(tokenize.ERRORTOKEN, "", start, end, line)
  53          else:
  54              # Catch EOF
  55              raise
  56  
  57  
  58  def line_at_cursor(cell, cursor_pos=0):
  59      """Return the line in a cell at a given cursor position
  60  
  61      Used for calling line-based APIs that don't support multi-line input, yet.
  62  
  63      Parameters
  64      ----------
  65      cell : str
  66          multiline block of text
  67      cursor_pos : integer
  68          the cursor position
  69  
  70      Returns
  71      -------
  72      (line, offset): (string, integer)
  73          The line with the current cursor, and the character offset of the start of the line.
  74      """
  75      offset = 0
  76      lines = cell.splitlines(True)
  77      for line in lines:
  78          next_offset = offset + len(line)
  79          if not line.endswith('\n'):
  80              # If the last line doesn't have a trailing newline, treat it as if
  81              # it does so that the cursor at the end of the line still counts
  82              # as being on that line.
  83              next_offset += 1
  84          if next_offset > cursor_pos:
  85              break
  86          offset = next_offset
  87      else:
  88          line = ""
  89      return (line, offset)
  90  
  91  
  92  def token_at_cursor(cell: str, cursor_pos: int = 0):
  93      """Get the token at a given cursor
  94  
  95      Used for introspection.
  96  
  97      Function calls are prioritized, so the token for the callable will be returned
  98      if the cursor is anywhere inside the call.
  99  
 100      Parameters
 101      ----------
 102      cell : str
 103          A block of Python code
 104      cursor_pos : int
 105          The location of the cursor in the block where the token should be found
 106      """
 107      names: List[str] = []
 108      tokens: List[Token] = []
 109      call_names = []
 110      
 111      offsets = {1: 0} # lines start at 1
 112      for tup in generate_tokens(StringIO(cell).readline):
 113          
 114          tok = Token(*tup)
 115          
 116          # token, text, start, end, line = tup
 117          start_line, start_col = tok.start
 118          end_line, end_col = tok.end
 119          if end_line + 1 not in offsets:
 120              # keep track of offsets for each line
 121              lines = tok.line.splitlines(True)
 122              for lineno, line in enumerate(lines, start_line + 1):
 123                  if lineno not in offsets:
 124                      offsets[lineno] = offsets[lineno-1] + len(line)
 125          
 126          offset = offsets[start_line]
 127          # allow '|foo' to find 'foo' at the beginning of a line
 128          boundary = cursor_pos + 1 if start_col == 0 else cursor_pos
 129          if offset + start_col >= boundary:
 130              # current token starts after the cursor,
 131              # don't consume it
 132              break
 133          
 134          if tok.token == tokenize.NAME and not iskeyword(tok.text):
 135              if names and tokens and tokens[-1].token == tokenize.OP and tokens[-1].text == '.':
 136                  names[-1] = "%s.%s" % (names[-1], tok.text)
 137              else:
 138                  names.append(tok.text)
 139          elif tok.token == tokenize.OP:
 140              if tok.text == '=' and names:
 141                  # don't inspect the lhs of an assignment
 142                  names.pop(-1)
 143              if tok.text == '(' and names:
 144                  # if we are inside a function call, inspect the function
 145                  call_names.append(names[-1])
 146              elif tok.text == ')' and call_names:
 147                  call_names.pop(-1)
 148          
 149          tokens.append(tok)
 150          
 151          if offsets[end_line] + end_col > cursor_pos:
 152              # we found the cursor, stop reading
 153              break
 154          
 155      if call_names:
 156          return call_names[-1]
 157      elif names:
 158          return names[-1]
 159      else:
 160          return ''
 161