##// END OF EJS Templates
Add description of priority system
Thomas Kluyver -
Show More
@@ -1,634 +1,638 b''
1 """Input transformer machinery to support IPython special syntax.
1 """Input transformer machinery to support IPython special syntax.
2
2
3 This includes the machinery to recognise and transform ``%magic`` commands,
3 This includes the machinery to recognise and transform ``%magic`` commands,
4 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
4 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
5
5
6 Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
6 Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
7 deprecated in 7.0.
7 deprecated in 7.0.
8 """
8 """
9
9
10 # Copyright (c) IPython Development Team.
10 # Copyright (c) IPython Development Team.
11 # Distributed under the terms of the Modified BSD License.
11 # Distributed under the terms of the Modified BSD License.
12
12
13 from codeop import compile_command
13 from codeop import compile_command
14 import re
14 import re
15 import tokenize
15 import tokenize
16 from typing import List, Tuple
16 from typing import List, Tuple
17 import warnings
17 import warnings
18
18
19 _indent_re = re.compile(r'^[ \t]+')
19 _indent_re = re.compile(r'^[ \t]+')
20
20
21 def leading_indent(lines):
21 def leading_indent(lines):
22 """Remove leading indentation.
22 """Remove leading indentation.
23
23
24 If the first line starts with a spaces or tabs, the same whitespace will be
24 If the first line starts with a spaces or tabs, the same whitespace will be
25 removed from each following line in the cell.
25 removed from each following line in the cell.
26 """
26 """
27 m = _indent_re.match(lines[0])
27 m = _indent_re.match(lines[0])
28 if not m:
28 if not m:
29 return lines
29 return lines
30 space = m.group(0)
30 space = m.group(0)
31 n = len(space)
31 n = len(space)
32 return [l[n:] if l.startswith(space) else l
32 return [l[n:] if l.startswith(space) else l
33 for l in lines]
33 for l in lines]
34
34
35 class PromptStripper:
35 class PromptStripper:
36 """Remove matching input prompts from a block of input.
36 """Remove matching input prompts from a block of input.
37
37
38 Parameters
38 Parameters
39 ----------
39 ----------
40 prompt_re : regular expression
40 prompt_re : regular expression
41 A regular expression matching any input prompt (including continuation,
41 A regular expression matching any input prompt (including continuation,
42 e.g. ``...``)
42 e.g. ``...``)
43 initial_re : regular expression, optional
43 initial_re : regular expression, optional
44 A regular expression matching only the initial prompt, but not continuation.
44 A regular expression matching only the initial prompt, but not continuation.
45 If no initial expression is given, prompt_re will be used everywhere.
45 If no initial expression is given, prompt_re will be used everywhere.
46 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
46 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
47 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
47 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
48
48
49 If initial_re and prompt_re differ,
49 If initial_re and prompt_re differ,
50 only initial_re will be tested against the first line.
50 only initial_re will be tested against the first line.
51 If any prompt is found on the first two lines,
51 If any prompt is found on the first two lines,
52 prompts will be stripped from the rest of the block.
52 prompts will be stripped from the rest of the block.
53 """
53 """
54 def __init__(self, prompt_re, initial_re=None):
54 def __init__(self, prompt_re, initial_re=None):
55 self.prompt_re = prompt_re
55 self.prompt_re = prompt_re
56 self.initial_re = initial_re or prompt_re
56 self.initial_re = initial_re or prompt_re
57
57
58 def _strip(self, lines):
58 def _strip(self, lines):
59 return [self.prompt_re.sub('', l, count=1) for l in lines]
59 return [self.prompt_re.sub('', l, count=1) for l in lines]
60
60
61 def __call__(self, lines):
61 def __call__(self, lines):
62 if self.initial_re.match(lines[0]) or \
62 if self.initial_re.match(lines[0]) or \
63 (len(lines) > 1 and self.prompt_re.match(lines[1])):
63 (len(lines) > 1 and self.prompt_re.match(lines[1])):
64 return self._strip(lines)
64 return self._strip(lines)
65 return lines
65 return lines
66
66
67 classic_prompt = PromptStripper(
67 classic_prompt = PromptStripper(
68 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
68 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
69 initial_re=re.compile(r'^>>>( |$)')
69 initial_re=re.compile(r'^>>>( |$)')
70 )
70 )
71
71
72 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
72 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
73
73
74 def cell_magic(lines):
74 def cell_magic(lines):
75 if not lines[0].startswith('%%'):
75 if not lines[0].startswith('%%'):
76 return lines
76 return lines
77 if re.match('%%\w+\?', lines[0]):
77 if re.match('%%\w+\?', lines[0]):
78 # This case will be handled by help_end
78 # This case will be handled by help_end
79 return lines
79 return lines
80 magic_name, _, first_line = lines[0][2:-1].partition(' ')
80 magic_name, _, first_line = lines[0][2:-1].partition(' ')
81 body = ''.join(lines[1:])
81 body = ''.join(lines[1:])
82 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
82 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
83 % (magic_name, first_line, body)]
83 % (magic_name, first_line, body)]
84
84
85
85
86 def _find_assign_op(token_line):
86 def _find_assign_op(token_line):
87 """Get the index of the first assignment in the line ('=' not inside brackets)
87 """Get the index of the first assignment in the line ('=' not inside brackets)
88
88
89 Note: We don't try to support multiple special assignment (a = b = %foo)
89 Note: We don't try to support multiple special assignment (a = b = %foo)
90 """
90 """
91 paren_level = 0
91 paren_level = 0
92 for i, ti in enumerate(token_line):
92 for i, ti in enumerate(token_line):
93 s = ti.string
93 s = ti.string
94 if s == '=' and paren_level == 0:
94 if s == '=' and paren_level == 0:
95 return i
95 return i
96 if s in '([{':
96 if s in '([{':
97 paren_level += 1
97 paren_level += 1
98 elif s in ')]}':
98 elif s in ')]}':
99 if paren_level > 0:
99 if paren_level > 0:
100 paren_level -= 1
100 paren_level -= 1
101
101
102 def find_end_of_continued_line(lines, start_line: int):
102 def find_end_of_continued_line(lines, start_line: int):
103 """Find the last line of a line explicitly extended using backslashes.
103 """Find the last line of a line explicitly extended using backslashes.
104
104
105 Uses 0-indexed line numbers.
105 Uses 0-indexed line numbers.
106 """
106 """
107 end_line = start_line
107 end_line = start_line
108 while lines[end_line].endswith('\\\n'):
108 while lines[end_line].endswith('\\\n'):
109 end_line += 1
109 end_line += 1
110 if end_line >= len(lines):
110 if end_line >= len(lines):
111 break
111 break
112 return end_line
112 return end_line
113
113
114 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
114 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
115 """Assemble a single line from multiple continued line pieces
115 """Assemble a single line from multiple continued line pieces
116
116
117 Continued lines are lines ending in ``\``, and the line following the last
117 Continued lines are lines ending in ``\``, and the line following the last
118 ``\`` in the block.
118 ``\`` in the block.
119
119
120 For example, this code continues over multiple lines::
120 For example, this code continues over multiple lines::
121
121
122 if (assign_ix is not None) \
122 if (assign_ix is not None) \
123 and (len(line) >= assign_ix + 2) \
123 and (len(line) >= assign_ix + 2) \
124 and (line[assign_ix+1].string == '%') \
124 and (line[assign_ix+1].string == '%') \
125 and (line[assign_ix+2].type == tokenize.NAME):
125 and (line[assign_ix+2].type == tokenize.NAME):
126
126
127 This statement contains four continued line pieces.
127 This statement contains four continued line pieces.
128 Assembling these pieces into a single line would give::
128 Assembling these pieces into a single line would give::
129
129
130 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
130 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
131
131
132 This uses 0-indexed line numbers. *start* is (lineno, colno).
132 This uses 0-indexed line numbers. *start* is (lineno, colno).
133
133
134 Used to allow ``%magic`` and ``!system`` commands to be continued over
134 Used to allow ``%magic`` and ``!system`` commands to be continued over
135 multiple lines.
135 multiple lines.
136 """
136 """
137 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
137 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
138 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
138 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
139 + [parts[-1][:-1]]) # Strip newline from last line
139 + [parts[-1][:-1]]) # Strip newline from last line
140
140
141 class TokenTransformBase:
141 class TokenTransformBase:
142 """Base class for transformations which examine tokens.
142 """Base class for transformations which examine tokens.
143
143
144 Special syntax should not be transformed when it occurs inside strings or
144 Special syntax should not be transformed when it occurs inside strings or
145 comments. This is hard to reliably avoid with regexes. The solution is to
145 comments. This is hard to reliably avoid with regexes. The solution is to
146 tokenise the code as Python, and recognise the special syntax in the tokens.
146 tokenise the code as Python, and recognise the special syntax in the tokens.
147
147
148 IPython's special syntax is not valid Python syntax, so tokenising may go
148 IPython's special syntax is not valid Python syntax, so tokenising may go
149 wrong after the special syntax starts. These classes therefore find and
149 wrong after the special syntax starts. These classes therefore find and
150 transform *one* instance of special syntax at a time into regular Python
150 transform *one* instance of special syntax at a time into regular Python
151 syntax. After each transformation, tokens are regenerated to find the next
151 syntax. After each transformation, tokens are regenerated to find the next
152 piece of special syntax.
152 piece of special syntax.
153
153
154 Subclasses need to implement one class method (find)
154 Subclasses need to implement one class method (find)
155 and one regular method (transform).
155 and one regular method (transform).
156
157 The priority attribute can select which transformation to apply if multiple
158 transformers match in the same place. Lower numbers have higher priority.
159 This allows "%magic?" to be turned into a help call rather than a magic call.
156 """
160 """
157 # Lower numbers -> higher priority (for matches in the same location)
161 # Lower numbers -> higher priority (for matches in the same location)
158 priority = 10
162 priority = 10
159
163
160 def sortby(self):
164 def sortby(self):
161 return self.start_line, self.start_col, self.priority
165 return self.start_line, self.start_col, self.priority
162
166
163 def __init__(self, start):
167 def __init__(self, start):
164 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
168 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
165 self.start_col = start[1]
169 self.start_col = start[1]
166
170
167 @classmethod
171 @classmethod
168 def find(cls, tokens_by_line):
172 def find(cls, tokens_by_line):
169 """Find one instance of special syntax in the provided tokens.
173 """Find one instance of special syntax in the provided tokens.
170
174
171 Tokens are grouped into logical lines for convenience,
175 Tokens are grouped into logical lines for convenience,
172 so it is easy to e.g. look at the first token of each line.
176 so it is easy to e.g. look at the first token of each line.
173 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
177 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
174
178
175 This should return an instance of its class, pointing to the start
179 This should return an instance of its class, pointing to the start
176 position it has found, or None if it found no match.
180 position it has found, or None if it found no match.
177 """
181 """
178 raise NotImplementedError
182 raise NotImplementedError
179
183
180 def transform(self, lines: List[str]):
184 def transform(self, lines: List[str]):
181 """Transform one instance of special syntax found by ``find()``
185 """Transform one instance of special syntax found by ``find()``
182
186
183 Takes a list of strings representing physical lines,
187 Takes a list of strings representing physical lines,
184 returns a similar list of transformed lines.
188 returns a similar list of transformed lines.
185 """
189 """
186 raise NotImplementedError
190 raise NotImplementedError
187
191
188 class MagicAssign(TokenTransformBase):
192 class MagicAssign(TokenTransformBase):
189 """Transformer for assignments from magics (a = %foo)"""
193 """Transformer for assignments from magics (a = %foo)"""
190 @classmethod
194 @classmethod
191 def find(cls, tokens_by_line):
195 def find(cls, tokens_by_line):
192 """Find the first magic assignment (a = %foo) in the cell.
196 """Find the first magic assignment (a = %foo) in the cell.
193 """
197 """
194 for line in tokens_by_line:
198 for line in tokens_by_line:
195 assign_ix = _find_assign_op(line)
199 assign_ix = _find_assign_op(line)
196 if (assign_ix is not None) \
200 if (assign_ix is not None) \
197 and (len(line) >= assign_ix + 2) \
201 and (len(line) >= assign_ix + 2) \
198 and (line[assign_ix+1].string == '%') \
202 and (line[assign_ix+1].string == '%') \
199 and (line[assign_ix+2].type == tokenize.NAME):
203 and (line[assign_ix+2].type == tokenize.NAME):
200 return cls(line[assign_ix+1].start)
204 return cls(line[assign_ix+1].start)
201
205
202 def transform(self, lines: List[str]):
206 def transform(self, lines: List[str]):
203 """Transform a magic assignment found by the ``find()`` classmethod.
207 """Transform a magic assignment found by the ``find()`` classmethod.
204 """
208 """
205 start_line, start_col = self.start_line, self.start_col
209 start_line, start_col = self.start_line, self.start_col
206 lhs = lines[start_line][:start_col]
210 lhs = lines[start_line][:start_col]
207 end_line = find_end_of_continued_line(lines, start_line)
211 end_line = find_end_of_continued_line(lines, start_line)
208 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
212 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
209 assert rhs.startswith('%'), rhs
213 assert rhs.startswith('%'), rhs
210 magic_name, _, args = rhs[1:].partition(' ')
214 magic_name, _, args = rhs[1:].partition(' ')
211
215
212 lines_before = lines[:start_line]
216 lines_before = lines[:start_line]
213 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
217 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
214 new_line = lhs + call + '\n'
218 new_line = lhs + call + '\n'
215 lines_after = lines[end_line+1:]
219 lines_after = lines[end_line+1:]
216
220
217 return lines_before + [new_line] + lines_after
221 return lines_before + [new_line] + lines_after
218
222
219
223
220 class SystemAssign(TokenTransformBase):
224 class SystemAssign(TokenTransformBase):
221 """Transformer for assignments from system commands (a = !foo)"""
225 """Transformer for assignments from system commands (a = !foo)"""
222 @classmethod
226 @classmethod
223 def find(cls, tokens_by_line):
227 def find(cls, tokens_by_line):
224 """Find the first system assignment (a = !foo) in the cell.
228 """Find the first system assignment (a = !foo) in the cell.
225 """
229 """
226 for line in tokens_by_line:
230 for line in tokens_by_line:
227 assign_ix = _find_assign_op(line)
231 assign_ix = _find_assign_op(line)
228 if (assign_ix is not None) \
232 if (assign_ix is not None) \
229 and (len(line) >= assign_ix + 2) \
233 and (len(line) >= assign_ix + 2) \
230 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
234 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
231 ix = assign_ix + 1
235 ix = assign_ix + 1
232
236
233 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
237 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
234 if line[ix].string == '!':
238 if line[ix].string == '!':
235 return cls(line[ix].start)
239 return cls(line[ix].start)
236 elif not line[ix].string.isspace():
240 elif not line[ix].string.isspace():
237 break
241 break
238 ix += 1
242 ix += 1
239
243
240 def transform(self, lines: List[str]):
244 def transform(self, lines: List[str]):
241 """Transform a system assignment found by the ``find()`` classmethod.
245 """Transform a system assignment found by the ``find()`` classmethod.
242 """
246 """
243 start_line, start_col = self.start_line, self.start_col
247 start_line, start_col = self.start_line, self.start_col
244
248
245 lhs = lines[start_line][:start_col]
249 lhs = lines[start_line][:start_col]
246 end_line = find_end_of_continued_line(lines, start_line)
250 end_line = find_end_of_continued_line(lines, start_line)
247 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
251 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
248 assert rhs.startswith('!'), rhs
252 assert rhs.startswith('!'), rhs
249 cmd = rhs[1:]
253 cmd = rhs[1:]
250
254
251 lines_before = lines[:start_line]
255 lines_before = lines[:start_line]
252 call = "get_ipython().getoutput({!r})".format(cmd)
256 call = "get_ipython().getoutput({!r})".format(cmd)
253 new_line = lhs + call + '\n'
257 new_line = lhs + call + '\n'
254 lines_after = lines[end_line + 1:]
258 lines_after = lines[end_line + 1:]
255
259
256 return lines_before + [new_line] + lines_after
260 return lines_before + [new_line] + lines_after
257
261
258 # The escape sequences that define the syntax transformations IPython will
262 # The escape sequences that define the syntax transformations IPython will
259 # apply to user input. These can NOT be just changed here: many regular
263 # apply to user input. These can NOT be just changed here: many regular
260 # expressions and other parts of the code may use their hardcoded values, and
264 # expressions and other parts of the code may use their hardcoded values, and
261 # for all intents and purposes they constitute the 'IPython syntax', so they
265 # for all intents and purposes they constitute the 'IPython syntax', so they
262 # should be considered fixed.
266 # should be considered fixed.
263
267
264 ESC_SHELL = '!' # Send line to underlying system shell
268 ESC_SHELL = '!' # Send line to underlying system shell
265 ESC_SH_CAP = '!!' # Send line to system shell and capture output
269 ESC_SH_CAP = '!!' # Send line to system shell and capture output
266 ESC_HELP = '?' # Find information about object
270 ESC_HELP = '?' # Find information about object
267 ESC_HELP2 = '??' # Find extra-detailed information about object
271 ESC_HELP2 = '??' # Find extra-detailed information about object
268 ESC_MAGIC = '%' # Call magic function
272 ESC_MAGIC = '%' # Call magic function
269 ESC_MAGIC2 = '%%' # Call cell-magic function
273 ESC_MAGIC2 = '%%' # Call cell-magic function
270 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
274 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
271 ESC_QUOTE2 = ';' # Quote all args as a single string, call
275 ESC_QUOTE2 = ';' # Quote all args as a single string, call
272 ESC_PAREN = '/' # Call first argument with rest of line as arguments
276 ESC_PAREN = '/' # Call first argument with rest of line as arguments
273
277
274 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
278 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
275 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
279 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
276
280
277 def _make_help_call(target, esc, next_input=None):
281 def _make_help_call(target, esc, next_input=None):
278 """Prepares a pinfo(2)/psearch call from a target name and the escape
282 """Prepares a pinfo(2)/psearch call from a target name and the escape
279 (i.e. ? or ??)"""
283 (i.e. ? or ??)"""
280 method = 'pinfo2' if esc == '??' \
284 method = 'pinfo2' if esc == '??' \
281 else 'psearch' if '*' in target \
285 else 'psearch' if '*' in target \
282 else 'pinfo'
286 else 'pinfo'
283 arg = " ".join([method, target])
287 arg = " ".join([method, target])
284 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
288 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
285 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
289 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
286 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
290 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
287 if next_input is None:
291 if next_input is None:
288 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
292 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
289 else:
293 else:
290 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
294 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
291 (next_input, t_magic_name, t_magic_arg_s)
295 (next_input, t_magic_name, t_magic_arg_s)
292
296
293 def _tr_help(content):
297 def _tr_help(content):
294 """Translate lines escaped with: ?
298 """Translate lines escaped with: ?
295
299
296 A naked help line should fire the intro help screen (shell.show_usage())
300 A naked help line should fire the intro help screen (shell.show_usage())
297 """
301 """
298 if not content:
302 if not content:
299 return 'get_ipython().show_usage()'
303 return 'get_ipython().show_usage()'
300
304
301 return _make_help_call(content, '?')
305 return _make_help_call(content, '?')
302
306
303 def _tr_help2(content):
307 def _tr_help2(content):
304 """Translate lines escaped with: ??
308 """Translate lines escaped with: ??
305
309
306 A naked help line should fire the intro help screen (shell.show_usage())
310 A naked help line should fire the intro help screen (shell.show_usage())
307 """
311 """
308 if not content:
312 if not content:
309 return 'get_ipython().show_usage()'
313 return 'get_ipython().show_usage()'
310
314
311 return _make_help_call(content, '??')
315 return _make_help_call(content, '??')
312
316
313 def _tr_magic(content):
317 def _tr_magic(content):
314 "Translate lines escaped with a percent sign: %"
318 "Translate lines escaped with a percent sign: %"
315 name, _, args = content.partition(' ')
319 name, _, args = content.partition(' ')
316 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
320 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
317
321
318 def _tr_quote(content):
322 def _tr_quote(content):
319 "Translate lines escaped with a comma: ,"
323 "Translate lines escaped with a comma: ,"
320 name, _, args = content.partition(' ')
324 name, _, args = content.partition(' ')
321 return '%s("%s")' % (name, '", "'.join(args.split()) )
325 return '%s("%s")' % (name, '", "'.join(args.split()) )
322
326
323 def _tr_quote2(content):
327 def _tr_quote2(content):
324 "Translate lines escaped with a semicolon: ;"
328 "Translate lines escaped with a semicolon: ;"
325 name, _, args = content.partition(' ')
329 name, _, args = content.partition(' ')
326 return '%s("%s")' % (name, args)
330 return '%s("%s")' % (name, args)
327
331
328 def _tr_paren(content):
332 def _tr_paren(content):
329 "Translate lines escaped with a slash: /"
333 "Translate lines escaped with a slash: /"
330 name, _, args = content.partition(' ')
334 name, _, args = content.partition(' ')
331 return '%s(%s)' % (name, ", ".join(args.split()))
335 return '%s(%s)' % (name, ", ".join(args.split()))
332
336
333 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
337 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
334 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
338 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
335 ESC_HELP : _tr_help,
339 ESC_HELP : _tr_help,
336 ESC_HELP2 : _tr_help2,
340 ESC_HELP2 : _tr_help2,
337 ESC_MAGIC : _tr_magic,
341 ESC_MAGIC : _tr_magic,
338 ESC_QUOTE : _tr_quote,
342 ESC_QUOTE : _tr_quote,
339 ESC_QUOTE2 : _tr_quote2,
343 ESC_QUOTE2 : _tr_quote2,
340 ESC_PAREN : _tr_paren }
344 ESC_PAREN : _tr_paren }
341
345
342 class EscapedCommand(TokenTransformBase):
346 class EscapedCommand(TokenTransformBase):
343 """Transformer for escaped commands like %foo, !foo, or /foo"""
347 """Transformer for escaped commands like %foo, !foo, or /foo"""
344 @classmethod
348 @classmethod
345 def find(cls, tokens_by_line):
349 def find(cls, tokens_by_line):
346 """Find the first escaped command (%foo, !foo, etc.) in the cell.
350 """Find the first escaped command (%foo, !foo, etc.) in the cell.
347 """
351 """
348 for line in tokens_by_line:
352 for line in tokens_by_line:
349 ix = 0
353 ix = 0
350 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
354 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
351 ix += 1
355 ix += 1
352 if line[ix].string in ESCAPE_SINGLES:
356 if line[ix].string in ESCAPE_SINGLES:
353 return cls(line[ix].start)
357 return cls(line[ix].start)
354
358
355 def transform(self, lines):
359 def transform(self, lines):
356 """Transform an escaped line found by the ``find()`` classmethod.
360 """Transform an escaped line found by the ``find()`` classmethod.
357 """
361 """
358 start_line, start_col = self.start_line, self.start_col
362 start_line, start_col = self.start_line, self.start_col
359
363
360 indent = lines[start_line][:start_col]
364 indent = lines[start_line][:start_col]
361 end_line = find_end_of_continued_line(lines, start_line)
365 end_line = find_end_of_continued_line(lines, start_line)
362 line = assemble_continued_line(lines, (start_line, start_col), end_line)
366 line = assemble_continued_line(lines, (start_line, start_col), end_line)
363
367
364 if line[:2] in ESCAPE_DOUBLES:
368 if line[:2] in ESCAPE_DOUBLES:
365 escape, content = line[:2], line[2:]
369 escape, content = line[:2], line[2:]
366 else:
370 else:
367 escape, content = line[:1], line[1:]
371 escape, content = line[:1], line[1:]
368 call = tr[escape](content)
372 call = tr[escape](content)
369
373
370 lines_before = lines[:start_line]
374 lines_before = lines[:start_line]
371 new_line = indent + call + '\n'
375 new_line = indent + call + '\n'
372 lines_after = lines[end_line + 1:]
376 lines_after = lines[end_line + 1:]
373
377
374 return lines_before + [new_line] + lines_after
378 return lines_before + [new_line] + lines_after
375
379
376 _help_end_re = re.compile(r"""(%{0,2}
380 _help_end_re = re.compile(r"""(%{0,2}
377 [a-zA-Z_*][\w*]* # Variable name
381 [a-zA-Z_*][\w*]* # Variable name
378 (\.[a-zA-Z_*][\w*]*)* # .etc.etc
382 (\.[a-zA-Z_*][\w*]*)* # .etc.etc
379 )
383 )
380 (\?\??)$ # ? or ??
384 (\?\??)$ # ? or ??
381 """,
385 """,
382 re.VERBOSE)
386 re.VERBOSE)
383
387
384 class HelpEnd(TokenTransformBase):
388 class HelpEnd(TokenTransformBase):
385 """Transformer for help syntax: obj? and obj??"""
389 """Transformer for help syntax: obj? and obj??"""
386 # This needs to be higher priority (lower number) than EscapedCommand so
390 # This needs to be higher priority (lower number) than EscapedCommand so
387 # that inspecting magics (%foo?) works.
391 # that inspecting magics (%foo?) works.
388 priority = 5
392 priority = 5
389
393
390 def __init__(self, start, q_locn):
394 def __init__(self, start, q_locn):
391 super().__init__(start)
395 super().__init__(start)
392 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
396 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
393 self.q_col = q_locn[1]
397 self.q_col = q_locn[1]
394
398
395 @classmethod
399 @classmethod
396 def find(cls, tokens_by_line):
400 def find(cls, tokens_by_line):
397 """Find the first help command (foo?) in the cell.
401 """Find the first help command (foo?) in the cell.
398 """
402 """
399 for line in tokens_by_line:
403 for line in tokens_by_line:
400 # Last token is NEWLINE; look at last but one
404 # Last token is NEWLINE; look at last but one
401 if len(line) > 2 and line[-2].string == '?':
405 if len(line) > 2 and line[-2].string == '?':
402 # Find the first token that's not INDENT/DEDENT
406 # Find the first token that's not INDENT/DEDENT
403 ix = 0
407 ix = 0
404 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
408 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
405 ix += 1
409 ix += 1
406 return cls(line[ix].start, line[-2].start)
410 return cls(line[ix].start, line[-2].start)
407
411
408 def transform(self, lines):
412 def transform(self, lines):
409 """Transform a help command found by the ``find()`` classmethod.
413 """Transform a help command found by the ``find()`` classmethod.
410 """
414 """
411 piece = ''.join(lines[self.start_line:self.q_line+1])
415 piece = ''.join(lines[self.start_line:self.q_line+1])
412 indent, content = piece[:self.start_col], piece[self.start_col:]
416 indent, content = piece[:self.start_col], piece[self.start_col:]
413 lines_before = lines[:self.start_line]
417 lines_before = lines[:self.start_line]
414 lines_after = lines[self.q_line + 1:]
418 lines_after = lines[self.q_line + 1:]
415
419
416 m = _help_end_re.search(content)
420 m = _help_end_re.search(content)
417 assert m is not None, content
421 assert m is not None, content
418 target = m.group(1)
422 target = m.group(1)
419 esc = m.group(3)
423 esc = m.group(3)
420
424
421 # If we're mid-command, put it back on the next prompt for the user.
425 # If we're mid-command, put it back on the next prompt for the user.
422 next_input = None
426 next_input = None
423 if (not lines_before) and (not lines_after) \
427 if (not lines_before) and (not lines_after) \
424 and content.strip() != m.group(0):
428 and content.strip() != m.group(0):
425 next_input = content.rstrip('?\n')
429 next_input = content.rstrip('?\n')
426
430
427 call = _make_help_call(target, esc, next_input=next_input)
431 call = _make_help_call(target, esc, next_input=next_input)
428 new_line = indent + call + '\n'
432 new_line = indent + call + '\n'
429
433
430 return lines_before + [new_line] + lines_after
434 return lines_before + [new_line] + lines_after
431
435
432 def make_tokens_by_line(lines):
436 def make_tokens_by_line(lines):
433 """Tokenize a series of lines and group tokens by line.
437 """Tokenize a series of lines and group tokens by line.
434
438
435 The tokens for a multiline Python string or expression are
439 The tokens for a multiline Python string or expression are
436 grouped as one line.
440 grouped as one line.
437 """
441 """
438 # NL tokens are used inside multiline expressions, but also after blank
442 # NL tokens are used inside multiline expressions, but also after blank
439 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
443 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
440 # We want to group the former case together but split the latter, so we
444 # We want to group the former case together but split the latter, so we
441 # track parentheses level, similar to the internals of tokenize.
445 # track parentheses level, similar to the internals of tokenize.
442 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL
446 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL
443 tokens_by_line = [[]]
447 tokens_by_line = [[]]
444 parenlev = 0
448 parenlev = 0
445 try:
449 try:
446 for token in tokenize.generate_tokens(iter(lines).__next__):
450 for token in tokenize.generate_tokens(iter(lines).__next__):
447 tokens_by_line[-1].append(token)
451 tokens_by_line[-1].append(token)
448 if (token.type == NEWLINE) \
452 if (token.type == NEWLINE) \
449 or ((token.type == NL) and (parenlev <= 0)):
453 or ((token.type == NL) and (parenlev <= 0)):
450 tokens_by_line.append([])
454 tokens_by_line.append([])
451 elif token.string in {'(', '[', '{'}:
455 elif token.string in {'(', '[', '{'}:
452 parenlev += 1
456 parenlev += 1
453 elif token.string in {')', ']', '}'}:
457 elif token.string in {')', ']', '}'}:
454 if parenlev > 0:
458 if parenlev > 0:
455 parenlev -= 1
459 parenlev -= 1
456 except tokenize.TokenError:
460 except tokenize.TokenError:
457 # Input ended in a multiline string or expression. That's OK for us.
461 # Input ended in a multiline string or expression. That's OK for us.
458 pass
462 pass
459
463
460 return tokens_by_line
464 return tokens_by_line
461
465
462 def show_linewise_tokens(s: str):
466 def show_linewise_tokens(s: str):
463 """For investigation and debugging"""
467 """For investigation and debugging"""
464 if not s.endswith('\n'):
468 if not s.endswith('\n'):
465 s += '\n'
469 s += '\n'
466 lines = s.splitlines(keepends=True)
470 lines = s.splitlines(keepends=True)
467 for line in make_tokens_by_line(lines):
471 for line in make_tokens_by_line(lines):
468 print("Line -------")
472 print("Line -------")
469 for tokinfo in line:
473 for tokinfo in line:
470 print(" ", tokinfo)
474 print(" ", tokinfo)
471
475
472 # Arbitrary limit to prevent getting stuck in infinite loops
476 # Arbitrary limit to prevent getting stuck in infinite loops
473 TRANSFORM_LOOP_LIMIT = 500
477 TRANSFORM_LOOP_LIMIT = 500
474
478
475 class TransformerManager:
479 class TransformerManager:
476 """Applies various transformations to a cell or code block.
480 """Applies various transformations to a cell or code block.
477
481
478 The key methods for external use are ``transform_cell()``
482 The key methods for external use are ``transform_cell()``
479 and ``check_complete()``.
483 and ``check_complete()``.
480 """
484 """
481 def __init__(self):
485 def __init__(self):
482 self.cleanup_transforms = [
486 self.cleanup_transforms = [
483 leading_indent,
487 leading_indent,
484 classic_prompt,
488 classic_prompt,
485 ipython_prompt,
489 ipython_prompt,
486 ]
490 ]
487 self.line_transforms = [
491 self.line_transforms = [
488 cell_magic,
492 cell_magic,
489 ]
493 ]
490 self.token_transformers = [
494 self.token_transformers = [
491 MagicAssign,
495 MagicAssign,
492 SystemAssign,
496 SystemAssign,
493 EscapedCommand,
497 EscapedCommand,
494 HelpEnd,
498 HelpEnd,
495 ]
499 ]
496
500
497 def do_one_token_transform(self, lines):
501 def do_one_token_transform(self, lines):
498 """Find and run the transform earliest in the code.
502 """Find and run the transform earliest in the code.
499
503
500 Returns (changed, lines).
504 Returns (changed, lines).
501
505
502 This method is called repeatedly until changed is False, indicating
506 This method is called repeatedly until changed is False, indicating
503 that all available transformations are complete.
507 that all available transformations are complete.
504
508
505 The tokens following IPython special syntax might not be valid, so
509 The tokens following IPython special syntax might not be valid, so
506 the transformed code is retokenised every time to identify the next
510 the transformed code is retokenised every time to identify the next
507 piece of special syntax. Hopefully long code cells are mostly valid
511 piece of special syntax. Hopefully long code cells are mostly valid
508 Python, not using lots of IPython special syntax, so this shouldn't be
512 Python, not using lots of IPython special syntax, so this shouldn't be
509 a performance issue.
513 a performance issue.
510 """
514 """
511 tokens_by_line = make_tokens_by_line(lines)
515 tokens_by_line = make_tokens_by_line(lines)
512 candidates = []
516 candidates = []
513 for transformer_cls in self.token_transformers:
517 for transformer_cls in self.token_transformers:
514 transformer = transformer_cls.find(tokens_by_line)
518 transformer = transformer_cls.find(tokens_by_line)
515 if transformer:
519 if transformer:
516 candidates.append(transformer)
520 candidates.append(transformer)
517
521
518 if not candidates:
522 if not candidates:
519 # Nothing to transform
523 # Nothing to transform
520 return False, lines
524 return False, lines
521
525
522 transformer = min(candidates, key=TokenTransformBase.sortby)
526 transformer = min(candidates, key=TokenTransformBase.sortby)
523 return True, transformer.transform(lines)
527 return True, transformer.transform(lines)
524
528
525 def do_token_transforms(self, lines):
529 def do_token_transforms(self, lines):
526 for _ in range(TRANSFORM_LOOP_LIMIT):
530 for _ in range(TRANSFORM_LOOP_LIMIT):
527 changed, lines = self.do_one_token_transform(lines)
531 changed, lines = self.do_one_token_transform(lines)
528 if not changed:
532 if not changed:
529 return lines
533 return lines
530
534
531 raise RuntimeError("Input transformation still changing after "
535 raise RuntimeError("Input transformation still changing after "
532 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
536 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
533
537
534 def transform_cell(self, cell: str) -> str:
538 def transform_cell(self, cell: str) -> str:
535 """Transforms a cell of input code"""
539 """Transforms a cell of input code"""
536 if not cell.endswith('\n'):
540 if not cell.endswith('\n'):
537 cell += '\n' # Ensure the cell has a trailing newline
541 cell += '\n' # Ensure the cell has a trailing newline
538 lines = cell.splitlines(keepends=True)
542 lines = cell.splitlines(keepends=True)
539 for transform in self.cleanup_transforms + self.line_transforms:
543 for transform in self.cleanup_transforms + self.line_transforms:
540 #print(transform, lines)
544 #print(transform, lines)
541 lines = transform(lines)
545 lines = transform(lines)
542
546
543 lines = self.do_token_transforms(lines)
547 lines = self.do_token_transforms(lines)
544 return ''.join(lines)
548 return ''.join(lines)
545
549
546 def check_complete(self, cell: str):
550 def check_complete(self, cell: str):
547 """Return whether a block of code is ready to execute, or should be continued
551 """Return whether a block of code is ready to execute, or should be continued
548
552
549 Parameters
553 Parameters
550 ----------
554 ----------
551 source : string
555 source : string
552 Python input code, which can be multiline.
556 Python input code, which can be multiline.
553
557
554 Returns
558 Returns
555 -------
559 -------
556 status : str
560 status : str
557 One of 'complete', 'incomplete', or 'invalid' if source is not a
561 One of 'complete', 'incomplete', or 'invalid' if source is not a
558 prefix of valid code.
562 prefix of valid code.
559 indent_spaces : int or None
563 indent_spaces : int or None
560 The number of spaces by which to indent the next line of code. If
564 The number of spaces by which to indent the next line of code. If
561 status is not 'incomplete', this is None.
565 status is not 'incomplete', this is None.
562 """
566 """
563 if not cell.endswith('\n'):
567 if not cell.endswith('\n'):
564 cell += '\n' # Ensure the cell has a trailing newline
568 cell += '\n' # Ensure the cell has a trailing newline
565 lines = cell.splitlines(keepends=True)
569 lines = cell.splitlines(keepends=True)
566 if lines[-1][:-1].endswith('\\'):
570 if lines[-1][:-1].endswith('\\'):
567 # Explicit backslash continuation
571 # Explicit backslash continuation
568 return 'incomplete', find_last_indent(lines)
572 return 'incomplete', find_last_indent(lines)
569
573
570 try:
574 try:
571 for transform in self.cleanup_transforms:
575 for transform in self.cleanup_transforms:
572 lines = transform(lines)
576 lines = transform(lines)
573 except SyntaxError:
577 except SyntaxError:
574 return 'invalid', None
578 return 'invalid', None
575
579
576 if lines[0].startswith('%%'):
580 if lines[0].startswith('%%'):
577 # Special case for cell magics - completion marked by blank line
581 # Special case for cell magics - completion marked by blank line
578 if lines[-1].strip():
582 if lines[-1].strip():
579 return 'incomplete', find_last_indent(lines)
583 return 'incomplete', find_last_indent(lines)
580 else:
584 else:
581 return 'complete', None
585 return 'complete', None
582
586
583 try:
587 try:
584 for transform in self.line_transforms:
588 for transform in self.line_transforms:
585 lines = transform(lines)
589 lines = transform(lines)
586 lines = self.do_token_transforms(lines)
590 lines = self.do_token_transforms(lines)
587 except SyntaxError:
591 except SyntaxError:
588 return 'invalid', None
592 return 'invalid', None
589
593
590 tokens_by_line = make_tokens_by_line(lines)
594 tokens_by_line = make_tokens_by_line(lines)
591 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
595 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
592 # We're in a multiline string or expression
596 # We're in a multiline string or expression
593 return 'incomplete', find_last_indent(lines)
597 return 'incomplete', find_last_indent(lines)
594
598
595 # Find the last token on the previous line that's not NEWLINE or COMMENT
599 # Find the last token on the previous line that's not NEWLINE or COMMENT
596 toks_last_line = tokens_by_line[-2]
600 toks_last_line = tokens_by_line[-2]
597 ix = len(toks_last_line) - 1
601 ix = len(toks_last_line) - 1
598 while ix >= 0 and toks_last_line[ix].type in {tokenize.NEWLINE,
602 while ix >= 0 and toks_last_line[ix].type in {tokenize.NEWLINE,
599 tokenize.COMMENT}:
603 tokenize.COMMENT}:
600 ix -= 1
604 ix -= 1
601
605
602 if toks_last_line[ix].string == ':':
606 if toks_last_line[ix].string == ':':
603 # The last line starts a block (e.g. 'if foo:')
607 # The last line starts a block (e.g. 'if foo:')
604 ix = 0
608 ix = 0
605 while toks_last_line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
609 while toks_last_line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
606 ix += 1
610 ix += 1
607 indent = toks_last_line[ix].start[1]
611 indent = toks_last_line[ix].start[1]
608 return 'incomplete', indent + 4
612 return 'incomplete', indent + 4
609
613
610 # If there's a blank line at the end, assume we're ready to execute.
614 # If there's a blank line at the end, assume we're ready to execute.
611 if not lines[-1].strip():
615 if not lines[-1].strip():
612 return 'complete', None
616 return 'complete', None
613
617
614 # At this point, our checks think the code is complete (or invalid).
618 # At this point, our checks think the code is complete (or invalid).
615 # We'll use codeop.compile_command to check this with the real parser.
619 # We'll use codeop.compile_command to check this with the real parser.
616
620
617 try:
621 try:
618 with warnings.catch_warnings():
622 with warnings.catch_warnings():
619 warnings.simplefilter('error', SyntaxWarning)
623 warnings.simplefilter('error', SyntaxWarning)
620 res = compile_command(''.join(lines), symbol='exec')
624 res = compile_command(''.join(lines), symbol='exec')
621 except (SyntaxError, OverflowError, ValueError, TypeError,
625 except (SyntaxError, OverflowError, ValueError, TypeError,
622 MemoryError, SyntaxWarning):
626 MemoryError, SyntaxWarning):
623 return 'invalid', None
627 return 'invalid', None
624 else:
628 else:
625 if res is None:
629 if res is None:
626 return 'incomplete', find_last_indent(lines)
630 return 'incomplete', find_last_indent(lines)
627 return 'complete', None
631 return 'complete', None
628
632
629
633
630 def find_last_indent(lines):
634 def find_last_indent(lines):
631 m = _indent_re.match(lines[-1])
635 m = _indent_re.match(lines[-1])
632 if not m:
636 if not m:
633 return 0
637 return 0
634 return len(m.group(0).replace('\t', ' '*4))
638 return len(m.group(0).replace('\t', ' '*4))
General Comments 0
You need to be logged in to leave comments. Login now