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