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