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