Source code for token_utils.utils

from collections import deque
from itertools import chain, islice

from token_utils.token_class import Token, make_fake_token, NULL


[docs] class BracketStack: """Helpful in keeping track of open and close brackets in a token sequence. It is intended to be strict, and only accept Token instances, with open brackets added before any close one can be added. """
[docs] def __init__(self): self.stack = []
[docs] def add(self, bracket): """Adds a bracket to a stack. If it is a closing bracket that matches the last added one, that last opening one is returned. If it is a close bracket not matching the last added one, or the first one added, a ``TypeError`` is raised. If an open bracket is added, the ``NULL`` token is returned. Note that the ``NULL`` token evaluates to ``False`` as a condition (e.g. ``if NULL == if False``.) """ if not isinstance(bracket, Token): raise TypeError("'bracket' parameter must be a Token.") if self.stack: if bracket.is_matching_bracket(self.stack[-1]): return self.stack.pop() elif not bracket.is_open_bracket(): raise TypeError( f"Close bracket does not match anything: {repr(bracket)}" ) else: self.stack.append(bracket) return False elif not bracket.is_open_bracket(): raise TypeError( f"First bracket added cannot be a closing one: {repr(bracket)}" ) else: self.stack.append(bracket) return False
[docs] def is_empty(self): """Return True if the stack is empty, False if it contains brackets.""" return not bool(self.stack)
[docs] class IndentStack: """Class designed to help keep track of indentation associated with the use of specific keywords.""" # In Python, the following keywords, or soft keywords, can signal the introduction # of an indented block:: # class, def, if/elif/else, for/else, try/except/else/finally, while/else, # with, match, case # For some code analysis or code modification, it might be very useful to track # of such keywords when they introduce a change in indentation. # When they do so, and ignoring end of line comments, # they will appear as the first non-space token # on a **logical line** terminated by a colon ``:``. # If we use token-utils' ``get_logical_lines()`` to analyze some source code, # the last token of each such line will always be a ``NEWLINE`` token, # preceded by a colon.
[docs] def __init__(self): self.stack = [] self.top_indent_keywords = [ "class", "def", "if", "for", "try", "while", "with", "match", "case", ] self.same_indent_keywords = [ "elif", "else", "except", "finally", ] self.indent_keywords = self.top_indent_keywords + self.same_indent_keywords
[docs] def add_top_indent_keyword(self, name): """Add a new top indent keyword, similar to class, def, try, etc. Useful when working with alternatives to standard Python syntax. """ self.top_indent_keywords.append(name) self.indent_keywords = self.top_indent_keywords + self.same_indent_keywords
[docs] def add_same_indent_keyword(self, name): """Add a new same indent keyword, similar to elif, else, except, etc. Useful when working with alternatives to standard Python syntax. """ self.same_indent_keywords.append(name) self.indent_keywords = self.top_indent_keywords + self.same_indent_keywords
[docs] def print_stack(self): """Useful for diagnostic""" print([(tok.string, tok.start) for tok in self.stack])
[docs] def top_item(self): """Returns the token at the top of the stack or a NULL token.""" if not self.stack: return NULL return self.stack[-1]
[docs] def update(self, line): """Receives a line as a list containing tokens and keeps track of the indentation of the code. A line will deem to signal the beginning of an indented block if it begins with an appropriate keyword (``class, def, if, else, ...``) **and** ends with a colon. For this reason, it is highly recommended to obtain lines using ``get_logical_lines()`` since lines obtained using ``get_physical_lines()`` may have their last token on a different physical line. If the line signals a change in indentation (indent or dedent), this function returns the last "top token" starting a line at that indentation. By "top token" we mean a token that can start a block such as ``if`` (for ``if/elif/else``), ``try``, etc. It returns the token ``NULL`` if there is no such token. """ if not (isinstance(line, list) or isinstance(line[0], Token)): raise TypeError( "'line' parameter must be a list containing 0 or more tokens" ) for token in line: if not isinstance(token, Token): raise TypeError("'line' parameter must be a list only Token instances") first_token = line[0] if not first_token.string.strip(): return NULL if first_token.is_comment(): return NULL # An indenting line will include at least a relevant keyword, # a colon and a NEWLINE token indenting_line = True if len(line) < 3: indenting_line = False elif not first_token.is_in(self.indent_keywords): indenting_line = False else: # line could start with a soft keyword colon = line[-2] if colon.is_comment(): colon = line[-3] if colon != ":": indenting_line = False # What about if we have a one-liner like: # def double(n): return n # While the colon will not have been found, such a line will NOT be the # beginning of an indented block. So, we only care to see if it signal # that we are leaving an indented block and dedenting the code if not indenting_line: # perhaps dedenting is occuring popped = 0 while self.stack: top_item = self.stack.pop() popped += 1 if top_item.start_col >= first_token.start_col: continue if (top_item.start_col == first_token.start_col) and first_token.is_in( self.top_indent_keywords ): # new block at same level self.stack.append(first_token) return first_token self.stack.append(top_item) if popped > 1: return top_item else: return NULL # no change: dedenting did not occur return NULL if not self.stack: self.stack.append(first_token) return first_token while True: top_item = self.stack.pop() # Indent: add popped item back and add new if top_item.start_col < first_token.start_col: self.stack.append(top_item) # add back self.stack.append(first_token) return first_token # No change: depends if token starts new block or not if top_item.start_col == first_token.start_col: if first_token.is_in(self.top_indent_keywords): # previous item at same level self.stack.append(first_token) return first_token else: self.stack.append(top_item) return top_item # Dedent; need to pop new item if possible if top_item.start_col > first_token.start_col: if not self.stack: self.stack.append(first_token) return first_token # new item at top continue
[docs] def is_token_in_named_block(self, token, name): """Returns True if the token is indented more than a token with the name given. Example usage: figuring out if a 'def' represents a method of a class, or a pure function definition. """ for tok in self.stack: if tok == name and tok.start_col < token.start_col: return True return False
[docs] def split_at_token(seq, token): """Split a list of tokens in two sublists: those occuring before the token specified, and those after.""" before = [] for index, tok in enumerate(seq): if tok.is_identical(token): break before.append(tok) else: raise ValueError("Token not found in the input sequence") after = seq[index + 1 :] return before, after
[docs] def pairwise(iterable, prev=0): """Similar to itertools.pairwise (Python 3.10+). However, it adds a fake token at the end if ``prev==0`` (the default) or at the beginning if ``prev==1``. Any other value will result in a ``ValueError`` Given a list of tokens represented by lower case letters, and a fake token by $, the default corresponds to something like:: pairwise('abcde') → ab bc cd de e$ """ if prev not in [0, 1]: raise ValueError("'prev' must be either 0 (the default) or 1.") iterator = iter(iterable) if prev: updated_iterator = chain([make_fake_token()], iterator) else: updated_iterator = chain(iterator, [make_fake_token()]) a = next(updated_iterator, None) for b in updated_iterator: yield a, b a = b
[docs] def sliding_window(iterable, n, prev=0): """Collect data into overlapping fixed-length chunks or blocks. This is inspired by an itertools recipe. Given an iterator, and a requested 'window' of size 'n', by default (prev=0), it adds 'n-1' fake token at the end and iterates, emitting 'n' items at a time until all the items have been served. Given a list of tokens represented by lower case letters, and $ representing a fake token, we would have something like: sliding_window('abcde', 3) → abc bcd cde deF e$$ With ``prev==1``, we would have 1 fake token prepended and one appended; thus sliding_window('abcde', 3, prev=1) → $ab abc bcd cde de$ We must have ``0 <= prev <= n-1``, otherwise a ValueError is raised We essentially have:: sliding_window(iterable, 2) == pairwise(iterator) """ if not 0 <= prev < n: raise ValueError(f"'prev' must be in the interval [0, {n}]") iterator = iter(iterable) fake = [make_fake_token(string=f"FAKE_{i}") for i in range(n - 1)] iterator = iter(iterable) updated_iterator = chain(fake[:prev], iterator, fake[prev : n - 1]) window = deque(islice(updated_iterator, n - 1), maxlen=n) for x in updated_iterator: window.append(x) yield tuple(window)