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