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