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