##// END OF EJS Templates
Backport PR #12456: Allow to mark transformers as having side effects
Matthias Bussonnier -
Show More
@@ -0,0 +1,2 b''
1 input_transformers can now have an attribute ``has_side_effects`` set to `True`, which will prevent the
2 transformers from being ran when IPython is trying to guess whether the user input is complete.
@@ -1,721 +1,723 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, Union
17 17 import warnings
18 18
19 19 _indent_re = re.compile(r'^[ \t]+')
20 20
21 21 def leading_empty_lines(lines):
22 22 """Remove leading empty lines
23 23
24 24 If the leading lines are empty or contain only whitespace, they will be
25 25 removed.
26 26 """
27 27 if not lines:
28 28 return lines
29 29 for i, line in enumerate(lines):
30 30 if line and not line.isspace():
31 31 return lines[i:]
32 32 return lines
33 33
34 34 def leading_indent(lines):
35 35 """Remove leading indentation.
36 36
37 37 If the first line starts with a spaces or tabs, the same whitespace will be
38 38 removed from each following line in the cell.
39 39 """
40 40 if not lines:
41 41 return lines
42 42 m = _indent_re.match(lines[0])
43 43 if not m:
44 44 return lines
45 45 space = m.group(0)
46 46 n = len(space)
47 47 return [l[n:] if l.startswith(space) else l
48 48 for l in lines]
49 49
50 50 class PromptStripper:
51 51 """Remove matching input prompts from a block of input.
52 52
53 53 Parameters
54 54 ----------
55 55 prompt_re : regular expression
56 56 A regular expression matching any input prompt (including continuation,
57 57 e.g. ``...``)
58 58 initial_re : regular expression, optional
59 59 A regular expression matching only the initial prompt, but not continuation.
60 60 If no initial expression is given, prompt_re will be used everywhere.
61 61 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
62 62 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
63 63
64 64 If initial_re and prompt_re differ,
65 65 only initial_re will be tested against the first line.
66 66 If any prompt is found on the first two lines,
67 67 prompts will be stripped from the rest of the block.
68 68 """
69 69 def __init__(self, prompt_re, initial_re=None):
70 70 self.prompt_re = prompt_re
71 71 self.initial_re = initial_re or prompt_re
72 72
73 73 def _strip(self, lines):
74 74 return [self.prompt_re.sub('', l, count=1) for l in lines]
75 75
76 76 def __call__(self, lines):
77 77 if not lines:
78 78 return lines
79 79 if self.initial_re.match(lines[0]) or \
80 80 (len(lines) > 1 and self.prompt_re.match(lines[1])):
81 81 return self._strip(lines)
82 82 return lines
83 83
84 84 classic_prompt = PromptStripper(
85 85 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
86 86 initial_re=re.compile(r'^>>>( |$)')
87 87 )
88 88
89 89 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
90 90
91 91 def cell_magic(lines):
92 92 if not lines or not lines[0].startswith('%%'):
93 93 return lines
94 94 if re.match(r'%%\w+\?', lines[0]):
95 95 # This case will be handled by help_end
96 96 return lines
97 97 magic_name, _, first_line = lines[0][2:-1].partition(' ')
98 98 body = ''.join(lines[1:])
99 99 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
100 100 % (magic_name, first_line, body)]
101 101
102 102
103 103 def _find_assign_op(token_line) -> Union[int, None]:
104 104 """Get the index of the first assignment in the line ('=' not inside brackets)
105 105
106 106 Note: We don't try to support multiple special assignment (a = b = %foo)
107 107 """
108 108 paren_level = 0
109 109 for i, ti in enumerate(token_line):
110 110 s = ti.string
111 111 if s == '=' and paren_level == 0:
112 112 return i
113 113 if s in {'(','[','{'}:
114 114 paren_level += 1
115 115 elif s in {')', ']', '}'}:
116 116 if paren_level > 0:
117 117 paren_level -= 1
118 118
119 119 def find_end_of_continued_line(lines, start_line: int):
120 120 """Find the last line of a line explicitly extended using backslashes.
121 121
122 122 Uses 0-indexed line numbers.
123 123 """
124 124 end_line = start_line
125 125 while lines[end_line].endswith('\\\n'):
126 126 end_line += 1
127 127 if end_line >= len(lines):
128 128 break
129 129 return end_line
130 130
131 131 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
132 132 r"""Assemble a single line from multiple continued line pieces
133 133
134 134 Continued lines are lines ending in ``\``, and the line following the last
135 135 ``\`` in the block.
136 136
137 137 For example, this code continues over multiple lines::
138 138
139 139 if (assign_ix is not None) \
140 140 and (len(line) >= assign_ix + 2) \
141 141 and (line[assign_ix+1].string == '%') \
142 142 and (line[assign_ix+2].type == tokenize.NAME):
143 143
144 144 This statement contains four continued line pieces.
145 145 Assembling these pieces into a single line would give::
146 146
147 147 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
148 148
149 149 This uses 0-indexed line numbers. *start* is (lineno, colno).
150 150
151 151 Used to allow ``%magic`` and ``!system`` commands to be continued over
152 152 multiple lines.
153 153 """
154 154 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
155 155 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
156 156 + [parts[-1][:-1]]) # Strip newline from last line
157 157
158 158 class TokenTransformBase:
159 159 """Base class for transformations which examine tokens.
160 160
161 161 Special syntax should not be transformed when it occurs inside strings or
162 162 comments. This is hard to reliably avoid with regexes. The solution is to
163 163 tokenise the code as Python, and recognise the special syntax in the tokens.
164 164
165 165 IPython's special syntax is not valid Python syntax, so tokenising may go
166 166 wrong after the special syntax starts. These classes therefore find and
167 167 transform *one* instance of special syntax at a time into regular Python
168 168 syntax. After each transformation, tokens are regenerated to find the next
169 169 piece of special syntax.
170 170
171 171 Subclasses need to implement one class method (find)
172 172 and one regular method (transform).
173 173
174 174 The priority attribute can select which transformation to apply if multiple
175 175 transformers match in the same place. Lower numbers have higher priority.
176 176 This allows "%magic?" to be turned into a help call rather than a magic call.
177 177 """
178 178 # Lower numbers -> higher priority (for matches in the same location)
179 179 priority = 10
180 180
181 181 def sortby(self):
182 182 return self.start_line, self.start_col, self.priority
183 183
184 184 def __init__(self, start):
185 185 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
186 186 self.start_col = start[1]
187 187
188 188 @classmethod
189 189 def find(cls, tokens_by_line):
190 190 """Find one instance of special syntax in the provided tokens.
191 191
192 192 Tokens are grouped into logical lines for convenience,
193 193 so it is easy to e.g. look at the first token of each line.
194 194 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
195 195
196 196 This should return an instance of its class, pointing to the start
197 197 position it has found, or None if it found no match.
198 198 """
199 199 raise NotImplementedError
200 200
201 201 def transform(self, lines: List[str]):
202 202 """Transform one instance of special syntax found by ``find()``
203 203
204 204 Takes a list of strings representing physical lines,
205 205 returns a similar list of transformed lines.
206 206 """
207 207 raise NotImplementedError
208 208
209 209 class MagicAssign(TokenTransformBase):
210 210 """Transformer for assignments from magics (a = %foo)"""
211 211 @classmethod
212 212 def find(cls, tokens_by_line):
213 213 """Find the first magic assignment (a = %foo) in the cell.
214 214 """
215 215 for line in tokens_by_line:
216 216 assign_ix = _find_assign_op(line)
217 217 if (assign_ix is not None) \
218 218 and (len(line) >= assign_ix + 2) \
219 219 and (line[assign_ix+1].string == '%') \
220 220 and (line[assign_ix+2].type == tokenize.NAME):
221 221 return cls(line[assign_ix+1].start)
222 222
223 223 def transform(self, lines: List[str]):
224 224 """Transform a magic assignment found by the ``find()`` classmethod.
225 225 """
226 226 start_line, start_col = self.start_line, self.start_col
227 227 lhs = lines[start_line][:start_col]
228 228 end_line = find_end_of_continued_line(lines, start_line)
229 229 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
230 230 assert rhs.startswith('%'), rhs
231 231 magic_name, _, args = rhs[1:].partition(' ')
232 232
233 233 lines_before = lines[:start_line]
234 234 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
235 235 new_line = lhs + call + '\n'
236 236 lines_after = lines[end_line+1:]
237 237
238 238 return lines_before + [new_line] + lines_after
239 239
240 240
241 241 class SystemAssign(TokenTransformBase):
242 242 """Transformer for assignments from system commands (a = !foo)"""
243 243 @classmethod
244 244 def find(cls, tokens_by_line):
245 245 """Find the first system assignment (a = !foo) in the cell.
246 246 """
247 247 for line in tokens_by_line:
248 248 assign_ix = _find_assign_op(line)
249 249 if (assign_ix is not None) \
250 250 and not line[assign_ix].line.strip().startswith('=') \
251 251 and (len(line) >= assign_ix + 2) \
252 252 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
253 253 ix = assign_ix + 1
254 254
255 255 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
256 256 if line[ix].string == '!':
257 257 return cls(line[ix].start)
258 258 elif not line[ix].string.isspace():
259 259 break
260 260 ix += 1
261 261
262 262 def transform(self, lines: List[str]):
263 263 """Transform a system assignment found by the ``find()`` classmethod.
264 264 """
265 265 start_line, start_col = self.start_line, self.start_col
266 266
267 267 lhs = lines[start_line][:start_col]
268 268 end_line = find_end_of_continued_line(lines, start_line)
269 269 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
270 270 assert rhs.startswith('!'), rhs
271 271 cmd = rhs[1:]
272 272
273 273 lines_before = lines[:start_line]
274 274 call = "get_ipython().getoutput({!r})".format(cmd)
275 275 new_line = lhs + call + '\n'
276 276 lines_after = lines[end_line + 1:]
277 277
278 278 return lines_before + [new_line] + lines_after
279 279
280 280 # The escape sequences that define the syntax transformations IPython will
281 281 # apply to user input. These can NOT be just changed here: many regular
282 282 # expressions and other parts of the code may use their hardcoded values, and
283 283 # for all intents and purposes they constitute the 'IPython syntax', so they
284 284 # should be considered fixed.
285 285
286 286 ESC_SHELL = '!' # Send line to underlying system shell
287 287 ESC_SH_CAP = '!!' # Send line to system shell and capture output
288 288 ESC_HELP = '?' # Find information about object
289 289 ESC_HELP2 = '??' # Find extra-detailed information about object
290 290 ESC_MAGIC = '%' # Call magic function
291 291 ESC_MAGIC2 = '%%' # Call cell-magic function
292 292 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
293 293 ESC_QUOTE2 = ';' # Quote all args as a single string, call
294 294 ESC_PAREN = '/' # Call first argument with rest of line as arguments
295 295
296 296 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
297 297 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
298 298
299 299 def _make_help_call(target, esc, next_input=None):
300 300 """Prepares a pinfo(2)/psearch call from a target name and the escape
301 301 (i.e. ? or ??)"""
302 302 method = 'pinfo2' if esc == '??' \
303 303 else 'psearch' if '*' in target \
304 304 else 'pinfo'
305 305 arg = " ".join([method, target])
306 306 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
307 307 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
308 308 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
309 309 if next_input is None:
310 310 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
311 311 else:
312 312 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
313 313 (next_input, t_magic_name, t_magic_arg_s)
314 314
315 315 def _tr_help(content):
316 316 """Translate lines escaped with: ?
317 317
318 318 A naked help line should fire the intro help screen (shell.show_usage())
319 319 """
320 320 if not content:
321 321 return 'get_ipython().show_usage()'
322 322
323 323 return _make_help_call(content, '?')
324 324
325 325 def _tr_help2(content):
326 326 """Translate lines escaped with: ??
327 327
328 328 A naked help line should fire the intro help screen (shell.show_usage())
329 329 """
330 330 if not content:
331 331 return 'get_ipython().show_usage()'
332 332
333 333 return _make_help_call(content, '??')
334 334
335 335 def _tr_magic(content):
336 336 "Translate lines escaped with a percent sign: %"
337 337 name, _, args = content.partition(' ')
338 338 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
339 339
340 340 def _tr_quote(content):
341 341 "Translate lines escaped with a comma: ,"
342 342 name, _, args = content.partition(' ')
343 343 return '%s("%s")' % (name, '", "'.join(args.split()) )
344 344
345 345 def _tr_quote2(content):
346 346 "Translate lines escaped with a semicolon: ;"
347 347 name, _, args = content.partition(' ')
348 348 return '%s("%s")' % (name, args)
349 349
350 350 def _tr_paren(content):
351 351 "Translate lines escaped with a slash: /"
352 352 name, _, args = content.partition(' ')
353 353 return '%s(%s)' % (name, ", ".join(args.split()))
354 354
355 355 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
356 356 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
357 357 ESC_HELP : _tr_help,
358 358 ESC_HELP2 : _tr_help2,
359 359 ESC_MAGIC : _tr_magic,
360 360 ESC_QUOTE : _tr_quote,
361 361 ESC_QUOTE2 : _tr_quote2,
362 362 ESC_PAREN : _tr_paren }
363 363
364 364 class EscapedCommand(TokenTransformBase):
365 365 """Transformer for escaped commands like %foo, !foo, or /foo"""
366 366 @classmethod
367 367 def find(cls, tokens_by_line):
368 368 """Find the first escaped command (%foo, !foo, etc.) in the cell.
369 369 """
370 370 for line in tokens_by_line:
371 371 if not line:
372 372 continue
373 373 ix = 0
374 374 ll = len(line)
375 375 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
376 376 ix += 1
377 377 if ix >= ll:
378 378 continue
379 379 if line[ix].string in ESCAPE_SINGLES:
380 380 return cls(line[ix].start)
381 381
382 382 def transform(self, lines):
383 383 """Transform an escaped line found by the ``find()`` classmethod.
384 384 """
385 385 start_line, start_col = self.start_line, self.start_col
386 386
387 387 indent = lines[start_line][:start_col]
388 388 end_line = find_end_of_continued_line(lines, start_line)
389 389 line = assemble_continued_line(lines, (start_line, start_col), end_line)
390 390
391 391 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
392 392 escape, content = line[:2], line[2:]
393 393 else:
394 394 escape, content = line[:1], line[1:]
395 395
396 396 if escape in tr:
397 397 call = tr[escape](content)
398 398 else:
399 399 call = ''
400 400
401 401 lines_before = lines[:start_line]
402 402 new_line = indent + call + '\n'
403 403 lines_after = lines[end_line + 1:]
404 404
405 405 return lines_before + [new_line] + lines_after
406 406
407 407 _help_end_re = re.compile(r"""(%{0,2}
408 408 (?!\d)[\w*]+ # Variable name
409 409 (\.(?!\d)[\w*]+)* # .etc.etc
410 410 )
411 411 (\?\??)$ # ? or ??
412 412 """,
413 413 re.VERBOSE)
414 414
415 415 class HelpEnd(TokenTransformBase):
416 416 """Transformer for help syntax: obj? and obj??"""
417 417 # This needs to be higher priority (lower number) than EscapedCommand so
418 418 # that inspecting magics (%foo?) works.
419 419 priority = 5
420 420
421 421 def __init__(self, start, q_locn):
422 422 super().__init__(start)
423 423 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
424 424 self.q_col = q_locn[1]
425 425
426 426 @classmethod
427 427 def find(cls, tokens_by_line):
428 428 """Find the first help command (foo?) in the cell.
429 429 """
430 430 for line in tokens_by_line:
431 431 # Last token is NEWLINE; look at last but one
432 432 if len(line) > 2 and line[-2].string == '?':
433 433 # Find the first token that's not INDENT/DEDENT
434 434 ix = 0
435 435 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
436 436 ix += 1
437 437 return cls(line[ix].start, line[-2].start)
438 438
439 439 def transform(self, lines):
440 440 """Transform a help command found by the ``find()`` classmethod.
441 441 """
442 442 piece = ''.join(lines[self.start_line:self.q_line+1])
443 443 indent, content = piece[:self.start_col], piece[self.start_col:]
444 444 lines_before = lines[:self.start_line]
445 445 lines_after = lines[self.q_line + 1:]
446 446
447 447 m = _help_end_re.search(content)
448 448 if not m:
449 449 raise SyntaxError(content)
450 450 assert m is not None, content
451 451 target = m.group(1)
452 452 esc = m.group(3)
453 453
454 454 # If we're mid-command, put it back on the next prompt for the user.
455 455 next_input = None
456 456 if (not lines_before) and (not lines_after) \
457 457 and content.strip() != m.group(0):
458 458 next_input = content.rstrip('?\n')
459 459
460 460 call = _make_help_call(target, esc, next_input=next_input)
461 461 new_line = indent + call + '\n'
462 462
463 463 return lines_before + [new_line] + lines_after
464 464
465 465 def make_tokens_by_line(lines:List[str]):
466 466 """Tokenize a series of lines and group tokens by line.
467 467
468 468 The tokens for a multiline Python string or expression are grouped as one
469 469 line. All lines except the last lines should keep their line ending ('\\n',
470 470 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
471 471 for example when passing block of text to this function.
472 472
473 473 """
474 474 # NL tokens are used inside multiline expressions, but also after blank
475 475 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
476 476 # We want to group the former case together but split the latter, so we
477 477 # track parentheses level, similar to the internals of tokenize.
478 478 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL
479 479 tokens_by_line = [[]]
480 480 if len(lines) > 1 and not lines[0].endswith(('\n', '\r', '\r\n', '\x0b', '\x0c')):
481 481 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")
482 482 parenlev = 0
483 483 try:
484 484 for token in tokenize.generate_tokens(iter(lines).__next__):
485 485 tokens_by_line[-1].append(token)
486 486 if (token.type == NEWLINE) \
487 487 or ((token.type == NL) and (parenlev <= 0)):
488 488 tokens_by_line.append([])
489 489 elif token.string in {'(', '[', '{'}:
490 490 parenlev += 1
491 491 elif token.string in {')', ']', '}'}:
492 492 if parenlev > 0:
493 493 parenlev -= 1
494 494 except tokenize.TokenError:
495 495 # Input ended in a multiline string or expression. That's OK for us.
496 496 pass
497 497
498 498
499 499 if not tokens_by_line[-1]:
500 500 tokens_by_line.pop()
501 501
502 502
503 503 return tokens_by_line
504 504
505 505 def show_linewise_tokens(s: str):
506 506 """For investigation and debugging"""
507 507 if not s.endswith('\n'):
508 508 s += '\n'
509 509 lines = s.splitlines(keepends=True)
510 510 for line in make_tokens_by_line(lines):
511 511 print("Line -------")
512 512 for tokinfo in line:
513 513 print(" ", tokinfo)
514 514
515 515 # Arbitrary limit to prevent getting stuck in infinite loops
516 516 TRANSFORM_LOOP_LIMIT = 500
517 517
518 518 class TransformerManager:
519 519 """Applies various transformations to a cell or code block.
520 520
521 521 The key methods for external use are ``transform_cell()``
522 522 and ``check_complete()``.
523 523 """
524 524 def __init__(self):
525 525 self.cleanup_transforms = [
526 526 leading_empty_lines,
527 527 leading_indent,
528 528 classic_prompt,
529 529 ipython_prompt,
530 530 ]
531 531 self.line_transforms = [
532 532 cell_magic,
533 533 ]
534 534 self.token_transformers = [
535 535 MagicAssign,
536 536 SystemAssign,
537 537 EscapedCommand,
538 538 HelpEnd,
539 539 ]
540 540
541 541 def do_one_token_transform(self, lines):
542 542 """Find and run the transform earliest in the code.
543 543
544 544 Returns (changed, lines).
545 545
546 546 This method is called repeatedly until changed is False, indicating
547 547 that all available transformations are complete.
548 548
549 549 The tokens following IPython special syntax might not be valid, so
550 550 the transformed code is retokenised every time to identify the next
551 551 piece of special syntax. Hopefully long code cells are mostly valid
552 552 Python, not using lots of IPython special syntax, so this shouldn't be
553 553 a performance issue.
554 554 """
555 555 tokens_by_line = make_tokens_by_line(lines)
556 556 candidates = []
557 557 for transformer_cls in self.token_transformers:
558 558 transformer = transformer_cls.find(tokens_by_line)
559 559 if transformer:
560 560 candidates.append(transformer)
561 561
562 562 if not candidates:
563 563 # Nothing to transform
564 564 return False, lines
565 565 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
566 566 for transformer in ordered_transformers:
567 567 try:
568 568 return True, transformer.transform(lines)
569 569 except SyntaxError:
570 570 pass
571 571 return False, lines
572 572
573 573 def do_token_transforms(self, lines):
574 574 for _ in range(TRANSFORM_LOOP_LIMIT):
575 575 changed, lines = self.do_one_token_transform(lines)
576 576 if not changed:
577 577 return lines
578 578
579 579 raise RuntimeError("Input transformation still changing after "
580 580 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
581 581
582 582 def transform_cell(self, cell: str) -> str:
583 583 """Transforms a cell of input code"""
584 584 if not cell.endswith('\n'):
585 585 cell += '\n' # Ensure the cell has a trailing newline
586 586 lines = cell.splitlines(keepends=True)
587 587 for transform in self.cleanup_transforms + self.line_transforms:
588 588 lines = transform(lines)
589 589
590 590 lines = self.do_token_transforms(lines)
591 591 return ''.join(lines)
592 592
593 593 def check_complete(self, cell: str):
594 594 """Return whether a block of code is ready to execute, or should be continued
595 595
596 596 Parameters
597 597 ----------
598 598 source : string
599 599 Python input code, which can be multiline.
600 600
601 601 Returns
602 602 -------
603 603 status : str
604 604 One of 'complete', 'incomplete', or 'invalid' if source is not a
605 605 prefix of valid code.
606 606 indent_spaces : int or None
607 607 The number of spaces by which to indent the next line of code. If
608 608 status is not 'incomplete', this is None.
609 609 """
610 610 # Remember if the lines ends in a new line.
611 611 ends_with_newline = False
612 612 for character in reversed(cell):
613 613 if character == '\n':
614 614 ends_with_newline = True
615 615 break
616 616 elif character.strip():
617 617 break
618 618 else:
619 619 continue
620 620
621 621 if not ends_with_newline:
622 622 # Append an newline for consistent tokenization
623 623 # See https://bugs.python.org/issue33899
624 624 cell += '\n'
625 625
626 626 lines = cell.splitlines(keepends=True)
627 627
628 628 if not lines:
629 629 return 'complete', None
630 630
631 631 if lines[-1].endswith('\\'):
632 632 # Explicit backslash continuation
633 633 return 'incomplete', find_last_indent(lines)
634 634
635 635 try:
636 636 for transform in self.cleanup_transforms:
637 lines = transform(lines)
637 if not getattr(transform, 'has_side_effects', False):
638 lines = transform(lines)
638 639 except SyntaxError:
639 640 return 'invalid', None
640 641
641 642 if lines[0].startswith('%%'):
642 643 # Special case for cell magics - completion marked by blank line
643 644 if lines[-1].strip():
644 645 return 'incomplete', find_last_indent(lines)
645 646 else:
646 647 return 'complete', None
647 648
648 649 try:
649 650 for transform in self.line_transforms:
650 lines = transform(lines)
651 if not getattr(transform, 'has_side_effects', False):
652 lines = transform(lines)
651 653 lines = self.do_token_transforms(lines)
652 654 except SyntaxError:
653 655 return 'invalid', None
654 656
655 657 tokens_by_line = make_tokens_by_line(lines)
656 658
657 659 if not tokens_by_line:
658 660 return 'incomplete', find_last_indent(lines)
659 661
660 662 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
661 663 # We're in a multiline string or expression
662 664 return 'incomplete', find_last_indent(lines)
663 665
664 666 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER}
665 667
666 668 # Pop the last line which only contains DEDENTs and ENDMARKER
667 669 last_token_line = None
668 670 if {t.type for t in tokens_by_line[-1]} in [
669 671 {tokenize.DEDENT, tokenize.ENDMARKER},
670 672 {tokenize.ENDMARKER}
671 673 ] and len(tokens_by_line) > 1:
672 674 last_token_line = tokens_by_line.pop()
673 675
674 676 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
675 677 tokens_by_line[-1].pop()
676 678
677 679 if not tokens_by_line[-1]:
678 680 return 'incomplete', find_last_indent(lines)
679 681
680 682 if tokens_by_line[-1][-1].string == ':':
681 683 # The last line starts a block (e.g. 'if foo:')
682 684 ix = 0
683 685 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
684 686 ix += 1
685 687
686 688 indent = tokens_by_line[-1][ix].start[1]
687 689 return 'incomplete', indent + 4
688 690
689 691 if tokens_by_line[-1][0].line.endswith('\\'):
690 692 return 'incomplete', None
691 693
692 694 # At this point, our checks think the code is complete (or invalid).
693 695 # We'll use codeop.compile_command to check this with the real parser
694 696 try:
695 697 with warnings.catch_warnings():
696 698 warnings.simplefilter('error', SyntaxWarning)
697 699 res = compile_command(''.join(lines), symbol='exec')
698 700 except (SyntaxError, OverflowError, ValueError, TypeError,
699 701 MemoryError, SyntaxWarning):
700 702 return 'invalid', None
701 703 else:
702 704 if res is None:
703 705 return 'incomplete', find_last_indent(lines)
704 706
705 707 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
706 708 if ends_with_newline:
707 709 return 'complete', None
708 710 return 'incomplete', find_last_indent(lines)
709 711
710 712 # If there's a blank line at the end, assume we're ready to execute
711 713 if not lines[-1].strip():
712 714 return 'complete', None
713 715
714 716 return 'complete', None
715 717
716 718
717 719 def find_last_indent(lines):
718 720 m = _indent_re.match(lines[-1])
719 721 if not m:
720 722 return 0
721 723 return len(m.group(0).replace('\t', ' '*4))
@@ -1,292 +1,326 b''
1 1 """Tests for the token-based transformers in IPython.core.inputtransformer2
2 2
3 3 Line-based transformers are the simpler ones; token-based transformers are
4 4 more complex. See test_inputtransformer2_line for tests for line-based
5 5 transformations.
6 6 """
7 7 import nose.tools as nt
8 8 import string
9 9
10 10 from IPython.core import inputtransformer2 as ipt2
11 11 from IPython.core.inputtransformer2 import make_tokens_by_line, _find_assign_op
12 12
13 13 from textwrap import dedent
14 14
15 15 MULTILINE_MAGIC = ("""\
16 16 a = f()
17 17 %foo \\
18 18 bar
19 19 g()
20 20 """.splitlines(keepends=True), (2, 0), """\
21 21 a = f()
22 22 get_ipython().run_line_magic('foo', ' bar')
23 23 g()
24 24 """.splitlines(keepends=True))
25 25
26 26 INDENTED_MAGIC = ("""\
27 27 for a in range(5):
28 28 %ls
29 29 """.splitlines(keepends=True), (2, 4), """\
30 30 for a in range(5):
31 31 get_ipython().run_line_magic('ls', '')
32 32 """.splitlines(keepends=True))
33 33
34 34 MULTILINE_MAGIC_ASSIGN = ("""\
35 35 a = f()
36 36 b = %foo \\
37 37 bar
38 38 g()
39 39 """.splitlines(keepends=True), (2, 4), """\
40 40 a = f()
41 41 b = get_ipython().run_line_magic('foo', ' bar')
42 42 g()
43 43 """.splitlines(keepends=True))
44 44
45 45 MULTILINE_SYSTEM_ASSIGN = ("""\
46 46 a = f()
47 47 b = !foo \\
48 48 bar
49 49 g()
50 50 """.splitlines(keepends=True), (2, 4), """\
51 51 a = f()
52 52 b = get_ipython().getoutput('foo bar')
53 53 g()
54 54 """.splitlines(keepends=True))
55 55
56 56 #####
57 57
58 58 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = ("""\
59 59 def test():
60 60 for i in range(1):
61 61 print(i)
62 62 res =! ls
63 63 """.splitlines(keepends=True), (4, 7), '''\
64 64 def test():
65 65 for i in range(1):
66 66 print(i)
67 67 res =get_ipython().getoutput(\' ls\')
68 68 '''.splitlines(keepends=True))
69 69
70 70 ######
71 71
72 72 AUTOCALL_QUOTE = (
73 73 [",f 1 2 3\n"], (1, 0),
74 74 ['f("1", "2", "3")\n']
75 75 )
76 76
77 77 AUTOCALL_QUOTE2 = (
78 78 [";f 1 2 3\n"], (1, 0),
79 79 ['f("1 2 3")\n']
80 80 )
81 81
82 82 AUTOCALL_PAREN = (
83 83 ["/f 1 2 3\n"], (1, 0),
84 84 ['f(1, 2, 3)\n']
85 85 )
86 86
87 87 SIMPLE_HELP = (
88 88 ["foo?\n"], (1, 0),
89 89 ["get_ipython().run_line_magic('pinfo', 'foo')\n"]
90 90 )
91 91
92 92 DETAILED_HELP = (
93 93 ["foo??\n"], (1, 0),
94 94 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"]
95 95 )
96 96
97 97 MAGIC_HELP = (
98 98 ["%foo?\n"], (1, 0),
99 99 ["get_ipython().run_line_magic('pinfo', '%foo')\n"]
100 100 )
101 101
102 102 HELP_IN_EXPR = (
103 103 ["a = b + c?\n"], (1, 0),
104 104 ["get_ipython().set_next_input('a = b + c');"
105 105 "get_ipython().run_line_magic('pinfo', 'c')\n"]
106 106 )
107 107
108 108 HELP_CONTINUED_LINE = ("""\
109 109 a = \\
110 110 zip?
111 111 """.splitlines(keepends=True), (1, 0),
112 112 [r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
113 113 )
114 114
115 115 HELP_MULTILINE = ("""\
116 116 (a,
117 117 b) = zip?
118 118 """.splitlines(keepends=True), (1, 0),
119 119 [r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
120 120 )
121 121
122 122 HELP_UNICODE = (
123 123 ["π.foo?\n"], (1, 0),
124 124 ["get_ipython().run_line_magic('pinfo', 'π.foo')\n"]
125 125 )
126 126
127 127
128 128 def null_cleanup_transformer(lines):
129 129 """
130 130 A cleanup transform that returns an empty list.
131 131 """
132 132 return []
133 133
134 134 def check_make_token_by_line_never_ends_empty():
135 135 """
136 136 Check that not sequence of single or double characters ends up leading to en empty list of tokens
137 137 """
138 138 from string import printable
139 139 for c in printable:
140 140 nt.assert_not_equal(make_tokens_by_line(c)[-1], [])
141 141 for k in printable:
142 142 nt.assert_not_equal(make_tokens_by_line(c+k)[-1], [])
143 143
144 144 def check_find(transformer, case, match=True):
145 145 sample, expected_start, _ = case
146 146 tbl = make_tokens_by_line(sample)
147 147 res = transformer.find(tbl)
148 148 if match:
149 149 # start_line is stored 0-indexed, expected values are 1-indexed
150 150 nt.assert_equal((res.start_line+1, res.start_col), expected_start)
151 151 return res
152 152 else:
153 153 nt.assert_is(res, None)
154 154
155 155 def check_transform(transformer_cls, case):
156 156 lines, start, expected = case
157 157 transformer = transformer_cls(start)
158 158 nt.assert_equal(transformer.transform(lines), expected)
159 159
160 160 def test_continued_line():
161 161 lines = MULTILINE_MAGIC_ASSIGN[0]
162 162 nt.assert_equal(ipt2.find_end_of_continued_line(lines, 1), 2)
163 163
164 164 nt.assert_equal(ipt2.assemble_continued_line(lines, (1, 5), 2), "foo bar")
165 165
166 166 def test_find_assign_magic():
167 167 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
168 168 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
169 169 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
170 170
171 171 def test_transform_assign_magic():
172 172 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
173 173
174 174 def test_find_assign_system():
175 175 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
176 176 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
177 177 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
178 178 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
179 179 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
180 180
181 181 def test_transform_assign_system():
182 182 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
183 183 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
184 184
185 185 def test_find_magic_escape():
186 186 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
187 187 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
188 188 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
189 189
190 190 def test_transform_magic_escape():
191 191 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
192 192 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
193 193
194 194 def test_find_autocalls():
195 195 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
196 196 print("Testing %r" % case[0])
197 197 check_find(ipt2.EscapedCommand, case)
198 198
199 199 def test_transform_autocall():
200 200 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
201 201 print("Testing %r" % case[0])
202 202 check_transform(ipt2.EscapedCommand, case)
203 203
204 204 def test_find_help():
205 205 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
206 206 check_find(ipt2.HelpEnd, case)
207 207
208 208 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
209 209 nt.assert_equal(tf.q_line, 1)
210 210 nt.assert_equal(tf.q_col, 3)
211 211
212 212 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
213 213 nt.assert_equal(tf.q_line, 1)
214 214 nt.assert_equal(tf.q_col, 8)
215 215
216 216 # ? in a comment does not trigger help
217 217 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
218 218 # Nor in a string
219 219 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
220 220
221 221 def test_transform_help():
222 222 tf = ipt2.HelpEnd((1, 0), (1, 9))
223 223 nt.assert_equal(tf.transform(HELP_IN_EXPR[0]), HELP_IN_EXPR[2])
224 224
225 225 tf = ipt2.HelpEnd((1, 0), (2, 3))
226 226 nt.assert_equal(tf.transform(HELP_CONTINUED_LINE[0]), HELP_CONTINUED_LINE[2])
227 227
228 228 tf = ipt2.HelpEnd((1, 0), (2, 8))
229 229 nt.assert_equal(tf.transform(HELP_MULTILINE[0]), HELP_MULTILINE[2])
230 230
231 231 tf = ipt2.HelpEnd((1, 0), (1, 0))
232 232 nt.assert_equal(tf.transform(HELP_UNICODE[0]), HELP_UNICODE[2])
233 233
234 234 def test_find_assign_op_dedent():
235 235 """
236 236 be careful that empty token like dedent are not counted as parens
237 237 """
238 238 class Tk:
239 239 def __init__(self, s):
240 240 self.string = s
241 241
242 242 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','a','=','b')]), 2)
243 243 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','(', 'a','=','b', ')', '=' ,'5')]), 6)
244 244
245 245 def test_check_complete():
246 246 cc = ipt2.TransformerManager().check_complete
247 247 nt.assert_equal(cc("a = 1"), ('complete', None))
248 248 nt.assert_equal(cc("for a in range(5):"), ('incomplete', 4))
249 249 nt.assert_equal(cc("for a in range(5):\n if a > 0:"), ('incomplete', 8))
250 250 nt.assert_equal(cc("raise = 2"), ('invalid', None))
251 251 nt.assert_equal(cc("a = [1,\n2,"), ('incomplete', 0))
252 252 nt.assert_equal(cc(")"), ('incomplete', 0))
253 253 nt.assert_equal(cc("\\\r\n"), ('incomplete', 0))
254 254 nt.assert_equal(cc("a = '''\n hi"), ('incomplete', 3))
255 255 nt.assert_equal(cc("def a():\n x=1\n global x"), ('invalid', None))
256 256 nt.assert_equal(cc("a \\ "), ('invalid', None)) # Nothing allowed after backslash
257 257 nt.assert_equal(cc("1\\\n+2"), ('complete', None))
258 258 nt.assert_equal(cc("exit"), ('complete', None))
259 259
260 260 example = dedent("""
261 261 if True:
262 262 a=1""" )
263 263
264 264 nt.assert_equal(cc(example), ('incomplete', 4))
265 265 nt.assert_equal(cc(example+'\n'), ('complete', None))
266 266 nt.assert_equal(cc(example+'\n '), ('complete', None))
267 267
268 268 # no need to loop on all the letters/numbers.
269 269 short = '12abAB'+string.printable[62:]
270 270 for c in short:
271 271 # test does not raise:
272 272 cc(c)
273 273 for k in short:
274 274 cc(c+k)
275 275
276 276 nt.assert_equal(cc("def f():\n x=0\n \\\n "), ('incomplete', 2))
277 277
278 278 def test_check_complete_II():
279 279 """
280 280 Test that multiple line strings are properly handled.
281 281
282 282 Separate test function for convenience
283 283
284 284 """
285 285 cc = ipt2.TransformerManager().check_complete
286 286 nt.assert_equal(cc('''def foo():\n """'''), ('incomplete', 4))
287 287
288 288
289 289 def test_null_cleanup_transformer():
290 290 manager = ipt2.TransformerManager()
291 291 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
292 nt.assert_is(manager.transform_cell(""), "")
292 assert manager.transform_cell("") == ""
293
294
295
296
297 def test_side_effects_I():
298 count = 0
299 def counter(lines):
300 nonlocal count
301 count += 1
302 return lines
303
304 counter.has_side_effects = True
305
306 manager = ipt2.TransformerManager()
307 manager.cleanup_transforms.insert(0, counter)
308 assert manager.check_complete("a=1\n") == ('complete', None)
309 assert count == 0
310
311
312
313
314 def test_side_effects_II():
315 count = 0
316 def counter(lines):
317 nonlocal count
318 count += 1
319 return lines
320
321 counter.has_side_effects = True
322
323 manager = ipt2.TransformerManager()
324 manager.line_transforms.insert(0, counter)
325 assert manager.check_complete("b=1\n") == ('complete', None)
326 assert count == 0
@@ -1,1018 +1,1042 b''
1 1 # -*- coding: utf-8 -*-
2 2 """Tests for the key interactiveshell module.
3 3
4 4 Historically the main classes in interactiveshell have been under-tested. This
5 5 module should grow as many single-method tests as possible to trap many of the
6 6 recurring bugs we seem to encounter with high-level interaction.
7 7 """
8 8
9 9 # Copyright (c) IPython Development Team.
10 10 # Distributed under the terms of the Modified BSD License.
11 11
12 12 import asyncio
13 13 import ast
14 14 import os
15 15 import signal
16 16 import shutil
17 17 import sys
18 18 import tempfile
19 19 import unittest
20 20 from unittest import mock
21 21
22 22 from os.path import join
23 23
24 24 import nose.tools as nt
25 25
26 26 from IPython.core.error import InputRejected
27 27 from IPython.core.inputtransformer import InputTransformer
28 28 from IPython.core import interactiveshell
29 29 from IPython.testing.decorators import (
30 30 skipif, skip_win32, onlyif_unicode_paths, onlyif_cmds_exist,
31 31 )
32 32 from IPython.testing import tools as tt
33 33 from IPython.utils.process import find_cmd
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Globals
37 37 #-----------------------------------------------------------------------------
38 38 # This is used by every single test, no point repeating it ad nauseam
39 39
40 40 #-----------------------------------------------------------------------------
41 41 # Tests
42 42 #-----------------------------------------------------------------------------
43 43
44 44 class DerivedInterrupt(KeyboardInterrupt):
45 45 pass
46 46
47 47 class InteractiveShellTestCase(unittest.TestCase):
48 48 def test_naked_string_cells(self):
49 49 """Test that cells with only naked strings are fully executed"""
50 50 # First, single-line inputs
51 51 ip.run_cell('"a"\n')
52 52 self.assertEqual(ip.user_ns['_'], 'a')
53 53 # And also multi-line cells
54 54 ip.run_cell('"""a\nb"""\n')
55 55 self.assertEqual(ip.user_ns['_'], 'a\nb')
56 56
57 57 def test_run_empty_cell(self):
58 58 """Just make sure we don't get a horrible error with a blank
59 59 cell of input. Yes, I did overlook that."""
60 60 old_xc = ip.execution_count
61 61 res = ip.run_cell('')
62 62 self.assertEqual(ip.execution_count, old_xc)
63 63 self.assertEqual(res.execution_count, None)
64 64
65 65 def test_run_cell_multiline(self):
66 66 """Multi-block, multi-line cells must execute correctly.
67 67 """
68 68 src = '\n'.join(["x=1",
69 69 "y=2",
70 70 "if 1:",
71 71 " x += 1",
72 72 " y += 1",])
73 73 res = ip.run_cell(src)
74 74 self.assertEqual(ip.user_ns['x'], 2)
75 75 self.assertEqual(ip.user_ns['y'], 3)
76 76 self.assertEqual(res.success, True)
77 77 self.assertEqual(res.result, None)
78 78
79 79 def test_multiline_string_cells(self):
80 80 "Code sprinkled with multiline strings should execute (GH-306)"
81 81 ip.run_cell('tmp=0')
82 82 self.assertEqual(ip.user_ns['tmp'], 0)
83 83 res = ip.run_cell('tmp=1;"""a\nb"""\n')
84 84 self.assertEqual(ip.user_ns['tmp'], 1)
85 85 self.assertEqual(res.success, True)
86 86 self.assertEqual(res.result, "a\nb")
87 87
88 88 def test_dont_cache_with_semicolon(self):
89 89 "Ending a line with semicolon should not cache the returned object (GH-307)"
90 90 oldlen = len(ip.user_ns['Out'])
91 91 for cell in ['1;', '1;1;']:
92 92 res = ip.run_cell(cell, store_history=True)
93 93 newlen = len(ip.user_ns['Out'])
94 94 self.assertEqual(oldlen, newlen)
95 95 self.assertIsNone(res.result)
96 96 i = 0
97 97 #also test the default caching behavior
98 98 for cell in ['1', '1;1']:
99 99 ip.run_cell(cell, store_history=True)
100 100 newlen = len(ip.user_ns['Out'])
101 101 i += 1
102 102 self.assertEqual(oldlen+i, newlen)
103 103
104 104 def test_syntax_error(self):
105 105 res = ip.run_cell("raise = 3")
106 106 self.assertIsInstance(res.error_before_exec, SyntaxError)
107 107
108 108 def test_In_variable(self):
109 109 "Verify that In variable grows with user input (GH-284)"
110 110 oldlen = len(ip.user_ns['In'])
111 111 ip.run_cell('1;', store_history=True)
112 112 newlen = len(ip.user_ns['In'])
113 113 self.assertEqual(oldlen+1, newlen)
114 114 self.assertEqual(ip.user_ns['In'][-1],'1;')
115 115
116 116 def test_magic_names_in_string(self):
117 117 ip.run_cell('a = """\n%exit\n"""')
118 118 self.assertEqual(ip.user_ns['a'], '\n%exit\n')
119 119
120 120 def test_trailing_newline(self):
121 121 """test that running !(command) does not raise a SyntaxError"""
122 122 ip.run_cell('!(true)\n', False)
123 123 ip.run_cell('!(true)\n\n\n', False)
124 124
125 125 def test_gh_597(self):
126 126 """Pretty-printing lists of objects with non-ascii reprs may cause
127 127 problems."""
128 128 class Spam(object):
129 129 def __repr__(self):
130 130 return "\xe9"*50
131 131 import IPython.core.formatters
132 132 f = IPython.core.formatters.PlainTextFormatter()
133 133 f([Spam(),Spam()])
134 134
135 135
136 136 def test_future_flags(self):
137 137 """Check that future flags are used for parsing code (gh-777)"""
138 138 ip.run_cell('from __future__ import barry_as_FLUFL')
139 139 try:
140 140 ip.run_cell('prfunc_return_val = 1 <> 2')
141 141 assert 'prfunc_return_val' in ip.user_ns
142 142 finally:
143 143 # Reset compiler flags so we don't mess up other tests.
144 144 ip.compile.reset_compiler_flags()
145 145
146 146 def test_can_pickle(self):
147 147 "Can we pickle objects defined interactively (GH-29)"
148 148 ip = get_ipython()
149 149 ip.reset()
150 150 ip.run_cell(("class Mylist(list):\n"
151 151 " def __init__(self,x=[]):\n"
152 152 " list.__init__(self,x)"))
153 153 ip.run_cell("w=Mylist([1,2,3])")
154 154
155 155 from pickle import dumps
156 156
157 157 # We need to swap in our main module - this is only necessary
158 158 # inside the test framework, because IPython puts the interactive module
159 159 # in place (but the test framework undoes this).
160 160 _main = sys.modules['__main__']
161 161 sys.modules['__main__'] = ip.user_module
162 162 try:
163 163 res = dumps(ip.user_ns["w"])
164 164 finally:
165 165 sys.modules['__main__'] = _main
166 166 self.assertTrue(isinstance(res, bytes))
167 167
168 168 def test_global_ns(self):
169 169 "Code in functions must be able to access variables outside them."
170 170 ip = get_ipython()
171 171 ip.run_cell("a = 10")
172 172 ip.run_cell(("def f(x):\n"
173 173 " return x + a"))
174 174 ip.run_cell("b = f(12)")
175 175 self.assertEqual(ip.user_ns["b"], 22)
176 176
177 177 def test_bad_custom_tb(self):
178 178 """Check that InteractiveShell is protected from bad custom exception handlers"""
179 179 ip.set_custom_exc((IOError,), lambda etype,value,tb: 1/0)
180 180 self.assertEqual(ip.custom_exceptions, (IOError,))
181 181 with tt.AssertPrints("Custom TB Handler failed", channel='stderr'):
182 182 ip.run_cell(u'raise IOError("foo")')
183 183 self.assertEqual(ip.custom_exceptions, ())
184 184
185 185 def test_bad_custom_tb_return(self):
186 186 """Check that InteractiveShell is protected from bad return types in custom exception handlers"""
187 187 ip.set_custom_exc((NameError,),lambda etype,value,tb, tb_offset=None: 1)
188 188 self.assertEqual(ip.custom_exceptions, (NameError,))
189 189 with tt.AssertPrints("Custom TB Handler failed", channel='stderr'):
190 190 ip.run_cell(u'a=abracadabra')
191 191 self.assertEqual(ip.custom_exceptions, ())
192 192
193 193 def test_drop_by_id(self):
194 194 myvars = {"a":object(), "b":object(), "c": object()}
195 195 ip.push(myvars, interactive=False)
196 196 for name in myvars:
197 197 assert name in ip.user_ns, name
198 198 assert name in ip.user_ns_hidden, name
199 199 ip.user_ns['b'] = 12
200 200 ip.drop_by_id(myvars)
201 201 for name in ["a", "c"]:
202 202 assert name not in ip.user_ns, name
203 203 assert name not in ip.user_ns_hidden, name
204 204 assert ip.user_ns['b'] == 12
205 205 ip.reset()
206 206
207 207 def test_var_expand(self):
208 208 ip.user_ns['f'] = u'Ca\xf1o'
209 209 self.assertEqual(ip.var_expand(u'echo $f'), u'echo Ca\xf1o')
210 210 self.assertEqual(ip.var_expand(u'echo {f}'), u'echo Ca\xf1o')
211 211 self.assertEqual(ip.var_expand(u'echo {f[:-1]}'), u'echo Ca\xf1')
212 212 self.assertEqual(ip.var_expand(u'echo {1*2}'), u'echo 2')
213 213
214 214 self.assertEqual(ip.var_expand(u"grep x | awk '{print $1}'"), u"grep x | awk '{print $1}'")
215 215
216 216 ip.user_ns['f'] = b'Ca\xc3\xb1o'
217 217 # This should not raise any exception:
218 218 ip.var_expand(u'echo $f')
219 219
220 220 def test_var_expand_local(self):
221 221 """Test local variable expansion in !system and %magic calls"""
222 222 # !system
223 223 ip.run_cell('def test():\n'
224 224 ' lvar = "ttt"\n'
225 225 ' ret = !echo {lvar}\n'
226 226 ' return ret[0]\n')
227 227 res = ip.user_ns['test']()
228 228 nt.assert_in('ttt', res)
229 229
230 230 # %magic
231 231 ip.run_cell('def makemacro():\n'
232 232 ' macroname = "macro_var_expand_locals"\n'
233 233 ' %macro {macroname} codestr\n')
234 234 ip.user_ns['codestr'] = "str(12)"
235 235 ip.run_cell('makemacro()')
236 236 nt.assert_in('macro_var_expand_locals', ip.user_ns)
237 237
238 238 def test_var_expand_self(self):
239 239 """Test variable expansion with the name 'self', which was failing.
240 240
241 241 See https://github.com/ipython/ipython/issues/1878#issuecomment-7698218
242 242 """
243 243 ip.run_cell('class cTest:\n'
244 244 ' classvar="see me"\n'
245 245 ' def test(self):\n'
246 246 ' res = !echo Variable: {self.classvar}\n'
247 247 ' return res[0]\n')
248 248 nt.assert_in('see me', ip.user_ns['cTest']().test())
249 249
250 250 def test_bad_var_expand(self):
251 251 """var_expand on invalid formats shouldn't raise"""
252 252 # SyntaxError
253 253 self.assertEqual(ip.var_expand(u"{'a':5}"), u"{'a':5}")
254 254 # NameError
255 255 self.assertEqual(ip.var_expand(u"{asdf}"), u"{asdf}")
256 256 # ZeroDivisionError
257 257 self.assertEqual(ip.var_expand(u"{1/0}"), u"{1/0}")
258 258
259 259 def test_silent_postexec(self):
260 260 """run_cell(silent=True) doesn't invoke pre/post_run_cell callbacks"""
261 261 pre_explicit = mock.Mock()
262 262 pre_always = mock.Mock()
263 263 post_explicit = mock.Mock()
264 264 post_always = mock.Mock()
265 265 all_mocks = [pre_explicit, pre_always, post_explicit, post_always]
266 266
267 267 ip.events.register('pre_run_cell', pre_explicit)
268 268 ip.events.register('pre_execute', pre_always)
269 269 ip.events.register('post_run_cell', post_explicit)
270 270 ip.events.register('post_execute', post_always)
271 271
272 272 try:
273 273 ip.run_cell("1", silent=True)
274 274 assert pre_always.called
275 275 assert not pre_explicit.called
276 276 assert post_always.called
277 277 assert not post_explicit.called
278 278 # double-check that non-silent exec did what we expected
279 279 # silent to avoid
280 280 ip.run_cell("1")
281 281 assert pre_explicit.called
282 282 assert post_explicit.called
283 283 info, = pre_explicit.call_args[0]
284 284 result, = post_explicit.call_args[0]
285 285 self.assertEqual(info, result.info)
286 286 # check that post hooks are always called
287 287 [m.reset_mock() for m in all_mocks]
288 288 ip.run_cell("syntax error")
289 289 assert pre_always.called
290 290 assert pre_explicit.called
291 291 assert post_always.called
292 292 assert post_explicit.called
293 293 info, = pre_explicit.call_args[0]
294 294 result, = post_explicit.call_args[0]
295 295 self.assertEqual(info, result.info)
296 296 finally:
297 297 # remove post-exec
298 298 ip.events.unregister('pre_run_cell', pre_explicit)
299 299 ip.events.unregister('pre_execute', pre_always)
300 300 ip.events.unregister('post_run_cell', post_explicit)
301 301 ip.events.unregister('post_execute', post_always)
302 302
303 303 def test_silent_noadvance(self):
304 304 """run_cell(silent=True) doesn't advance execution_count"""
305 305 ec = ip.execution_count
306 306 # silent should force store_history=False
307 307 ip.run_cell("1", store_history=True, silent=True)
308 308
309 309 self.assertEqual(ec, ip.execution_count)
310 310 # double-check that non-silent exec did what we expected
311 311 # silent to avoid
312 312 ip.run_cell("1", store_history=True)
313 313 self.assertEqual(ec+1, ip.execution_count)
314 314
315 315 def test_silent_nodisplayhook(self):
316 316 """run_cell(silent=True) doesn't trigger displayhook"""
317 317 d = dict(called=False)
318 318
319 319 trap = ip.display_trap
320 320 save_hook = trap.hook
321 321
322 322 def failing_hook(*args, **kwargs):
323 323 d['called'] = True
324 324
325 325 try:
326 326 trap.hook = failing_hook
327 327 res = ip.run_cell("1", silent=True)
328 328 self.assertFalse(d['called'])
329 329 self.assertIsNone(res.result)
330 330 # double-check that non-silent exec did what we expected
331 331 # silent to avoid
332 332 ip.run_cell("1")
333 333 self.assertTrue(d['called'])
334 334 finally:
335 335 trap.hook = save_hook
336 336
337 337 def test_ofind_line_magic(self):
338 338 from IPython.core.magic import register_line_magic
339 339
340 340 @register_line_magic
341 341 def lmagic(line):
342 342 "A line magic"
343 343
344 344 # Get info on line magic
345 345 lfind = ip._ofind('lmagic')
346 346 info = dict(found=True, isalias=False, ismagic=True,
347 347 namespace = 'IPython internal', obj= lmagic.__wrapped__,
348 348 parent = None)
349 349 nt.assert_equal(lfind, info)
350 350
351 351 def test_ofind_cell_magic(self):
352 352 from IPython.core.magic import register_cell_magic
353 353
354 354 @register_cell_magic
355 355 def cmagic(line, cell):
356 356 "A cell magic"
357 357
358 358 # Get info on cell magic
359 359 find = ip._ofind('cmagic')
360 360 info = dict(found=True, isalias=False, ismagic=True,
361 361 namespace = 'IPython internal', obj= cmagic.__wrapped__,
362 362 parent = None)
363 363 nt.assert_equal(find, info)
364 364
365 365 def test_ofind_property_with_error(self):
366 366 class A(object):
367 367 @property
368 368 def foo(self):
369 369 raise NotImplementedError()
370 370 a = A()
371 371
372 372 found = ip._ofind('a.foo', [('locals', locals())])
373 373 info = dict(found=True, isalias=False, ismagic=False,
374 374 namespace='locals', obj=A.foo, parent=a)
375 375 nt.assert_equal(found, info)
376 376
377 377 def test_ofind_multiple_attribute_lookups(self):
378 378 class A(object):
379 379 @property
380 380 def foo(self):
381 381 raise NotImplementedError()
382 382
383 383 a = A()
384 384 a.a = A()
385 385 a.a.a = A()
386 386
387 387 found = ip._ofind('a.a.a.foo', [('locals', locals())])
388 388 info = dict(found=True, isalias=False, ismagic=False,
389 389 namespace='locals', obj=A.foo, parent=a.a.a)
390 390 nt.assert_equal(found, info)
391 391
392 392 def test_ofind_slotted_attributes(self):
393 393 class A(object):
394 394 __slots__ = ['foo']
395 395 def __init__(self):
396 396 self.foo = 'bar'
397 397
398 398 a = A()
399 399 found = ip._ofind('a.foo', [('locals', locals())])
400 400 info = dict(found=True, isalias=False, ismagic=False,
401 401 namespace='locals', obj=a.foo, parent=a)
402 402 nt.assert_equal(found, info)
403 403
404 404 found = ip._ofind('a.bar', [('locals', locals())])
405 405 info = dict(found=False, isalias=False, ismagic=False,
406 406 namespace=None, obj=None, parent=a)
407 407 nt.assert_equal(found, info)
408 408
409 409 def test_ofind_prefers_property_to_instance_level_attribute(self):
410 410 class A(object):
411 411 @property
412 412 def foo(self):
413 413 return 'bar'
414 414 a = A()
415 415 a.__dict__['foo'] = 'baz'
416 416 nt.assert_equal(a.foo, 'bar')
417 417 found = ip._ofind('a.foo', [('locals', locals())])
418 418 nt.assert_is(found['obj'], A.foo)
419 419
420 420 def test_custom_syntaxerror_exception(self):
421 421 called = []
422 422 def my_handler(shell, etype, value, tb, tb_offset=None):
423 423 called.append(etype)
424 424 shell.showtraceback((etype, value, tb), tb_offset=tb_offset)
425 425
426 426 ip.set_custom_exc((SyntaxError,), my_handler)
427 427 try:
428 428 ip.run_cell("1f")
429 429 # Check that this was called, and only once.
430 430 self.assertEqual(called, [SyntaxError])
431 431 finally:
432 432 # Reset the custom exception hook
433 433 ip.set_custom_exc((), None)
434 434
435 435 def test_custom_exception(self):
436 436 called = []
437 437 def my_handler(shell, etype, value, tb, tb_offset=None):
438 438 called.append(etype)
439 439 shell.showtraceback((etype, value, tb), tb_offset=tb_offset)
440 440
441 441 ip.set_custom_exc((ValueError,), my_handler)
442 442 try:
443 443 res = ip.run_cell("raise ValueError('test')")
444 444 # Check that this was called, and only once.
445 445 self.assertEqual(called, [ValueError])
446 446 # Check that the error is on the result object
447 447 self.assertIsInstance(res.error_in_exec, ValueError)
448 448 finally:
449 449 # Reset the custom exception hook
450 450 ip.set_custom_exc((), None)
451 451
452 452 def test_mktempfile(self):
453 453 filename = ip.mktempfile()
454 454 # Check that we can open the file again on Windows
455 455 with open(filename, 'w') as f:
456 456 f.write('abc')
457 457
458 458 filename = ip.mktempfile(data='blah')
459 459 with open(filename, 'r') as f:
460 460 self.assertEqual(f.read(), 'blah')
461 461
462 462 def test_new_main_mod(self):
463 463 # Smoketest to check that this accepts a unicode module name
464 464 name = u'jiefmw'
465 465 mod = ip.new_main_mod(u'%s.py' % name, name)
466 466 self.assertEqual(mod.__name__, name)
467 467
468 468 def test_get_exception_only(self):
469 469 try:
470 470 raise KeyboardInterrupt
471 471 except KeyboardInterrupt:
472 472 msg = ip.get_exception_only()
473 473 self.assertEqual(msg, 'KeyboardInterrupt\n')
474 474
475 475 try:
476 476 raise DerivedInterrupt("foo")
477 477 except KeyboardInterrupt:
478 478 msg = ip.get_exception_only()
479 479 self.assertEqual(msg, 'IPython.core.tests.test_interactiveshell.DerivedInterrupt: foo\n')
480 480
481 481 def test_inspect_text(self):
482 482 ip.run_cell('a = 5')
483 483 text = ip.object_inspect_text('a')
484 484 self.assertIsInstance(text, str)
485 485
486 486 def test_last_execution_result(self):
487 487 """ Check that last execution result gets set correctly (GH-10702) """
488 488 result = ip.run_cell('a = 5; a')
489 489 self.assertTrue(ip.last_execution_succeeded)
490 490 self.assertEqual(ip.last_execution_result.result, 5)
491 491
492 492 result = ip.run_cell('a = x_invalid_id_x')
493 493 self.assertFalse(ip.last_execution_succeeded)
494 494 self.assertFalse(ip.last_execution_result.success)
495 495 self.assertIsInstance(ip.last_execution_result.error_in_exec, NameError)
496 496
497 497 def test_reset_aliasing(self):
498 498 """ Check that standard posix aliases work after %reset. """
499 499 if os.name != 'posix':
500 500 return
501 501
502 502 ip.reset()
503 503 for cmd in ('clear', 'more', 'less', 'man'):
504 504 res = ip.run_cell('%' + cmd)
505 505 self.assertEqual(res.success, True)
506 506
507 507
508 508 class TestSafeExecfileNonAsciiPath(unittest.TestCase):
509 509
510 510 @onlyif_unicode_paths
511 511 def setUp(self):
512 512 self.BASETESTDIR = tempfile.mkdtemp()
513 513 self.TESTDIR = join(self.BASETESTDIR, u"åäö")
514 514 os.mkdir(self.TESTDIR)
515 515 with open(join(self.TESTDIR, u"åäötestscript.py"), "w") as sfile:
516 516 sfile.write("pass\n")
517 517 self.oldpath = os.getcwd()
518 518 os.chdir(self.TESTDIR)
519 519 self.fname = u"åäötestscript.py"
520 520
521 521 def tearDown(self):
522 522 os.chdir(self.oldpath)
523 523 shutil.rmtree(self.BASETESTDIR)
524 524
525 525 @onlyif_unicode_paths
526 526 def test_1(self):
527 527 """Test safe_execfile with non-ascii path
528 528 """
529 529 ip.safe_execfile(self.fname, {}, raise_exceptions=True)
530 530
531 531 class ExitCodeChecks(tt.TempFileMixin):
532 532
533 533 def setUp(self):
534 534 self.system = ip.system_raw
535 535
536 536 def test_exit_code_ok(self):
537 537 self.system('exit 0')
538 538 self.assertEqual(ip.user_ns['_exit_code'], 0)
539 539
540 540 def test_exit_code_error(self):
541 541 self.system('exit 1')
542 542 self.assertEqual(ip.user_ns['_exit_code'], 1)
543 543
544 544 @skipif(not hasattr(signal, 'SIGALRM'))
545 545 def test_exit_code_signal(self):
546 546 self.mktmp("import signal, time\n"
547 547 "signal.setitimer(signal.ITIMER_REAL, 0.1)\n"
548 548 "time.sleep(1)\n")
549 549 self.system("%s %s" % (sys.executable, self.fname))
550 550 self.assertEqual(ip.user_ns['_exit_code'], -signal.SIGALRM)
551 551
552 552 @onlyif_cmds_exist("csh")
553 553 def test_exit_code_signal_csh(self):
554 554 SHELL = os.environ.get('SHELL', None)
555 555 os.environ['SHELL'] = find_cmd("csh")
556 556 try:
557 557 self.test_exit_code_signal()
558 558 finally:
559 559 if SHELL is not None:
560 560 os.environ['SHELL'] = SHELL
561 561 else:
562 562 del os.environ['SHELL']
563 563
564 564
565 565 class TestSystemRaw(ExitCodeChecks):
566 566
567 567 def setUp(self):
568 568 super().setUp()
569 569 self.system = ip.system_raw
570 570
571 571 @onlyif_unicode_paths
572 572 def test_1(self):
573 573 """Test system_raw with non-ascii cmd
574 574 """
575 575 cmd = u'''python -c "'åäö'" '''
576 576 ip.system_raw(cmd)
577 577
578 578 @mock.patch('subprocess.call', side_effect=KeyboardInterrupt)
579 579 @mock.patch('os.system', side_effect=KeyboardInterrupt)
580 580 def test_control_c(self, *mocks):
581 581 try:
582 582 self.system("sleep 1 # wont happen")
583 583 except KeyboardInterrupt:
584 584 self.fail("system call should intercept "
585 585 "keyboard interrupt from subprocess.call")
586 586 self.assertEqual(ip.user_ns['_exit_code'], -signal.SIGINT)
587 587
588 588 # TODO: Exit codes are currently ignored on Windows.
589 589 class TestSystemPipedExitCode(ExitCodeChecks):
590 590
591 591 def setUp(self):
592 592 super().setUp()
593 593 self.system = ip.system_piped
594 594
595 595 @skip_win32
596 596 def test_exit_code_ok(self):
597 597 ExitCodeChecks.test_exit_code_ok(self)
598 598
599 599 @skip_win32
600 600 def test_exit_code_error(self):
601 601 ExitCodeChecks.test_exit_code_error(self)
602 602
603 603 @skip_win32
604 604 def test_exit_code_signal(self):
605 605 ExitCodeChecks.test_exit_code_signal(self)
606 606
607 607 class TestModules(tt.TempFileMixin):
608 608 def test_extraneous_loads(self):
609 609 """Test we're not loading modules on startup that we shouldn't.
610 610 """
611 611 self.mktmp("import sys\n"
612 612 "print('numpy' in sys.modules)\n"
613 613 "print('ipyparallel' in sys.modules)\n"
614 614 "print('ipykernel' in sys.modules)\n"
615 615 )
616 616 out = "False\nFalse\nFalse\n"
617 617 tt.ipexec_validate(self.fname, out)
618 618
619 619 class Negator(ast.NodeTransformer):
620 620 """Negates all number literals in an AST."""
621 621
622 622 # for python 3.7 and earlier
623 623 def visit_Num(self, node):
624 624 node.n = -node.n
625 625 return node
626 626
627 627 # for python 3.8+
628 628 def visit_Constant(self, node):
629 629 if isinstance(node.value, int):
630 630 return self.visit_Num(node)
631 631 return node
632 632
633 633 class TestAstTransform(unittest.TestCase):
634 634 def setUp(self):
635 635 self.negator = Negator()
636 636 ip.ast_transformers.append(self.negator)
637 637
638 638 def tearDown(self):
639 639 ip.ast_transformers.remove(self.negator)
640 640
641 641 def test_run_cell(self):
642 642 with tt.AssertPrints('-34'):
643 643 ip.run_cell('print (12 + 22)')
644 644
645 645 # A named reference to a number shouldn't be transformed.
646 646 ip.user_ns['n'] = 55
647 647 with tt.AssertNotPrints('-55'):
648 648 ip.run_cell('print (n)')
649 649
650 650 def test_timeit(self):
651 651 called = set()
652 652 def f(x):
653 653 called.add(x)
654 654 ip.push({'f':f})
655 655
656 656 with tt.AssertPrints("std. dev. of"):
657 657 ip.run_line_magic("timeit", "-n1 f(1)")
658 658 self.assertEqual(called, {-1})
659 659 called.clear()
660 660
661 661 with tt.AssertPrints("std. dev. of"):
662 662 ip.run_cell_magic("timeit", "-n1 f(2)", "f(3)")
663 663 self.assertEqual(called, {-2, -3})
664 664
665 665 def test_time(self):
666 666 called = []
667 667 def f(x):
668 668 called.append(x)
669 669 ip.push({'f':f})
670 670
671 671 # Test with an expression
672 672 with tt.AssertPrints("Wall time: "):
673 673 ip.run_line_magic("time", "f(5+9)")
674 674 self.assertEqual(called, [-14])
675 675 called[:] = []
676 676
677 677 # Test with a statement (different code path)
678 678 with tt.AssertPrints("Wall time: "):
679 679 ip.run_line_magic("time", "a = f(-3 + -2)")
680 680 self.assertEqual(called, [5])
681 681
682 682 def test_macro(self):
683 683 ip.push({'a':10})
684 684 # The AST transformation makes this do a+=-1
685 685 ip.define_macro("amacro", "a+=1\nprint(a)")
686 686
687 687 with tt.AssertPrints("9"):
688 688 ip.run_cell("amacro")
689 689 with tt.AssertPrints("8"):
690 690 ip.run_cell("amacro")
691 691
692 class TestMiscTransform(unittest.TestCase):
693
694
695 def test_transform_only_once(self):
696 cleanup = 0
697 line_t = 0
698 def count_cleanup(lines):
699 nonlocal cleanup
700 cleanup += 1
701 return lines
702
703 def count_line_t(lines):
704 nonlocal line_t
705 line_t += 1
706 return lines
707
708 ip.input_transformer_manager.cleanup_transforms.append(count_cleanup)
709 ip.input_transformer_manager.line_transforms.append(count_line_t)
710
711 ip.run_cell('1')
712
713 assert cleanup == 1
714 assert line_t == 1
715
692 716 class IntegerWrapper(ast.NodeTransformer):
693 717 """Wraps all integers in a call to Integer()"""
694 718
695 719 # for Python 3.7 and earlier
696 720
697 721 # for Python 3.7 and earlier
698 722 def visit_Num(self, node):
699 723 if isinstance(node.n, int):
700 724 return ast.Call(func=ast.Name(id='Integer', ctx=ast.Load()),
701 725 args=[node], keywords=[])
702 726 return node
703 727
704 728 # For Python 3.8+
705 729 def visit_Constant(self, node):
706 730 if isinstance(node.value, int):
707 731 return self.visit_Num(node)
708 732 return node
709 733
710 734
711 735 class TestAstTransform2(unittest.TestCase):
712 736 def setUp(self):
713 737 self.intwrapper = IntegerWrapper()
714 738 ip.ast_transformers.append(self.intwrapper)
715 739
716 740 self.calls = []
717 741 def Integer(*args):
718 742 self.calls.append(args)
719 743 return args
720 744 ip.push({"Integer": Integer})
721 745
722 746 def tearDown(self):
723 747 ip.ast_transformers.remove(self.intwrapper)
724 748 del ip.user_ns['Integer']
725 749
726 750 def test_run_cell(self):
727 751 ip.run_cell("n = 2")
728 752 self.assertEqual(self.calls, [(2,)])
729 753
730 754 # This shouldn't throw an error
731 755 ip.run_cell("o = 2.0")
732 756 self.assertEqual(ip.user_ns['o'], 2.0)
733 757
734 758 def test_timeit(self):
735 759 called = set()
736 760 def f(x):
737 761 called.add(x)
738 762 ip.push({'f':f})
739 763
740 764 with tt.AssertPrints("std. dev. of"):
741 765 ip.run_line_magic("timeit", "-n1 f(1)")
742 766 self.assertEqual(called, {(1,)})
743 767 called.clear()
744 768
745 769 with tt.AssertPrints("std. dev. of"):
746 770 ip.run_cell_magic("timeit", "-n1 f(2)", "f(3)")
747 771 self.assertEqual(called, {(2,), (3,)})
748 772
749 773 class ErrorTransformer(ast.NodeTransformer):
750 774 """Throws an error when it sees a number."""
751 775
752 776 # for Python 3.7 and earlier
753 777 def visit_Num(self, node):
754 778 raise ValueError("test")
755 779
756 780 # for Python 3.8+
757 781 def visit_Constant(self, node):
758 782 if isinstance(node.value, int):
759 783 return self.visit_Num(node)
760 784 return node
761 785
762 786
763 787 class TestAstTransformError(unittest.TestCase):
764 788 def test_unregistering(self):
765 789 err_transformer = ErrorTransformer()
766 790 ip.ast_transformers.append(err_transformer)
767 791
768 792 with self.assertWarnsRegex(UserWarning, "It will be unregistered"):
769 793 ip.run_cell("1 + 2")
770 794
771 795 # This should have been removed.
772 796 nt.assert_not_in(err_transformer, ip.ast_transformers)
773 797
774 798
775 799 class StringRejector(ast.NodeTransformer):
776 800 """Throws an InputRejected when it sees a string literal.
777 801
778 802 Used to verify that NodeTransformers can signal that a piece of code should
779 803 not be executed by throwing an InputRejected.
780 804 """
781 805
782 806 #for python 3.7 and earlier
783 807 def visit_Str(self, node):
784 808 raise InputRejected("test")
785 809
786 810 # 3.8 only
787 811 def visit_Constant(self, node):
788 812 if isinstance(node.value, str):
789 813 raise InputRejected("test")
790 814 return node
791 815
792 816
793 817 class TestAstTransformInputRejection(unittest.TestCase):
794 818
795 819 def setUp(self):
796 820 self.transformer = StringRejector()
797 821 ip.ast_transformers.append(self.transformer)
798 822
799 823 def tearDown(self):
800 824 ip.ast_transformers.remove(self.transformer)
801 825
802 826 def test_input_rejection(self):
803 827 """Check that NodeTransformers can reject input."""
804 828
805 829 expect_exception_tb = tt.AssertPrints("InputRejected: test")
806 830 expect_no_cell_output = tt.AssertNotPrints("'unsafe'", suppress=False)
807 831
808 832 # Run the same check twice to verify that the transformer is not
809 833 # disabled after raising.
810 834 with expect_exception_tb, expect_no_cell_output:
811 835 ip.run_cell("'unsafe'")
812 836
813 837 with expect_exception_tb, expect_no_cell_output:
814 838 res = ip.run_cell("'unsafe'")
815 839
816 840 self.assertIsInstance(res.error_before_exec, InputRejected)
817 841
818 842 def test__IPYTHON__():
819 843 # This shouldn't raise a NameError, that's all
820 844 __IPYTHON__
821 845
822 846
823 847 class DummyRepr(object):
824 848 def __repr__(self):
825 849 return "DummyRepr"
826 850
827 851 def _repr_html_(self):
828 852 return "<b>dummy</b>"
829 853
830 854 def _repr_javascript_(self):
831 855 return "console.log('hi');", {'key': 'value'}
832 856
833 857
834 858 def test_user_variables():
835 859 # enable all formatters
836 860 ip.display_formatter.active_types = ip.display_formatter.format_types
837 861
838 862 ip.user_ns['dummy'] = d = DummyRepr()
839 863 keys = {'dummy', 'doesnotexist'}
840 864 r = ip.user_expressions({ key:key for key in keys})
841 865
842 866 nt.assert_equal(keys, set(r.keys()))
843 867 dummy = r['dummy']
844 868 nt.assert_equal({'status', 'data', 'metadata'}, set(dummy.keys()))
845 869 nt.assert_equal(dummy['status'], 'ok')
846 870 data = dummy['data']
847 871 metadata = dummy['metadata']
848 872 nt.assert_equal(data.get('text/html'), d._repr_html_())
849 873 js, jsmd = d._repr_javascript_()
850 874 nt.assert_equal(data.get('application/javascript'), js)
851 875 nt.assert_equal(metadata.get('application/javascript'), jsmd)
852 876
853 877 dne = r['doesnotexist']
854 878 nt.assert_equal(dne['status'], 'error')
855 879 nt.assert_equal(dne['ename'], 'NameError')
856 880
857 881 # back to text only
858 882 ip.display_formatter.active_types = ['text/plain']
859 883
860 884 def test_user_expression():
861 885 # enable all formatters
862 886 ip.display_formatter.active_types = ip.display_formatter.format_types
863 887 query = {
864 888 'a' : '1 + 2',
865 889 'b' : '1/0',
866 890 }
867 891 r = ip.user_expressions(query)
868 892 import pprint
869 893 pprint.pprint(r)
870 894 nt.assert_equal(set(r.keys()), set(query.keys()))
871 895 a = r['a']
872 896 nt.assert_equal({'status', 'data', 'metadata'}, set(a.keys()))
873 897 nt.assert_equal(a['status'], 'ok')
874 898 data = a['data']
875 899 metadata = a['metadata']
876 900 nt.assert_equal(data.get('text/plain'), '3')
877 901
878 902 b = r['b']
879 903 nt.assert_equal(b['status'], 'error')
880 904 nt.assert_equal(b['ename'], 'ZeroDivisionError')
881 905
882 906 # back to text only
883 907 ip.display_formatter.active_types = ['text/plain']
884 908
885 909
886 910 class TestSyntaxErrorTransformer(unittest.TestCase):
887 911 """Check that SyntaxError raised by an input transformer is handled by run_cell()"""
888 912
889 913 @staticmethod
890 914 def transformer(lines):
891 915 for line in lines:
892 916 pos = line.find('syntaxerror')
893 917 if pos >= 0:
894 918 e = SyntaxError('input contains "syntaxerror"')
895 919 e.text = line
896 920 e.offset = pos + 1
897 921 raise e
898 922 return lines
899 923
900 924 def setUp(self):
901 925 ip.input_transformers_post.append(self.transformer)
902 926
903 927 def tearDown(self):
904 928 ip.input_transformers_post.remove(self.transformer)
905 929
906 930 def test_syntaxerror_input_transformer(self):
907 931 with tt.AssertPrints('1234'):
908 932 ip.run_cell('1234')
909 933 with tt.AssertPrints('SyntaxError: invalid syntax'):
910 934 ip.run_cell('1 2 3') # plain python syntax error
911 935 with tt.AssertPrints('SyntaxError: input contains "syntaxerror"'):
912 936 ip.run_cell('2345 # syntaxerror') # input transformer syntax error
913 937 with tt.AssertPrints('3456'):
914 938 ip.run_cell('3456')
915 939
916 940
917 941 class TestWarningSuppression(unittest.TestCase):
918 942 def test_warning_suppression(self):
919 943 ip.run_cell("import warnings")
920 944 try:
921 945 with self.assertWarnsRegex(UserWarning, "asdf"):
922 946 ip.run_cell("warnings.warn('asdf')")
923 947 # Here's the real test -- if we run that again, we should get the
924 948 # warning again. Traditionally, each warning was only issued once per
925 949 # IPython session (approximately), even if the user typed in new and
926 950 # different code that should have also triggered the warning, leading
927 951 # to much confusion.
928 952 with self.assertWarnsRegex(UserWarning, "asdf"):
929 953 ip.run_cell("warnings.warn('asdf')")
930 954 finally:
931 955 ip.run_cell("del warnings")
932 956
933 957
934 958 def test_deprecation_warning(self):
935 959 ip.run_cell("""
936 960 import warnings
937 961 def wrn():
938 962 warnings.warn(
939 963 "I AM A WARNING",
940 964 DeprecationWarning
941 965 )
942 966 """)
943 967 try:
944 968 with self.assertWarnsRegex(DeprecationWarning, "I AM A WARNING"):
945 969 ip.run_cell("wrn()")
946 970 finally:
947 971 ip.run_cell("del warnings")
948 972 ip.run_cell("del wrn")
949 973
950 974
951 975 class TestImportNoDeprecate(tt.TempFileMixin):
952 976
953 977 def setUp(self):
954 978 """Make a valid python temp file."""
955 979 self.mktmp("""
956 980 import warnings
957 981 def wrn():
958 982 warnings.warn(
959 983 "I AM A WARNING",
960 984 DeprecationWarning
961 985 )
962 986 """)
963 987 super().setUp()
964 988
965 989 def test_no_dep(self):
966 990 """
967 991 No deprecation warning should be raised from imported functions
968 992 """
969 993 ip.run_cell("from {} import wrn".format(self.fname))
970 994
971 995 with tt.AssertNotPrints("I AM A WARNING"):
972 996 ip.run_cell("wrn()")
973 997 ip.run_cell("del wrn")
974 998
975 999
976 1000 def test_custom_exc_count():
977 1001 hook = mock.Mock(return_value=None)
978 1002 ip.set_custom_exc((SyntaxError,), hook)
979 1003 before = ip.execution_count
980 1004 ip.run_cell("def foo()", store_history=True)
981 1005 # restore default excepthook
982 1006 ip.set_custom_exc((), None)
983 1007 nt.assert_equal(hook.call_count, 1)
984 1008 nt.assert_equal(ip.execution_count, before + 1)
985 1009
986 1010
987 1011 def test_run_cell_async():
988 1012 loop = asyncio.get_event_loop()
989 1013 ip.run_cell("import asyncio")
990 1014 coro = ip.run_cell_async("await asyncio.sleep(0.01)\n5")
991 1015 assert asyncio.iscoroutine(coro)
992 1016 result = loop.run_until_complete(coro)
993 1017 assert isinstance(result, interactiveshell.ExecutionResult)
994 1018 assert result.result == 5
995 1019
996 1020
997 1021 def test_should_run_async():
998 1022 assert not ip.should_run_async("a = 5")
999 1023 assert ip.should_run_async("await x")
1000 1024 assert ip.should_run_async("import asyncio; await asyncio.sleep(1)")
1001 1025
1002 1026
1003 1027 def test_set_custom_completer():
1004 1028 num_completers = len(ip.Completer.matchers)
1005 1029
1006 1030 def foo(*args, **kwargs):
1007 1031 return "I'm a completer!"
1008 1032
1009 1033 ip.set_custom_completer(foo, 0)
1010 1034
1011 1035 # check that we've really added a new completer
1012 1036 assert len(ip.Completer.matchers) == num_completers + 1
1013 1037
1014 1038 # check that the first completer is the function we defined
1015 1039 assert ip.Completer.matchers[0]() == "I'm a completer!"
1016 1040
1017 1041 # clean up
1018 1042 ip.Completer.custom_matchers.pop()
@@ -1,82 +1,90 b''
1 1
2 2 ===========================
3 3 Custom input transformation
4 4 ===========================
5 5
6 6 IPython extends Python syntax to allow things like magic commands, and help with
7 7 the ``?`` syntax. There are several ways to customise how the user's input is
8 8 processed into Python code to be executed.
9 9
10 10 These hooks are mainly for other projects using IPython as the core of their
11 11 interactive interface. Using them carelessly can easily break IPython!
12 12
13 13 String based transformations
14 14 ============================
15 15
16 16 .. currentmodule:: IPython.core.inputtransforms
17 17
18 18 When the user enters code, it is first processed as a string. By the
19 19 end of this stage, it must be valid Python syntax.
20 20
21 21 .. versionchanged:: 7.0
22 22
23 23 The API for string and token-based transformations has been completely
24 24 redesigned. Any third party code extending input transformation will need to
25 25 be rewritten. The new API is, hopefully, simpler.
26 26
27 27 String based transformations are functions which accept a list of strings:
28 28 each string is a single line of the input cell, including its line ending.
29 29 The transformation function should return output in the same structure.
30 30
31 31 These transformations are in two groups, accessible as attributes of
32 32 the :class:`~IPython.core.interactiveshell.InteractiveShell` instance.
33 33 Each group is a list of transformation functions.
34 34
35 35 * ``input_transformers_cleanup`` run first on input, to do things like stripping
36 36 prompts and leading indents from copied code. It may not be possible at this
37 37 stage to parse the input as valid Python code.
38 38 * Then IPython runs its own transformations to handle its special syntax, like
39 39 ``%magics`` and ``!system`` commands. This part does not expose extension
40 40 points.
41 41 * ``input_transformers_post`` run as the last step, to do things like converting
42 42 float literals into decimal objects. These may attempt to parse the input as
43 43 Python code.
44 44
45 45 These transformers may raise :exc:`SyntaxError` if the input code is invalid, but
46 46 in most cases it is clearer to pass unrecognised code through unmodified and let
47 47 Python's own parser decide whether it is valid.
48 48
49 49 For example, imagine we want to obfuscate our code by reversing each line, so
50 50 we'd write ``)5(f =+ a`` instead of ``a += f(5)``. Here's how we could swap it
51 51 back the right way before IPython tries to run it::
52 52
53 53 def reverse_line_chars(lines):
54 54 new_lines = []
55 55 for line in lines:
56 56 chars = line[:-1] # the newline needs to stay at the end
57 57 new_lines.append(chars[::-1] + '\n')
58 58 return new_lines
59 59
60 60 To start using this::
61 61
62 62 ip = get_ipython()
63 63 ip.input_transformers_cleanup.append(reverse_line_chars)
64 64
65 .. versionadded:: 7.17
66
67 input_transformers can now have an attribute ``has_side_effects`` set to
68 `True`, which will prevent the transformers from being ran when IPython is
69 trying to guess whether the user input is complete.
70
71
72
65 73 AST transformations
66 74 ===================
67 75
68 76 After the code has been parsed as Python syntax, you can use Python's powerful
69 77 *Abstract Syntax Tree* tools to modify it. Subclass :class:`ast.NodeTransformer`,
70 78 and add an instance to ``shell.ast_transformers``.
71 79
72 80 This example wraps integer literals in an ``Integer`` class, which is useful for
73 81 mathematical frameworks that want to handle e.g. ``1/3`` as a precise fraction::
74 82
75 83
76 84 class IntegerWrapper(ast.NodeTransformer):
77 85 """Wraps all integers in a call to Integer()"""
78 86 def visit_Num(self, node):
79 87 if isinstance(node.n, int):
80 88 return ast.Call(func=ast.Name(id='Integer', ctx=ast.Load()),
81 89 args=[node], keywords=[])
82 90 return node
General Comments 0
You need to be logged in to leave comments. Login now