Tutorial: recipes, tips, and tricks
In what follows, I will attempt to demonstrate some useful ways to
use token-utils, focusing on those that make changes to
existing code. For simplicity, all the examples will assume
that I did from token_utils import *.
Adding a factorial symbol
The factorial function is included in the math module. In usual mathematical notation, we would write something like:
n! == n * (n-1) * .... * 2 * 1
Let’s see what tokens would represents a simple factorial:
>>> print_tokens("n!")
Extracting physical lines from source:
type=1 (NAME) string='n' start=(1, 0) end=(1, 1) line='n!'
type=60 (ERRORTOKEN) string='!' start=(1, 1) end=(1, 2) line='n!'
type=4 (NEWLINE) string='' start=(1, 2) end=(1, 3) line='n!'
--> line: 'n!'
type=0 (ENDMARKER) string='' start=(2, 0) end=(2, 0) line=''
--> line: ''
While there’s nothing wrong with ! being an ERRORTOKEN for us,
let’s nonetheless add it to the list of valid operators recognized by
Python. We will see a bit later why it is often very useful
to add symbols to the list of operators.
>>> add_operator("!", "exclamation")
True
>>> print_tokens("n!")
Extracting physical lines from source:
type=1 (NAME) string='n' start=(1, 0) end=(1, 1) line='n!'
type=54 (OP: EXCLAMATION) string='!' start=(1, 1) end=(1, 2) line='n!'
type=4 (NEWLINE) string='' start=(1, 2) end=(1, 3) line='n!'
--> line: 'n!'
type=0 (ENDMARKER) string='' start=(2, 0) end=(2, 0) line=''
--> line: ''
>>> tokens = tokenize("n!")
>>> tokens[1] == "!"
True
>>> tokens[1].is_operator()
True
We can see that, not only ! is a valid operator token, but it is also
recognized as such by the Token method is_operator().
The function add_operator() returned True. If we were to add
a string already known to be an operator, it would return False,
with no negative consequence.
While Python does not care about spaces between tokens, humans do. For the mathematically inclined, it wouldn’t be appropriate to write something like:
a = 3 !
expecting a to be now equal to 6. So, to define a factorial, we will
require ! to come immediately after either a variable name (identifier)
or an integer; by ‘immediately after’, we mean no space between the two
tokens.
Here is how we can do it, using token-utils’ immediately_after() instead
of immediately_before() as it looks more natural in this example.
# factorial_1.py
from token_utils import tokenize, untokenize, add_operator, pairwise
add_operator("!", "exclamation")
def make_factorial(source):
tokens = tokenize(source)
new_tokens = []
for token, next_ in pairwise(tokens):
if (
next_ == "!"
and token.is_immediately_before(next_)
and (token.is_integer() or token.is_identifier())
):
token.string = f"factorial({token.string})"
next_.string = ""
new_tokens.append(token)
return untokenize(new_tokens)
Before we show it in action, we should add a word about pairwise.
token-utils has its own implementation of Python’s itertools function pairwise
(available only for Python 3.10+). Given an iterable denoted by ABCDEF,
itertools’ pairwise will emit the items in pairs (tuples) like so:
AB BC CD DE EF
Notice how the last item is never emitted as the first item of a pair.
To “solve this problem”,
token-utils adds a fake token ($) to the iterable list so that each
token of the original sequence appears as a the first element of the pair:
AB BC CD DE EF F$
There is also an option to have this extra token at the beginning if it is more convenient to focus on the second element of a pair.
As for the rest of the algorithm, we assume that is should be straightforward
to understand given our description of
the problem, and with the corresponding method names
is_immediately_before(), is_identifier(), and is_integer().
The only subtlety is when we modify next_.string: we can do this as
we haven’t added this token to the new_tokens list.
Here’s a quick demonstration of our transformation:
>>> from factorial_1 import make_factorial
>>> make_factorial("3!")
'factorial(3)'
>>> make_factorial("a = 5!")
'a = factorial(5)'
>>> make_factorial("4 !") # not immediately before/after
'4 !'
Better factorial and BracketStack
Before we describe how to improve our factorial, we make a
brief detour to introduce BracketStack, a class that
keeps track of whether or not brackets, (), {}, [],
are closed or unclosed in a stream. When a closing bracket is added
as a token-utils Token, BracketStack will return
a corresponding opening bracket if is is found, and will
return False otherwise.
Here’s a sample session:
>>> stack = BracketStack()
>>> for token in generate_tokens(" ({[ ]}) []"):
... if token.is_bracket():
... other = stack.add(token)
... if other:
... print(other, token, stack.is_empty())
...
[ ] False
{ } False
( ) True
[ ] True
Going back to our factorial: in addition to appearing after
integers or variable names, the factorial symbol, !,
can appear after a closing parenthesis, ),
as in (n + 1)!, for example.
We can modify our make_factorial() function to include
such cases, again insisting that ! had to appear
immediately after a symbol, such as ), in order to
mean that it is meant to indicate that it is a factorial.
In addition to keeping track of open/close brackets, we have one more difficulty to consider. Imagine that we have something like the following:
A B C D E ( a b c )!
If we proceed to add tokens as we find them to a new_tokens list,
when we arrive at )!, we would have to go backwards, retrieve
all the previously added tokens (a b c), until we find the
right open parenthesis, do the appropriate modification and add back
the extracted material. This can actually be done easily using
the function split_at_token().
Here’s a modified make_factorial() that uses this.
# factorial_2.py
from token_utils import (
tokenize,
untokenize,
add_operator,
pairwise,
BracketStack,
split_at_token,
)
add_operator("!", "exclamation")
def make_factorial(source):
tokens = tokenize(source)
stack = BracketStack()
new_tokens = []
for token, next_ in pairwise(tokens):
if (
next_ == "!"
and token.is_immediately_before(next_)
and (token.is_integer() or token.is_identifier())
):
token.string = f"factorial({token.string})"
next_.string = ""
elif next_ == "!" and token.is_immediately_before(next_) and token == ")":
matching_bracket = stack.add(token)
next_.string = ""
new_tokens, remainder = split_at_token(new_tokens, matching_bracket)
matching_bracket.string = "factorial("
new_tokens.append(matching_bracket)
new_tokens.extend(remainder)
else:
if token.is_bracket():
stack.add(token)
new_tokens.append(token)
return untokenize(new_tokens)
Inside split_at_token, we make use of the is_identical() method
to ensure that we find the correct (. Note that we simply need to
modify matching_bracket.string from ( to factorial(.
Instead of simply showing the result of such transformations,
let me instead show it after evaluation by Python using
the ideas project I have already mentioned in which
this is done as an example:
> ideas -a factorial
The following initializing code has been executed in the Ideas console:
from math import factorial
Ideas Console version 0.3.12. [Python version: 3.11.9]
ideas> 3!
6
ideas> 3 + (4 + 7 - 3)!
40323
ideas> n = 8
ideas> (n + 1)!
362880
Keeping track of indentation with IndentStack
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 of keeping 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.
This allowed us to define the utility class IndentStack.