##// END OF EJS Templates
Prevent infinite loops in input transformation
Prevent infinite loops in input transformation

File last commit:

r24171:f3bd056d
r24371:b52ac3d4
Show More
inputtransforms.rst
167 lines | 6.6 KiB | text/x-rst | RstLexer

Custom input transformation

IPython extends Python syntax to allow things like magic commands, and help with the ? syntax. There are several ways to customise how the user's input is processed into Python code to be executed.

These hooks are mainly for other projects using IPython as the core of their interactive interface. Using them carelessly can easily break IPython!

String based transformations

When the user enters code, it is first processed as a string. By the end of this stage, it must be valid Python syntax.

String based transformations are managed by :class:`IPython.core.inputtransformer2.TransformerManager`, which is attached to the :class:`~IPython.core.interactiveshell.InteractiveShell` instance as input_transformer_manager. This passes the data through a series of individual transformers. There are two kinds of transformers stored in three groups:

  • cleanup_transforms and line_transforms are lists of functions. Each function is called with a list of input lines (which include trailing newlines), and they return a list in the same format. cleanup_transforms are run first; they strip prompts and leading indentation from input. The only default transform in line_transforms processes cell magics.
  • token_transformers is a list of :class:`IPython.core.inputtransformer2.TokenTransformBase` subclasses (not instances). They recognise special syntax like %line magics and help?, and transform them to Python syntax. The interface for these is more complex; see below.

These transformers may raise :exc:`SyntaxError` if the input code is invalid, but in most cases it is clearer to pass unrecognised code through unmodified and let Python's own parser decide whether it is valid.

Line based transformations

For example, imagine we want to obfuscate our code by reversing each line, so we'd write )5(f =+ a instead of a += f(5). Here's how we could swap it back the right way before IPython tries to run it:

def reverse_line_chars(lines):
    new_lines = []
    for line in lines:
        chars = line[:-1]  # the newline needs to stay at the end
        new_lines.append(chars[::-1] + '\n')
    return new_lines

To start using this:

ip = get_ipython()
ip.input_transformer_manager.line_transforms.append(reverse_line_chars)

Token based transformations

These recognise special syntax like %magics and help?, and transform it into valid Python code. Using tokens makes it easy to avoid transforming similar patterns inside comments or strings.

The API for a token-based transformation looks like this:

.. class:: MyTokenTransformer

Because each transformation may affect the parsing of the code after it, TransformerManager takes a careful approach. It calls find() on all available transformers. If any find a match, the transformation which matched closest to the start is run. Then it tokenises the transformed code again, and starts the process again. This continues until none of the transformers return a match. So it's important that the transformation removes the pattern which find() recognises, otherwise it will enter an infinite loop.

For example, here's a transformer which will recognise ¬ as a prefix for a new kind of special command:

import tokenize
from IPython.core.inputtransformer2 import TokenTransformBase

class MySpecialCommand(TokenTransformBase):
    @classmethod
    def find(cls, tokens_by_line):
        """Find the first escaped command (¬foo) in the cell.
        """
        for line in tokens_by_line:
            ix = 0
            # Find the first token that's not INDENT/DEDENT
            while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
                ix += 1
            if line[ix].string == '¬':
                return cls(line[ix].start)

    def transform(self, lines):
        indent  = lines[self.start_line][:self.start_col]
        content = lines[self.start_line][self.start_col+1:]

        lines_before = lines[:self.start_line]
        call = "specialcommand(%r)" % content
        new_line = indent + call + '\n'
        lines_after = lines[self.start_line + 1:]

        return lines_before + [new_line] + lines_after

And here's how you'd use it:

ip = get_ipython()
ip.input_transformer_manager.token_transformers.append(MySpecialCommand)

AST transformations

After the code has been parsed as Python syntax, you can use Python's powerful Abstract Syntax Tree tools to modify it. Subclass :class:`ast.NodeTransformer`, and add an instance to shell.ast_transformers.

This example wraps integer literals in an Integer class, which is useful for mathematical frameworks that want to handle e.g. 1/3 as a precise fraction:

class IntegerWrapper(ast.NodeTransformer):
    """Wraps all integers in a call to Integer()"""
    def visit_Num(self, node):
        if isinstance(node.n, int):
            return ast.Call(func=ast.Name(id='Integer', ctx=ast.Load()),
                            args=[node], keywords=[])
        return node