##// END OF EJS Templates
Merge pull request #13630 from Carreau/longerfail...
Matthias Bussonnier -
r27628:14214027 merge
parent child Browse files
Show More
@@ -1,750 +1,752 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 import ast
14 14 import sys
15 15 from codeop import CommandCompiler, Compile
16 16 import re
17 17 import tokenize
18 18 from typing import List, Tuple, Union
19 19 import warnings
20 20
21 21 _indent_re = re.compile(r'^[ \t]+')
22 22
23 23 def leading_empty_lines(lines):
24 24 """Remove leading empty lines
25 25
26 26 If the leading lines are empty or contain only whitespace, they will be
27 27 removed.
28 28 """
29 29 if not lines:
30 30 return lines
31 31 for i, line in enumerate(lines):
32 32 if line and not line.isspace():
33 33 return lines[i:]
34 34 return lines
35 35
36 36 def leading_indent(lines):
37 37 """Remove leading indentation.
38 38
39 39 If the first line starts with a spaces or tabs, the same whitespace will be
40 40 removed from each following line in the cell.
41 41 """
42 42 if not lines:
43 43 return lines
44 44 m = _indent_re.match(lines[0])
45 45 if not m:
46 46 return lines
47 47 space = m.group(0)
48 48 n = len(space)
49 49 return [l[n:] if l.startswith(space) else l
50 50 for l in lines]
51 51
52 52 class PromptStripper:
53 53 """Remove matching input prompts from a block of input.
54 54
55 55 Parameters
56 56 ----------
57 57 prompt_re : regular expression
58 58 A regular expression matching any input prompt (including continuation,
59 59 e.g. ``...``)
60 60 initial_re : regular expression, optional
61 61 A regular expression matching only the initial prompt, but not continuation.
62 62 If no initial expression is given, prompt_re will be used everywhere.
63 63 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
64 64 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
65 65
66 66 Notes
67 67 -----
68 68
69 69 If initial_re and prompt_re differ,
70 70 only initial_re will be tested against the first line.
71 71 If any prompt is found on the first two lines,
72 72 prompts will be stripped from the rest of the block.
73 73 """
74 74 def __init__(self, prompt_re, initial_re=None):
75 75 self.prompt_re = prompt_re
76 76 self.initial_re = initial_re or prompt_re
77 77
78 78 def _strip(self, lines):
79 79 return [self.prompt_re.sub('', l, count=1) for l in lines]
80 80
81 81 def __call__(self, lines):
82 82 if not lines:
83 83 return lines
84 84 if self.initial_re.match(lines[0]) or \
85 85 (len(lines) > 1 and self.prompt_re.match(lines[1])):
86 86 return self._strip(lines)
87 87 return lines
88 88
89 89 classic_prompt = PromptStripper(
90 90 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
91 91 initial_re=re.compile(r'^>>>( |$)')
92 92 )
93 93
94 94 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
95 95
96 96 def cell_magic(lines):
97 97 if not lines or not lines[0].startswith('%%'):
98 98 return lines
99 99 if re.match(r'%%\w+\?', lines[0]):
100 100 # This case will be handled by help_end
101 101 return lines
102 102 magic_name, _, first_line = lines[0][2:].rstrip().partition(' ')
103 103 body = ''.join(lines[1:])
104 104 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
105 105 % (magic_name, first_line, body)]
106 106
107 107
108 108 def _find_assign_op(token_line) -> Union[int, None]:
109 109 """Get the index of the first assignment in the line ('=' not inside brackets)
110 110
111 111 Note: We don't try to support multiple special assignment (a = b = %foo)
112 112 """
113 113 paren_level = 0
114 114 for i, ti in enumerate(token_line):
115 115 s = ti.string
116 116 if s == '=' and paren_level == 0:
117 117 return i
118 118 if s in {'(','[','{'}:
119 119 paren_level += 1
120 120 elif s in {')', ']', '}'}:
121 121 if paren_level > 0:
122 122 paren_level -= 1
123 123
124 124 def find_end_of_continued_line(lines, start_line: int):
125 125 """Find the last line of a line explicitly extended using backslashes.
126 126
127 127 Uses 0-indexed line numbers.
128 128 """
129 129 end_line = start_line
130 130 while lines[end_line].endswith('\\\n'):
131 131 end_line += 1
132 132 if end_line >= len(lines):
133 133 break
134 134 return end_line
135 135
136 136 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
137 137 r"""Assemble a single line from multiple continued line pieces
138 138
139 139 Continued lines are lines ending in ``\``, and the line following the last
140 140 ``\`` in the block.
141 141
142 142 For example, this code continues over multiple lines::
143 143
144 144 if (assign_ix is not None) \
145 145 and (len(line) >= assign_ix + 2) \
146 146 and (line[assign_ix+1].string == '%') \
147 147 and (line[assign_ix+2].type == tokenize.NAME):
148 148
149 149 This statement contains four continued line pieces.
150 150 Assembling these pieces into a single line would give::
151 151
152 152 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
153 153
154 154 This uses 0-indexed line numbers. *start* is (lineno, colno).
155 155
156 156 Used to allow ``%magic`` and ``!system`` commands to be continued over
157 157 multiple lines.
158 158 """
159 159 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
160 160 return ' '.join([p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline
161 161 + [parts[-1].rstrip()]) # Strip newline from last line
162 162
163 163 class TokenTransformBase:
164 164 """Base class for transformations which examine tokens.
165 165
166 166 Special syntax should not be transformed when it occurs inside strings or
167 167 comments. This is hard to reliably avoid with regexes. The solution is to
168 168 tokenise the code as Python, and recognise the special syntax in the tokens.
169 169
170 170 IPython's special syntax is not valid Python syntax, so tokenising may go
171 171 wrong after the special syntax starts. These classes therefore find and
172 172 transform *one* instance of special syntax at a time into regular Python
173 173 syntax. After each transformation, tokens are regenerated to find the next
174 174 piece of special syntax.
175 175
176 176 Subclasses need to implement one class method (find)
177 177 and one regular method (transform).
178 178
179 179 The priority attribute can select which transformation to apply if multiple
180 180 transformers match in the same place. Lower numbers have higher priority.
181 181 This allows "%magic?" to be turned into a help call rather than a magic call.
182 182 """
183 183 # Lower numbers -> higher priority (for matches in the same location)
184 184 priority = 10
185 185
186 186 def sortby(self):
187 187 return self.start_line, self.start_col, self.priority
188 188
189 189 def __init__(self, start):
190 190 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
191 191 self.start_col = start[1]
192 192
193 193 @classmethod
194 194 def find(cls, tokens_by_line):
195 195 """Find one instance of special syntax in the provided tokens.
196 196
197 197 Tokens are grouped into logical lines for convenience,
198 198 so it is easy to e.g. look at the first token of each line.
199 199 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
200 200
201 201 This should return an instance of its class, pointing to the start
202 202 position it has found, or None if it found no match.
203 203 """
204 204 raise NotImplementedError
205 205
206 206 def transform(self, lines: List[str]):
207 207 """Transform one instance of special syntax found by ``find()``
208 208
209 209 Takes a list of strings representing physical lines,
210 210 returns a similar list of transformed lines.
211 211 """
212 212 raise NotImplementedError
213 213
214 214 class MagicAssign(TokenTransformBase):
215 215 """Transformer for assignments from magics (a = %foo)"""
216 216 @classmethod
217 217 def find(cls, tokens_by_line):
218 218 """Find the first magic assignment (a = %foo) in the cell.
219 219 """
220 220 for line in tokens_by_line:
221 221 assign_ix = _find_assign_op(line)
222 222 if (assign_ix is not None) \
223 223 and (len(line) >= assign_ix + 2) \
224 224 and (line[assign_ix+1].string == '%') \
225 225 and (line[assign_ix+2].type == tokenize.NAME):
226 226 return cls(line[assign_ix+1].start)
227 227
228 228 def transform(self, lines: List[str]):
229 229 """Transform a magic assignment found by the ``find()`` classmethod.
230 230 """
231 231 start_line, start_col = self.start_line, self.start_col
232 232 lhs = lines[start_line][:start_col]
233 233 end_line = find_end_of_continued_line(lines, start_line)
234 234 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
235 235 assert rhs.startswith('%'), rhs
236 236 magic_name, _, args = rhs[1:].partition(' ')
237 237
238 238 lines_before = lines[:start_line]
239 239 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
240 240 new_line = lhs + call + '\n'
241 241 lines_after = lines[end_line+1:]
242 242
243 243 return lines_before + [new_line] + lines_after
244 244
245 245
246 246 class SystemAssign(TokenTransformBase):
247 247 """Transformer for assignments from system commands (a = !foo)"""
248 248 @classmethod
249 249 def find(cls, tokens_by_line):
250 250 """Find the first system assignment (a = !foo) in the cell.
251 251 """
252 252 for line in tokens_by_line:
253 253 assign_ix = _find_assign_op(line)
254 254 if (assign_ix is not None) \
255 255 and not line[assign_ix].line.strip().startswith('=') \
256 256 and (len(line) >= assign_ix + 2) \
257 257 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
258 258 ix = assign_ix + 1
259 259
260 260 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
261 261 if line[ix].string == '!':
262 262 return cls(line[ix].start)
263 263 elif not line[ix].string.isspace():
264 264 break
265 265 ix += 1
266 266
267 267 def transform(self, lines: List[str]):
268 268 """Transform a system assignment found by the ``find()`` classmethod.
269 269 """
270 270 start_line, start_col = self.start_line, self.start_col
271 271
272 272 lhs = lines[start_line][:start_col]
273 273 end_line = find_end_of_continued_line(lines, start_line)
274 274 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
275 275 assert rhs.startswith('!'), rhs
276 276 cmd = rhs[1:]
277 277
278 278 lines_before = lines[:start_line]
279 279 call = "get_ipython().getoutput({!r})".format(cmd)
280 280 new_line = lhs + call + '\n'
281 281 lines_after = lines[end_line + 1:]
282 282
283 283 return lines_before + [new_line] + lines_after
284 284
285 285 # The escape sequences that define the syntax transformations IPython will
286 286 # apply to user input. These can NOT be just changed here: many regular
287 287 # expressions and other parts of the code may use their hardcoded values, and
288 288 # for all intents and purposes they constitute the 'IPython syntax', so they
289 289 # should be considered fixed.
290 290
291 291 ESC_SHELL = '!' # Send line to underlying system shell
292 292 ESC_SH_CAP = '!!' # Send line to system shell and capture output
293 293 ESC_HELP = '?' # Find information about object
294 294 ESC_HELP2 = '??' # Find extra-detailed information about object
295 295 ESC_MAGIC = '%' # Call magic function
296 296 ESC_MAGIC2 = '%%' # Call cell-magic function
297 297 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
298 298 ESC_QUOTE2 = ';' # Quote all args as a single string, call
299 299 ESC_PAREN = '/' # Call first argument with rest of line as arguments
300 300
301 301 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
302 302 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
303 303
304 304 def _make_help_call(target, esc, next_input=None):
305 305 """Prepares a pinfo(2)/psearch call from a target name and the escape
306 306 (i.e. ? or ??)"""
307 307 method = 'pinfo2' if esc == '??' \
308 308 else 'psearch' if '*' in target \
309 309 else 'pinfo'
310 310 arg = " ".join([method, target])
311 311 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
312 312 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
313 313 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
314 314 if next_input is None:
315 315 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
316 316 else:
317 317 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
318 318 (next_input, t_magic_name, t_magic_arg_s)
319 319
320 320 def _tr_help(content):
321 321 """Translate lines escaped with: ?
322 322
323 323 A naked help line should fire the intro help screen (shell.show_usage())
324 324 """
325 325 if not content:
326 326 return 'get_ipython().show_usage()'
327 327
328 328 return _make_help_call(content, '?')
329 329
330 330 def _tr_help2(content):
331 331 """Translate lines escaped with: ??
332 332
333 333 A naked help line should fire the intro help screen (shell.show_usage())
334 334 """
335 335 if not content:
336 336 return 'get_ipython().show_usage()'
337 337
338 338 return _make_help_call(content, '??')
339 339
340 340 def _tr_magic(content):
341 341 "Translate lines escaped with a percent sign: %"
342 342 name, _, args = content.partition(' ')
343 343 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
344 344
345 345 def _tr_quote(content):
346 346 "Translate lines escaped with a comma: ,"
347 347 name, _, args = content.partition(' ')
348 348 return '%s("%s")' % (name, '", "'.join(args.split()) )
349 349
350 350 def _tr_quote2(content):
351 351 "Translate lines escaped with a semicolon: ;"
352 352 name, _, args = content.partition(' ')
353 353 return '%s("%s")' % (name, args)
354 354
355 355 def _tr_paren(content):
356 356 "Translate lines escaped with a slash: /"
357 357 name, _, args = content.partition(' ')
358 358 return '%s(%s)' % (name, ", ".join(args.split()))
359 359
360 360 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
361 361 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
362 362 ESC_HELP : _tr_help,
363 363 ESC_HELP2 : _tr_help2,
364 364 ESC_MAGIC : _tr_magic,
365 365 ESC_QUOTE : _tr_quote,
366 366 ESC_QUOTE2 : _tr_quote2,
367 367 ESC_PAREN : _tr_paren }
368 368
369 369 class EscapedCommand(TokenTransformBase):
370 370 """Transformer for escaped commands like %foo, !foo, or /foo"""
371 371 @classmethod
372 372 def find(cls, tokens_by_line):
373 373 """Find the first escaped command (%foo, !foo, etc.) in the cell.
374 374 """
375 375 for line in tokens_by_line:
376 376 if not line:
377 377 continue
378 378 ix = 0
379 379 ll = len(line)
380 380 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
381 381 ix += 1
382 382 if ix >= ll:
383 383 continue
384 384 if line[ix].string in ESCAPE_SINGLES:
385 385 return cls(line[ix].start)
386 386
387 387 def transform(self, lines):
388 388 """Transform an escaped line found by the ``find()`` classmethod.
389 389 """
390 390 start_line, start_col = self.start_line, self.start_col
391 391
392 392 indent = lines[start_line][:start_col]
393 393 end_line = find_end_of_continued_line(lines, start_line)
394 394 line = assemble_continued_line(lines, (start_line, start_col), end_line)
395 395
396 396 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
397 397 escape, content = line[:2], line[2:]
398 398 else:
399 399 escape, content = line[:1], line[1:]
400 400
401 401 if escape in tr:
402 402 call = tr[escape](content)
403 403 else:
404 404 call = ''
405 405
406 406 lines_before = lines[:start_line]
407 407 new_line = indent + call + '\n'
408 408 lines_after = lines[end_line + 1:]
409 409
410 410 return lines_before + [new_line] + lines_after
411 411
412 412 _help_end_re = re.compile(r"""(%{0,2}
413 413 (?!\d)[\w*]+ # Variable name
414 414 (\.(?!\d)[\w*]+)* # .etc.etc
415 415 )
416 416 (\?\??)$ # ? or ??
417 417 """,
418 418 re.VERBOSE)
419 419
420 420 class HelpEnd(TokenTransformBase):
421 421 """Transformer for help syntax: obj? and obj??"""
422 422 # This needs to be higher priority (lower number) than EscapedCommand so
423 423 # that inspecting magics (%foo?) works.
424 424 priority = 5
425 425
426 426 def __init__(self, start, q_locn):
427 427 super().__init__(start)
428 428 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
429 429 self.q_col = q_locn[1]
430 430
431 431 @classmethod
432 432 def find(cls, tokens_by_line):
433 433 """Find the first help command (foo?) in the cell.
434 434 """
435 435 for line in tokens_by_line:
436 436 # Last token is NEWLINE; look at last but one
437 437 if len(line) > 2 and line[-2].string == '?':
438 438 # Find the first token that's not INDENT/DEDENT
439 439 ix = 0
440 440 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
441 441 ix += 1
442 442 return cls(line[ix].start, line[-2].start)
443 443
444 444 def transform(self, lines):
445 445 """Transform a help command found by the ``find()`` classmethod.
446 446 """
447 447 piece = ''.join(lines[self.start_line:self.q_line+1])
448 448 indent, content = piece[:self.start_col], piece[self.start_col:]
449 449 lines_before = lines[:self.start_line]
450 450 lines_after = lines[self.q_line + 1:]
451 451
452 452 m = _help_end_re.search(content)
453 453 if not m:
454 454 raise SyntaxError(content)
455 455 assert m is not None, content
456 456 target = m.group(1)
457 457 esc = m.group(3)
458 458
459 459 # If we're mid-command, put it back on the next prompt for the user.
460 460 next_input = None
461 461 if (not lines_before) and (not lines_after) \
462 462 and content.strip() != m.group(0):
463 463 next_input = content.rstrip('?\n')
464 464
465 465 call = _make_help_call(target, esc, next_input=next_input)
466 466 new_line = indent + call + '\n'
467 467
468 468 return lines_before + [new_line] + lines_after
469 469
470 470 def make_tokens_by_line(lines:List[str]):
471 471 """Tokenize a series of lines and group tokens by line.
472 472
473 473 The tokens for a multiline Python string or expression are grouped as one
474 474 line. All lines except the last lines should keep their line ending ('\\n',
475 475 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
476 476 for example when passing block of text to this function.
477 477
478 478 """
479 479 # NL tokens are used inside multiline expressions, but also after blank
480 480 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
481 481 # We want to group the former case together but split the latter, so we
482 482 # track parentheses level, similar to the internals of tokenize.
483 483 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL
484 484 tokens_by_line = [[]]
485 485 if len(lines) > 1 and not lines[0].endswith(('\n', '\r', '\r\n', '\x0b', '\x0c')):
486 486 warnings.warn("`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified")
487 487 parenlev = 0
488 488 try:
489 489 for token in tokenize.generate_tokens(iter(lines).__next__):
490 490 tokens_by_line[-1].append(token)
491 491 if (token.type == NEWLINE) \
492 492 or ((token.type == NL) and (parenlev <= 0)):
493 493 tokens_by_line.append([])
494 494 elif token.string in {'(', '[', '{'}:
495 495 parenlev += 1
496 496 elif token.string in {')', ']', '}'}:
497 497 if parenlev > 0:
498 498 parenlev -= 1
499 499 except tokenize.TokenError:
500 500 # Input ended in a multiline string or expression. That's OK for us.
501 501 pass
502 502
503 503
504 504 if not tokens_by_line[-1]:
505 505 tokens_by_line.pop()
506 506
507 507
508 508 return tokens_by_line
509 509
510 510 def show_linewise_tokens(s: str):
511 511 """For investigation and debugging"""
512 512 if not s.endswith('\n'):
513 513 s += '\n'
514 514 lines = s.splitlines(keepends=True)
515 515 for line in make_tokens_by_line(lines):
516 516 print("Line -------")
517 517 for tokinfo in line:
518 518 print(" ", tokinfo)
519 519
520 520 # Arbitrary limit to prevent getting stuck in infinite loops
521 521 TRANSFORM_LOOP_LIMIT = 500
522 522
523 523 class TransformerManager:
524 524 """Applies various transformations to a cell or code block.
525 525
526 526 The key methods for external use are ``transform_cell()``
527 527 and ``check_complete()``.
528 528 """
529 529 def __init__(self):
530 530 self.cleanup_transforms = [
531 531 leading_empty_lines,
532 532 leading_indent,
533 533 classic_prompt,
534 534 ipython_prompt,
535 535 ]
536 536 self.line_transforms = [
537 537 cell_magic,
538 538 ]
539 539 self.token_transformers = [
540 540 MagicAssign,
541 541 SystemAssign,
542 542 EscapedCommand,
543 543 HelpEnd,
544 544 ]
545 545
546 546 def do_one_token_transform(self, lines):
547 547 """Find and run the transform earliest in the code.
548 548
549 549 Returns (changed, lines).
550 550
551 551 This method is called repeatedly until changed is False, indicating
552 552 that all available transformations are complete.
553 553
554 554 The tokens following IPython special syntax might not be valid, so
555 555 the transformed code is retokenised every time to identify the next
556 556 piece of special syntax. Hopefully long code cells are mostly valid
557 557 Python, not using lots of IPython special syntax, so this shouldn't be
558 558 a performance issue.
559 559 """
560 560 tokens_by_line = make_tokens_by_line(lines)
561 561 candidates = []
562 562 for transformer_cls in self.token_transformers:
563 563 transformer = transformer_cls.find(tokens_by_line)
564 564 if transformer:
565 565 candidates.append(transformer)
566 566
567 567 if not candidates:
568 568 # Nothing to transform
569 569 return False, lines
570 570 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
571 571 for transformer in ordered_transformers:
572 572 try:
573 573 return True, transformer.transform(lines)
574 574 except SyntaxError:
575 575 pass
576 576 return False, lines
577 577
578 578 def do_token_transforms(self, lines):
579 579 for _ in range(TRANSFORM_LOOP_LIMIT):
580 580 changed, lines = self.do_one_token_transform(lines)
581 581 if not changed:
582 582 return lines
583 583
584 584 raise RuntimeError("Input transformation still changing after "
585 585 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
586 586
587 587 def transform_cell(self, cell: str) -> str:
588 588 """Transforms a cell of input code"""
589 589 if not cell.endswith('\n'):
590 590 cell += '\n' # Ensure the cell has a trailing newline
591 591 lines = cell.splitlines(keepends=True)
592 592 for transform in self.cleanup_transforms + self.line_transforms:
593 593 lines = transform(lines)
594 594
595 595 lines = self.do_token_transforms(lines)
596 596 return ''.join(lines)
597 597
598 598 def check_complete(self, cell: str):
599 599 """Return whether a block of code is ready to execute, or should be continued
600 600
601 601 Parameters
602 602 ----------
603 603 source : string
604 604 Python input code, which can be multiline.
605 605
606 606 Returns
607 607 -------
608 608 status : str
609 609 One of 'complete', 'incomplete', or 'invalid' if source is not a
610 610 prefix of valid code.
611 611 indent_spaces : int or None
612 612 The number of spaces by which to indent the next line of code. If
613 613 status is not 'incomplete', this is None.
614 614 """
615 615 # Remember if the lines ends in a new line.
616 616 ends_with_newline = False
617 617 for character in reversed(cell):
618 618 if character == '\n':
619 619 ends_with_newline = True
620 620 break
621 621 elif character.strip():
622 622 break
623 623 else:
624 624 continue
625 625
626 626 if not ends_with_newline:
627 627 # Append an newline for consistent tokenization
628 628 # See https://bugs.python.org/issue33899
629 629 cell += '\n'
630 630
631 631 lines = cell.splitlines(keepends=True)
632 632
633 633 if not lines:
634 634 return 'complete', None
635 635
636 636 if lines[-1].endswith('\\'):
637 637 # Explicit backslash continuation
638 638 return 'incomplete', find_last_indent(lines)
639 639
640 640 try:
641 641 for transform in self.cleanup_transforms:
642 642 if not getattr(transform, 'has_side_effects', False):
643 643 lines = transform(lines)
644 644 except SyntaxError:
645 645 return 'invalid', None
646 646
647 647 if lines[0].startswith('%%'):
648 648 # Special case for cell magics - completion marked by blank line
649 649 if lines[-1].strip():
650 650 return 'incomplete', find_last_indent(lines)
651 651 else:
652 652 return 'complete', None
653 653
654 654 try:
655 655 for transform in self.line_transforms:
656 656 if not getattr(transform, 'has_side_effects', False):
657 657 lines = transform(lines)
658 658 lines = self.do_token_transforms(lines)
659 659 except SyntaxError:
660 660 return 'invalid', None
661 661
662 662 tokens_by_line = make_tokens_by_line(lines)
663 663
664 664 if not tokens_by_line:
665 665 return 'incomplete', find_last_indent(lines)
666 666
667 667 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
668 668 # We're in a multiline string or expression
669 669 return 'incomplete', find_last_indent(lines)
670 670
671 671 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER}
672 672
673 673 # Pop the last line which only contains DEDENTs and ENDMARKER
674 674 last_token_line = None
675 675 if {t.type for t in tokens_by_line[-1]} in [
676 676 {tokenize.DEDENT, tokenize.ENDMARKER},
677 677 {tokenize.ENDMARKER}
678 678 ] and len(tokens_by_line) > 1:
679 679 last_token_line = tokens_by_line.pop()
680 680
681 681 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
682 682 tokens_by_line[-1].pop()
683 683
684 684 if not tokens_by_line[-1]:
685 685 return 'incomplete', find_last_indent(lines)
686 686
687 687 if tokens_by_line[-1][-1].string == ':':
688 688 # The last line starts a block (e.g. 'if foo:')
689 689 ix = 0
690 690 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
691 691 ix += 1
692 692
693 693 indent = tokens_by_line[-1][ix].start[1]
694 694 return 'incomplete', indent + 4
695 695
696 696 if tokens_by_line[-1][0].line.endswith('\\'):
697 697 return 'incomplete', None
698 698
699 699 # At this point, our checks think the code is complete (or invalid).
700 700 # We'll use codeop.compile_command to check this with the real parser
701 701 try:
702 702 with warnings.catch_warnings():
703 703 warnings.simplefilter('error', SyntaxWarning)
704 704 res = compile_command(''.join(lines), symbol='exec')
705 705 except (SyntaxError, OverflowError, ValueError, TypeError,
706 706 MemoryError, SyntaxWarning):
707 707 return 'invalid', None
708 708 else:
709 709 if res is None:
710 710 return 'incomplete', find_last_indent(lines)
711 711
712 712 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
713 713 if ends_with_newline:
714 714 return 'complete', None
715 715 return 'incomplete', find_last_indent(lines)
716 716
717 717 # If there's a blank line at the end, assume we're ready to execute
718 718 if not lines[-1].strip():
719 719 return 'complete', None
720 720
721 721 return 'complete', None
722 722
723 723
724 724 def find_last_indent(lines):
725 725 m = _indent_re.match(lines[-1])
726 726 if not m:
727 727 return 0
728 728 return len(m.group(0).replace('\t', ' '*4))
729 729
730 730
731 731 class MaybeAsyncCompile(Compile):
732 732 def __init__(self, extra_flags=0):
733 733 super().__init__()
734 734 self.flags |= extra_flags
735 735
736 def __call__(self, *args, **kwds):
737 return compile(*args, **kwds)
736
737 if sys.version_info < (3,8):
738 def __call__(self, *args, **kwds):
739 return compile(*args, **kwds)
738 740
739 741
740 742 class MaybeAsyncCommandCompiler(CommandCompiler):
741 743 def __init__(self, extra_flags=0):
742 744 self.compiler = MaybeAsyncCompile(extra_flags=extra_flags)
743 745
744 746
745 747 if (sys.version_info.major, sys.version_info.minor) >= (3, 8):
746 748 _extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
747 749 else:
748 750 _extra_flags = ast.PyCF_ONLY_AST
749 751
750 752 compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags)
@@ -1,579 +1,580 b''
1 1 """Tests for debugging machinery.
2 2 """
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 import bdb
8 8 import builtins
9 9 import os
10 10 import signal
11 11 import subprocess
12 12 import sys
13 13 import time
14 14 import warnings
15 15
16 16 from subprocess import PIPE, CalledProcessError, check_output
17 17 from tempfile import NamedTemporaryFile
18 18 from textwrap import dedent
19 19 from unittest.mock import patch
20 20
21 21 import nose.tools as nt
22 22
23 23 from IPython.core import debugger
24 24 from IPython.testing import IPYTHON_TESTING_TIMEOUT_SCALE
25 25 from IPython.testing.decorators import skip_win32
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Helper classes, from CPython's Pdb test suite
29 29 #-----------------------------------------------------------------------------
30 30
31 31 class _FakeInput(object):
32 32 """
33 33 A fake input stream for pdb's interactive debugger. Whenever a
34 34 line is read, print it (to simulate the user typing it), and then
35 35 return it. The set of lines to return is specified in the
36 36 constructor; they should not have trailing newlines.
37 37 """
38 38 def __init__(self, lines):
39 39 self.lines = iter(lines)
40 40
41 41 def readline(self):
42 42 line = next(self.lines)
43 43 print(line)
44 44 return line+'\n'
45 45
46 46 class PdbTestInput(object):
47 47 """Context manager that makes testing Pdb in doctests easier."""
48 48
49 49 def __init__(self, input):
50 50 self.input = input
51 51
52 52 def __enter__(self):
53 53 self.real_stdin = sys.stdin
54 54 sys.stdin = _FakeInput(self.input)
55 55
56 56 def __exit__(self, *exc):
57 57 sys.stdin = self.real_stdin
58 58
59 59 #-----------------------------------------------------------------------------
60 60 # Tests
61 61 #-----------------------------------------------------------------------------
62 62
63 63 def test_longer_repr():
64 64 try:
65 65 from reprlib import repr as trepr # Py 3
66 66 except ImportError:
67 67 from repr import repr as trepr # Py 2
68 68
69 69 a = '1234567890'* 7
70 70 ar = "'1234567890123456789012345678901234567890123456789012345678901234567890'"
71 71 a_trunc = "'123456789012...8901234567890'"
72 72 nt.assert_equal(trepr(a), a_trunc)
73 73 # The creation of our tracer modifies the repr module's repr function
74 74 # in-place, since that global is used directly by the stdlib's pdb module.
75 75 with warnings.catch_warnings():
76 76 warnings.simplefilter('ignore', DeprecationWarning)
77 77 debugger.Tracer()
78 78 nt.assert_equal(trepr(a), ar)
79 79
80 80 def test_ipdb_magics():
81 81 '''Test calling some IPython magics from ipdb.
82 82
83 83 First, set up some test functions and classes which we can inspect.
84 84
85 85 >>> class ExampleClass(object):
86 86 ... """Docstring for ExampleClass."""
87 87 ... def __init__(self):
88 88 ... """Docstring for ExampleClass.__init__"""
89 89 ... pass
90 90 ... def __str__(self):
91 91 ... return "ExampleClass()"
92 92
93 93 >>> def example_function(x, y, z="hello"):
94 94 ... """Docstring for example_function."""
95 95 ... pass
96 96
97 97 >>> old_trace = sys.gettrace()
98 98
99 99 Create a function which triggers ipdb.
100 100
101 101 >>> def trigger_ipdb():
102 102 ... a = ExampleClass()
103 103 ... debugger.Pdb().set_trace()
104 104
105 105 >>> with PdbTestInput([
106 106 ... 'pdef example_function',
107 107 ... 'pdoc ExampleClass',
108 108 ... 'up',
109 109 ... 'down',
110 110 ... 'list',
111 111 ... 'pinfo a',
112 112 ... 'll',
113 113 ... 'continue',
114 114 ... ]):
115 115 ... trigger_ipdb()
116 116 --Return--
117 117 None
118 118 > <doctest ...>(3)trigger_ipdb()
119 119 1 def trigger_ipdb():
120 120 2 a = ExampleClass()
121 121 ----> 3 debugger.Pdb().set_trace()
122 122 <BLANKLINE>
123 123 ipdb> pdef example_function
124 124 example_function(x, y, z='hello')
125 125 ipdb> pdoc ExampleClass
126 126 Class docstring:
127 127 Docstring for ExampleClass.
128 128 Init docstring:
129 129 Docstring for ExampleClass.__init__
130 130 ipdb> up
131 131 > <doctest ...>(11)<module>()
132 132 7 'pinfo a',
133 133 8 'll',
134 134 9 'continue',
135 135 10 ]):
136 136 ---> 11 trigger_ipdb()
137 137 <BLANKLINE>
138 138 ipdb> down
139 139 None
140 140 > <doctest ...>(3)trigger_ipdb()
141 141 1 def trigger_ipdb():
142 142 2 a = ExampleClass()
143 143 ----> 3 debugger.Pdb().set_trace()
144 144 <BLANKLINE>
145 145 ipdb> list
146 146 1 def trigger_ipdb():
147 147 2 a = ExampleClass()
148 148 ----> 3 debugger.Pdb().set_trace()
149 149 <BLANKLINE>
150 150 ipdb> pinfo a
151 151 Type: ExampleClass
152 152 String form: ExampleClass()
153 153 Namespace: Local...
154 154 Docstring: Docstring for ExampleClass.
155 155 Init docstring: Docstring for ExampleClass.__init__
156 156 ipdb> ll
157 157 1 def trigger_ipdb():
158 158 2 a = ExampleClass()
159 159 ----> 3 debugger.Pdb().set_trace()
160 160 <BLANKLINE>
161 161 ipdb> continue
162 162
163 163 Restore previous trace function, e.g. for coverage.py
164 164
165 165 >>> sys.settrace(old_trace)
166 166 '''
167 167
168 168 def test_ipdb_magics2():
169 169 '''Test ipdb with a very short function.
170 170
171 171 >>> old_trace = sys.gettrace()
172 172
173 173 >>> def bar():
174 174 ... pass
175 175
176 176 Run ipdb.
177 177
178 178 >>> with PdbTestInput([
179 179 ... 'continue',
180 180 ... ]):
181 181 ... debugger.Pdb().runcall(bar)
182 182 > <doctest ...>(2)bar()
183 183 1 def bar():
184 184 ----> 2 pass
185 185 <BLANKLINE>
186 186 ipdb> continue
187 187
188 188 Restore previous trace function, e.g. for coverage.py
189 189
190 190 >>> sys.settrace(old_trace)
191 191 '''
192 192
193 193 def can_quit():
194 194 '''Test that quit work in ipydb
195 195
196 196 >>> old_trace = sys.gettrace()
197 197
198 198 >>> def bar():
199 199 ... pass
200 200
201 201 >>> with PdbTestInput([
202 202 ... 'quit',
203 203 ... ]):
204 204 ... debugger.Pdb().runcall(bar)
205 205 > <doctest ...>(2)bar()
206 206 1 def bar():
207 207 ----> 2 pass
208 208 <BLANKLINE>
209 209 ipdb> quit
210 210
211 211 Restore previous trace function, e.g. for coverage.py
212 212
213 213 >>> sys.settrace(old_trace)
214 214 '''
215 215
216 216
217 217 def can_exit():
218 218 '''Test that quit work in ipydb
219 219
220 220 >>> old_trace = sys.gettrace()
221 221
222 222 >>> def bar():
223 223 ... pass
224 224
225 225 >>> with PdbTestInput([
226 226 ... 'exit',
227 227 ... ]):
228 228 ... debugger.Pdb().runcall(bar)
229 229 > <doctest ...>(2)bar()
230 230 1 def bar():
231 231 ----> 2 pass
232 232 <BLANKLINE>
233 233 ipdb> exit
234 234
235 235 Restore previous trace function, e.g. for coverage.py
236 236
237 237 >>> sys.settrace(old_trace)
238 238 '''
239 239
240 240
241 241 def test_interruptible_core_debugger():
242 242 """The debugger can be interrupted.
243 243
244 244 The presumption is there is some mechanism that causes a KeyboardInterrupt
245 245 (this is implemented in ipykernel). We want to ensure the
246 246 KeyboardInterrupt cause debugging to cease.
247 247 """
248 248 def raising_input(msg="", called=[0]):
249 249 called[0] += 1
250 250 if called[0] == 1:
251 251 raise KeyboardInterrupt()
252 252 else:
253 253 raise AssertionError("input() should only be called once!")
254 254
255 255 with patch.object(builtins, "input", raising_input):
256 256 debugger.InterruptiblePdb().set_trace()
257 257 # The way this test will fail is by set_trace() never exiting,
258 258 # resulting in a timeout by the test runner. The alternative
259 259 # implementation would involve a subprocess, but that adds issues with
260 260 # interrupting subprocesses that are rather complex, so it's simpler
261 261 # just to do it this way.
262 262
263 263 @skip_win32
264 264 def test_xmode_skip():
265 265 """that xmode skip frames
266 266
267 267 Not as a doctest as pytest does not run doctests.
268 268 """
269 269 import pexpect
270 270 env = os.environ.copy()
271 271 env["IPY_TEST_SIMPLE_PROMPT"] = "1"
272 272
273 273 child = pexpect.spawn(
274 274 sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env
275 275 )
276 276 child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE
277 277
278 278 child.expect("IPython")
279 279 child.expect("\n")
280 280 child.expect_exact("In [1]")
281 281
282 282 block = dedent(
283 283 """
284 284 def f():
285 285 __tracebackhide__ = True
286 286 g()
287 287
288 288 def g():
289 289 raise ValueError
290 290
291 291 f()
292 292 """
293 293 )
294 294
295 295 for line in block.splitlines():
296 296 child.sendline(line)
297 297 child.expect_exact(line)
298 298 child.expect_exact("skipping")
299 299
300 300 block = dedent(
301 301 """
302 302 def f():
303 303 __tracebackhide__ = True
304 304 g()
305 305
306 306 def g():
307 307 from IPython.core.debugger import set_trace
308 308 set_trace()
309 309
310 310 f()
311 311 """
312 312 )
313 313
314 314 for line in block.splitlines():
315 315 child.sendline(line)
316 316 child.expect_exact(line)
317 317
318 318 child.expect("ipdb>")
319 319 child.sendline("w")
320 320 child.expect("hidden")
321 321 child.expect("ipdb>")
322 322 child.sendline("skip_hidden false")
323 323 child.sendline("w")
324 324 child.expect("__traceba")
325 325 child.expect("ipdb>")
326 326
327 327 child.close()
328 328
329 329
330 330 skip_decorators_blocks = (
331 331 """
332 332 def helpers_helper():
333 333 pass # should not stop here except breakpoint
334 334 """,
335 335 """
336 336 def helper_1():
337 337 helpers_helper() # should not stop here
338 338 """,
339 339 """
340 340 def helper_2():
341 341 pass # should not stop here
342 342 """,
343 343 """
344 344 def pdb_skipped_decorator2(function):
345 345 def wrapped_fn(*args, **kwargs):
346 346 __debuggerskip__ = True
347 347 helper_2()
348 348 __debuggerskip__ = False
349 349 result = function(*args, **kwargs)
350 350 __debuggerskip__ = True
351 351 helper_2()
352 352 return result
353 353 return wrapped_fn
354 354 """,
355 355 """
356 356 def pdb_skipped_decorator(function):
357 357 def wrapped_fn(*args, **kwargs):
358 358 __debuggerskip__ = True
359 359 helper_1()
360 360 __debuggerskip__ = False
361 361 result = function(*args, **kwargs)
362 362 __debuggerskip__ = True
363 363 helper_2()
364 364 return result
365 365 return wrapped_fn
366 366 """,
367 367 """
368 368 @pdb_skipped_decorator
369 369 @pdb_skipped_decorator2
370 370 def bar(x, y):
371 371 return x * y
372 372 """,
373 373 """import IPython.terminal.debugger as ipdb""",
374 374 """
375 375 def f():
376 376 ipdb.set_trace()
377 377 bar(3, 4)
378 378 """,
379 379 """
380 380 f()
381 381 """,
382 382 )
383 383
384 384
385 385 def _decorator_skip_setup():
386 386 import pexpect
387 387
388 388 env = os.environ.copy()
389 389 env["IPY_TEST_SIMPLE_PROMPT"] = "1"
390 390
391 391 child = pexpect.spawn(
392 392 sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env
393 393 )
394 child.str_last_chars = 1000
394 395 child.timeout = 5 * IPYTHON_TESTING_TIMEOUT_SCALE
395 396
396 397 child.expect("IPython")
397 398 child.expect("\n")
398 399
399 400 dedented_blocks = [dedent(b).strip() for b in skip_decorators_blocks]
400 401 in_prompt_number = 1
401 402 for cblock in dedented_blocks:
402 403 child.expect_exact(f"In [{in_prompt_number}]:")
403 404 in_prompt_number += 1
404 405 for line in cblock.splitlines():
405 406 child.sendline(line)
406 407 child.expect_exact(line)
407 408 child.sendline("")
408 409 return child
409 410
410 411
411 412 @skip_win32
412 413 def test_decorator_skip():
413 414 """test that decorator frames can be skipped."""
414 415
415 416 child = _decorator_skip_setup()
416 417
417 418 child.expect_exact("3 bar(3, 4)")
418 419 child.expect("ipdb>")
419 420
420 421 child.expect("ipdb>")
421 422 child.sendline("step")
422 423 child.expect_exact("step")
423 424
424 425 child.expect_exact("1 @pdb_skipped_decorator")
425 426
426 427 child.sendline("s")
427 428 child.expect_exact("return x * y")
428 429
429 430 child.close()
430 431
431 432
432 433 @skip_win32
433 434 def test_decorator_skip_disabled():
434 435 """test that decorator frame skipping can be disabled"""
435 436
436 437 child = _decorator_skip_setup()
437 438
438 439 child.expect_exact("3 bar(3, 4)")
439 440
440 441 for input_, expected in [
441 442 ("skip_predicates debuggerskip False", ""),
442 443 ("skip_predicates", "debuggerskip : False"),
443 444 ("step", "---> 2 def wrapped_fn"),
444 445 ("step", "----> 3 __debuggerskip__"),
445 446 ("step", "----> 4 helper_1()"),
446 447 ("step", "---> 1 def helper_1():"),
447 448 ("next", "----> 2 helpers_helper()"),
448 449 ("next", "--Return--"),
449 450 ("next", "----> 5 __debuggerskip__ = False"),
450 451 ]:
451 452 child.expect("ipdb>")
452 453 child.sendline(input_)
453 454 child.expect_exact(input_)
454 455 child.expect_exact(expected)
455 456
456 457 child.close()
457 458
458 459
459 460 @skip_win32
460 461 def test_decorator_skip_with_breakpoint():
461 462 """test that decorator frame skipping can be disabled"""
462 463
463 464 import pexpect
464 465
465 466 env = os.environ.copy()
466 467 env["IPY_TEST_SIMPLE_PROMPT"] = "1"
467 468
468 469 child = pexpect.spawn(
469 470 sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env
470 471 )
471 472 child.timeout = 5 * IPYTHON_TESTING_TIMEOUT_SCALE
472 473
473 474 child.expect("IPython")
474 475 child.expect("\n")
475 476
476 477 ### we need a filename, so we need to exec the full block with a filename
477 478 with NamedTemporaryFile(suffix=".py", dir=".", delete=True) as tf:
478 479
479 480 name = tf.name[:-3].split("/")[-1]
480 481 tf.write("\n".join([dedent(x) for x in skip_decorators_blocks[:-1]]).encode())
481 482 tf.flush()
482 483 codeblock = f"from {name} import f"
483 484
484 485 dedented_blocks = [
485 486 codeblock,
486 487 "f()",
487 488 ]
488 489
489 490 in_prompt_number = 1
490 491 for cblock in dedented_blocks:
491 492 child.expect_exact(f"In [{in_prompt_number}]:")
492 493 in_prompt_number += 1
493 494 for line in cblock.splitlines():
494 495 child.sendline(line)
495 496 child.expect_exact(line)
496 497 child.sendline("")
497 498
498 499 # as the filename does not exists, we'll rely on the filename prompt
499 500 child.expect_exact("47 bar(3, 4)")
500 501
501 502 for input_, expected in [
502 503 (f"b {name}.py:3", ""),
503 504 ("step", "1---> 3 pass # should not stop here except"),
504 505 ("step", "---> 38 @pdb_skipped_decorator"),
505 506 ("continue", ""),
506 507 ]:
507 508 child.expect("ipdb>")
508 509 child.sendline(input_)
509 510 child.expect_exact(input_)
510 511 child.expect_exact(expected)
511 512
512 513 child.close()
513 514
514 515
515 516 @skip_win32
516 517 def test_where_erase_value():
517 518 """Test that `where` does not access f_locals and erase values."""
518 519 import pexpect
519 520
520 521 env = os.environ.copy()
521 522 env["IPY_TEST_SIMPLE_PROMPT"] = "1"
522 523
523 524 child = pexpect.spawn(
524 525 sys.executable, ["-m", "IPython", "--colors=nocolor"], env=env
525 526 )
526 527 child.timeout = 15 * IPYTHON_TESTING_TIMEOUT_SCALE
527 528
528 529 child.expect("IPython")
529 530 child.expect("\n")
530 531 child.expect_exact("In [1]")
531 532
532 533 block = dedent(
533 534 """
534 535 def simple_f():
535 536 myvar = 1
536 537 print(myvar)
537 538 1/0
538 539 print(myvar)
539 540 simple_f() """
540 541 )
541 542
542 543 for line in block.splitlines():
543 544 child.sendline(line)
544 545 child.expect_exact(line)
545 546 child.expect_exact("ZeroDivisionError")
546 547 child.expect_exact("In [2]:")
547 548
548 549 child.sendline("%debug")
549 550
550 551 ##
551 552 child.expect("ipdb>")
552 553
553 554 child.sendline("myvar")
554 555 child.expect("1")
555 556
556 557 ##
557 558 child.expect("ipdb>")
558 559
559 560 child.sendline("myvar = 2")
560 561
561 562 ##
562 563 child.expect_exact("ipdb>")
563 564
564 565 child.sendline("myvar")
565 566
566 567 child.expect_exact("2")
567 568
568 569 ##
569 570 child.expect("ipdb>")
570 571 child.sendline("where")
571 572
572 573 ##
573 574 child.expect("ipdb>")
574 575 child.sendline("myvar")
575 576
576 577 child.expect_exact("2")
577 578 child.expect("ipdb>")
578 579
579 580 child.close()
General Comments 0
You need to be logged in to leave comments. Login now