##// END OF EJS Templates
Simpler mechanism to extend input transformations
Thomas Kluyver -
Show More
@@ -339,6 +339,15 b' class InteractiveShell(SingletonConfigurable):'
339 ())
339 ())
340
340
341 @property
341 @property
342 def input_transformers_cleanup(self):
343 return self.input_transformer_manager.cleanup_transforms
344
345 input_transformers_post = List([],
346 help="A list of string input transformers, to be applied after IPython's "
347 "own input transformations."
348 )
349
350 @property
342 def input_splitter(self):
351 def input_splitter(self):
343 """Make this available for compatibility
352 """Make this available for compatibility
344
353
@@ -2717,21 +2726,10 b' class InteractiveShell(SingletonConfigurable):'
2717 preprocessing_exc_tuple = None
2726 preprocessing_exc_tuple = None
2718 try:
2727 try:
2719 # Static input transformations
2728 # Static input transformations
2720 cell = self.input_transformer_manager.transform_cell(raw_cell)
2729 cell = self.transform_cell(raw_cell)
2721 except SyntaxError:
2730 except Exception:
2722 preprocessing_exc_tuple = sys.exc_info()
2731 preprocessing_exc_tuple = sys.exc_info()
2723 cell = raw_cell # cell has to exist so it can be stored/logged
2732 cell = raw_cell # cell has to exist so it can be stored/logged
2724 else:
2725 if len(cell.splitlines()) == 1:
2726 # Dynamic transformations - only applied for single line commands
2727 with self.builtin_trap:
2728 try:
2729 # use prefilter_lines to handle trailing newlines
2730 # restore trailing newline for ast.parse
2731 cell = self.prefilter_manager.prefilter_lines(cell) + '\n'
2732 except Exception:
2733 # don't allow prefilter errors to crash IPython
2734 preprocessing_exc_tuple = sys.exc_info()
2735
2733
2736 # Store raw and processed history
2734 # Store raw and processed history
2737 if store_history:
2735 if store_history:
@@ -2802,6 +2800,24 b' class InteractiveShell(SingletonConfigurable):'
2802 self.execution_count += 1
2800 self.execution_count += 1
2803
2801
2804 return result
2802 return result
2803
2804 def transform_cell(self, raw_cell):
2805 # Static input transformations
2806 cell = self.input_transformer_manager.transform_cell(raw_cell)
2807
2808 if len(cell.splitlines()) == 1:
2809 # Dynamic transformations - only applied for single line commands
2810 with self.builtin_trap:
2811 # use prefilter_lines to handle trailing newlines
2812 # restore trailing newline for ast.parse
2813 cell = self.prefilter_manager.prefilter_lines(cell) + '\n'
2814
2815 lines = cell.splitlines(keepends=True)
2816 for transform in self.input_transformers_post:
2817 lines = transform(lines)
2818 cell = ''.join(lines)
2819
2820 return cell
2805
2821
2806 def transform_ast(self, node):
2822 def transform_ast(self, node):
2807 """Apply the AST transformations from self.ast_transformers
2823 """Apply the AST transformations from self.ast_transformers
@@ -24,34 +24,28 b' end of this stage, it must be valid Python syntax.'
24 redesigned. Any third party code extending input transformation will need to
24 redesigned. Any third party code extending input transformation will need to
25 be rewritten. The new API is, hopefully, simpler.
25 be rewritten. The new API is, hopefully, simpler.
26
26
27 String based transformations are managed by
27 String based transformations are functions which accept a list of strings:
28 :class:`IPython.core.inputtransformer2.TransformerManager`, which is attached to
28 each string is a single line of the input cell, including its line ending.
29 the :class:`~IPython.core.interactiveshell.InteractiveShell` instance as
29 The transformation function should return output in the same structure.
30 ``input_transformer_manager``. This passes the
30
31 data through a series of individual transformers. There are two kinds of
31 These transformations are in two groups, accessible as attributes of
32 transformers stored in three groups:
32 the :class:`~IPython.core.interactiveshell.InteractiveShell` instance.
33
33 Each group is a list of transformation functions.
34 * ``cleanup_transforms`` and ``line_transforms`` are lists of functions. Each
34
35 function is called with a list of input lines (which include trailing
35 * ``input_transformers_cleanup`` run first on input, to do things like stripping
36 newlines), and they return a list in the same format. ``cleanup_transforms``
36 prompts and leading indents from copied code. It may not be possible at this
37 are run first; they strip prompts and leading indentation from input.
37 stage to parse the input as valid Python code.
38 The only default transform in ``line_transforms`` processes cell magics.
38 * Then IPython runs its own transformations to handle its special syntax, like
39 * ``token_transformers`` is a list of :class:`IPython.core.inputtransformer2.TokenTransformBase`
39 ``%magics`` and ``!system`` commands. This part does not expose extension
40 subclasses (not instances). They recognise special syntax like
40 points.
41 ``%line magics`` and ``help?``, and transform them to Python syntax. The
41 * ``input_transformers_post`` run as the last step, to do things like converting
42 interface for these is more complex; see below.
42 float literals into decimal objects. These may attempt to parse the input as
43 Python code.
43
44
44 These transformers may raise :exc:`SyntaxError` if the input code is invalid, but
45 These transformers may raise :exc:`SyntaxError` if the input code is invalid, but
45 in most cases it is clearer to pass unrecognised code through unmodified and let
46 in most cases it is clearer to pass unrecognised code through unmodified and let
46 Python's own parser decide whether it is valid.
47 Python's own parser decide whether it is valid.
47
48
48 .. versionchanged:: 2.0
49
50 Added the option to raise :exc:`SyntaxError`.
51
52 Line based transformations
53 --------------------------
54
55 For example, imagine we want to obfuscate our code by reversing each line, so
49 For example, imagine we want to obfuscate our code by reversing each line, so
56 we'd write ``)5(f =+ a`` instead of ``a += f(5)``. Here's how we could swap it
50 we'd write ``)5(f =+ a`` instead of ``a += f(5)``. Here's how we could swap it
57 back the right way before IPython tries to run it::
51 back the right way before IPython tries to run it::
@@ -66,86 +60,7 b' back the right way before IPython tries to run it::'
66 To start using this::
60 To start using this::
67
61
68 ip = get_ipython()
62 ip = get_ipython()
69 ip.input_transformer_manager.line_transforms.append(reverse_line_chars)
63 ip.input_transformers_cleanup.append(reverse_line_chars)
70
71 Token based transformations
72 ---------------------------
73
74 These recognise special syntax like ``%magics`` and ``help?``, and transform it
75 into valid Python code. Using tokens makes it easy to avoid transforming similar
76 patterns inside comments or strings.
77
78 The API for a token-based transformation looks like this::
79
80 .. class:: MyTokenTransformer
81
82 .. classmethod:: find(tokens_by_line)
83
84 Takes a list of lists of :class:`tokenize.TokenInfo` objects. Each sublist
85 is the tokens from one Python line, which may span several physical lines,
86 because of line continuations, multiline strings or expressions. If it
87 finds a pattern to transform, it returns an instance of the class.
88 Otherwise, it returns None.
89
90 .. attribute:: start_lineno
91 start_col
92 priority
93
94 These attributes are used to select which transformation to run first.
95 ``start_lineno`` is 0-indexed (whereas the locations on
96 :class:`~tokenize.TokenInfo` use 1-indexed line numbers). If there are
97 multiple matches in the same location, the one with the smaller
98 ``priority`` number is used.
99
100 .. method:: transform(lines)
101
102 This should transform the individual recognised pattern that was
103 previously found. As with line-based transforms, it takes a list of
104 lines as strings, and returns a similar list.
105
106 Because each transformation may affect the parsing of the code after it,
107 ``TransformerManager`` takes a careful approach. It calls ``find()`` on all
108 available transformers. If any find a match, the transformation which matched
109 closest to the start is run. Then it tokenises the transformed code again,
110 and starts the process again. This continues until none of the transformers
111 return a match. So it's important that the transformation removes the pattern
112 which ``find()`` recognises, otherwise it will enter an infinite loop.
113
114 For example, here's a transformer which will recognise ``¬`` as a prefix for a
115 new kind of special command::
116
117 import tokenize
118 from IPython.core.inputtransformer2 import TokenTransformBase
119
120 class MySpecialCommand(TokenTransformBase):
121 @classmethod
122 def find(cls, tokens_by_line):
123 """Find the first escaped command (¬foo) in the cell.
124 """
125 for line in tokens_by_line:
126 ix = 0
127 # Find the first token that's not INDENT/DEDENT
128 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
129 ix += 1
130 if line[ix].string == '¬':
131 return cls(line[ix].start)
132
133 def transform(self, lines):
134 indent = lines[self.start_line][:self.start_col]
135 content = lines[self.start_line][self.start_col+1:]
136
137 lines_before = lines[:self.start_line]
138 call = "specialcommand(%r)" % content
139 new_line = indent + call + '\n'
140 lines_after = lines[self.start_line + 1:]
141
142 return lines_before + [new_line] + lines_after
143
144 And here's how you'd use it::
145
146 ip = get_ipython()
147 ip.input_transformer_manager.token_transformers.append(MySpecialCommand)
148
149
64
150 AST transformations
65 AST transformations
151 ===================
66 ===================
General Comments 0
You need to be logged in to leave comments. Login now