##// END OF EJS Templates
Merge pull request #11041 from takluyver/inputtransformer2...
Matthias Bussonnier -
r24504:0fbad832 merge
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (638 lines changed) Show them Hide them
@@ -0,0 +1,638 b''
1 """Input transformer machinery to support IPython special syntax.
2
3 This includes the machinery to recognise and transform ``%magic`` commands,
4 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
5
6 Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
7 deprecated in 7.0.
8 """
9
10 # Copyright (c) IPython Development Team.
11 # Distributed under the terms of the Modified BSD License.
12
13 from codeop import compile_command
14 import re
15 import tokenize
16 from typing import List, Tuple
17 import warnings
18
19 _indent_re = re.compile(r'^[ \t]+')
20
21 def leading_indent(lines):
22 """Remove leading indentation.
23
24 If the first line starts with a spaces or tabs, the same whitespace will be
25 removed from each following line in the cell.
26 """
27 m = _indent_re.match(lines[0])
28 if not m:
29 return lines
30 space = m.group(0)
31 n = len(space)
32 return [l[n:] if l.startswith(space) else l
33 for l in lines]
34
35 class PromptStripper:
36 """Remove matching input prompts from a block of input.
37
38 Parameters
39 ----------
40 prompt_re : regular expression
41 A regular expression matching any input prompt (including continuation,
42 e.g. ``...``)
43 initial_re : regular expression, optional
44 A regular expression matching only the initial prompt, but not continuation.
45 If no initial expression is given, prompt_re will be used everywhere.
46 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
47 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
48
49 If initial_re and prompt_re differ,
50 only initial_re will be tested against the first line.
51 If any prompt is found on the first two lines,
52 prompts will be stripped from the rest of the block.
53 """
54 def __init__(self, prompt_re, initial_re=None):
55 self.prompt_re = prompt_re
56 self.initial_re = initial_re or prompt_re
57
58 def _strip(self, lines):
59 return [self.prompt_re.sub('', l, count=1) for l in lines]
60
61 def __call__(self, lines):
62 if self.initial_re.match(lines[0]) or \
63 (len(lines) > 1 and self.prompt_re.match(lines[1])):
64 return self._strip(lines)
65 return lines
66
67 classic_prompt = PromptStripper(
68 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
69 initial_re=re.compile(r'^>>>( |$)')
70 )
71
72 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
73
74 def cell_magic(lines):
75 if not lines[0].startswith('%%'):
76 return lines
77 if re.match('%%\w+\?', lines[0]):
78 # This case will be handled by help_end
79 return lines
80 magic_name, _, first_line = lines[0][2:-1].partition(' ')
81 body = ''.join(lines[1:])
82 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
83 % (magic_name, first_line, body)]
84
85
86 def _find_assign_op(token_line):
87 """Get the index of the first assignment in the line ('=' not inside brackets)
88
89 Note: We don't try to support multiple special assignment (a = b = %foo)
90 """
91 paren_level = 0
92 for i, ti in enumerate(token_line):
93 s = ti.string
94 if s == '=' and paren_level == 0:
95 return i
96 if s in '([{':
97 paren_level += 1
98 elif s in ')]}':
99 if paren_level > 0:
100 paren_level -= 1
101
102 def find_end_of_continued_line(lines, start_line: int):
103 """Find the last line of a line explicitly extended using backslashes.
104
105 Uses 0-indexed line numbers.
106 """
107 end_line = start_line
108 while lines[end_line].endswith('\\\n'):
109 end_line += 1
110 if end_line >= len(lines):
111 break
112 return end_line
113
114 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
115 """Assemble a single line from multiple continued line pieces
116
117 Continued lines are lines ending in ``\``, and the line following the last
118 ``\`` in the block.
119
120 For example, this code continues over multiple lines::
121
122 if (assign_ix is not None) \
123 and (len(line) >= assign_ix + 2) \
124 and (line[assign_ix+1].string == '%') \
125 and (line[assign_ix+2].type == tokenize.NAME):
126
127 This statement contains four continued line pieces.
128 Assembling these pieces into a single line would give::
129
130 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
131
132 This uses 0-indexed line numbers. *start* is (lineno, colno).
133
134 Used to allow ``%magic`` and ``!system`` commands to be continued over
135 multiple lines.
136 """
137 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
138 return ' '.join([p[:-2] for p in parts[:-1]] # Strip backslash+newline
139 + [parts[-1][:-1]]) # Strip newline from last line
140
141 class TokenTransformBase:
142 """Base class for transformations which examine tokens.
143
144 Special syntax should not be transformed when it occurs inside strings or
145 comments. This is hard to reliably avoid with regexes. The solution is to
146 tokenise the code as Python, and recognise the special syntax in the tokens.
147
148 IPython's special syntax is not valid Python syntax, so tokenising may go
149 wrong after the special syntax starts. These classes therefore find and
150 transform *one* instance of special syntax at a time into regular Python
151 syntax. After each transformation, tokens are regenerated to find the next
152 piece of special syntax.
153
154 Subclasses need to implement one class method (find)
155 and one regular method (transform).
156
157 The priority attribute can select which transformation to apply if multiple
158 transformers match in the same place. Lower numbers have higher priority.
159 This allows "%magic?" to be turned into a help call rather than a magic call.
160 """
161 # Lower numbers -> higher priority (for matches in the same location)
162 priority = 10
163
164 def sortby(self):
165 return self.start_line, self.start_col, self.priority
166
167 def __init__(self, start):
168 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
169 self.start_col = start[1]
170
171 @classmethod
172 def find(cls, tokens_by_line):
173 """Find one instance of special syntax in the provided tokens.
174
175 Tokens are grouped into logical lines for convenience,
176 so it is easy to e.g. look at the first token of each line.
177 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
178
179 This should return an instance of its class, pointing to the start
180 position it has found, or None if it found no match.
181 """
182 raise NotImplementedError
183
184 def transform(self, lines: List[str]):
185 """Transform one instance of special syntax found by ``find()``
186
187 Takes a list of strings representing physical lines,
188 returns a similar list of transformed lines.
189 """
190 raise NotImplementedError
191
192 class MagicAssign(TokenTransformBase):
193 """Transformer for assignments from magics (a = %foo)"""
194 @classmethod
195 def find(cls, tokens_by_line):
196 """Find the first magic assignment (a = %foo) in the cell.
197 """
198 for line in tokens_by_line:
199 assign_ix = _find_assign_op(line)
200 if (assign_ix is not None) \
201 and (len(line) >= assign_ix + 2) \
202 and (line[assign_ix+1].string == '%') \
203 and (line[assign_ix+2].type == tokenize.NAME):
204 return cls(line[assign_ix+1].start)
205
206 def transform(self, lines: List[str]):
207 """Transform a magic assignment found by the ``find()`` classmethod.
208 """
209 start_line, start_col = self.start_line, self.start_col
210 lhs = lines[start_line][:start_col]
211 end_line = find_end_of_continued_line(lines, start_line)
212 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
213 assert rhs.startswith('%'), rhs
214 magic_name, _, args = rhs[1:].partition(' ')
215
216 lines_before = lines[:start_line]
217 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
218 new_line = lhs + call + '\n'
219 lines_after = lines[end_line+1:]
220
221 return lines_before + [new_line] + lines_after
222
223
224 class SystemAssign(TokenTransformBase):
225 """Transformer for assignments from system commands (a = !foo)"""
226 @classmethod
227 def find(cls, tokens_by_line):
228 """Find the first system assignment (a = !foo) in the cell.
229 """
230 for line in tokens_by_line:
231 assign_ix = _find_assign_op(line)
232 if (assign_ix is not None) \
233 and (len(line) >= assign_ix + 2) \
234 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
235 ix = assign_ix + 1
236
237 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
238 if line[ix].string == '!':
239 return cls(line[ix].start)
240 elif not line[ix].string.isspace():
241 break
242 ix += 1
243
244 def transform(self, lines: List[str]):
245 """Transform a system assignment found by the ``find()`` classmethod.
246 """
247 start_line, start_col = self.start_line, self.start_col
248
249 lhs = lines[start_line][:start_col]
250 end_line = find_end_of_continued_line(lines, start_line)
251 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
252 assert rhs.startswith('!'), rhs
253 cmd = rhs[1:]
254
255 lines_before = lines[:start_line]
256 call = "get_ipython().getoutput({!r})".format(cmd)
257 new_line = lhs + call + '\n'
258 lines_after = lines[end_line + 1:]
259
260 return lines_before + [new_line] + lines_after
261
262 # The escape sequences that define the syntax transformations IPython will
263 # apply to user input. These can NOT be just changed here: many regular
264 # expressions and other parts of the code may use their hardcoded values, and
265 # for all intents and purposes they constitute the 'IPython syntax', so they
266 # should be considered fixed.
267
268 ESC_SHELL = '!' # Send line to underlying system shell
269 ESC_SH_CAP = '!!' # Send line to system shell and capture output
270 ESC_HELP = '?' # Find information about object
271 ESC_HELP2 = '??' # Find extra-detailed information about object
272 ESC_MAGIC = '%' # Call magic function
273 ESC_MAGIC2 = '%%' # Call cell-magic function
274 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
275 ESC_QUOTE2 = ';' # Quote all args as a single string, call
276 ESC_PAREN = '/' # Call first argument with rest of line as arguments
277
278 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
279 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
280
281 def _make_help_call(target, esc, next_input=None):
282 """Prepares a pinfo(2)/psearch call from a target name and the escape
283 (i.e. ? or ??)"""
284 method = 'pinfo2' if esc == '??' \
285 else 'psearch' if '*' in target \
286 else 'pinfo'
287 arg = " ".join([method, target])
288 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
289 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
290 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
291 if next_input is None:
292 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
293 else:
294 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
295 (next_input, t_magic_name, t_magic_arg_s)
296
297 def _tr_help(content):
298 """Translate lines escaped with: ?
299
300 A naked help line should fire the intro help screen (shell.show_usage())
301 """
302 if not content:
303 return 'get_ipython().show_usage()'
304
305 return _make_help_call(content, '?')
306
307 def _tr_help2(content):
308 """Translate lines escaped with: ??
309
310 A naked help line should fire the intro help screen (shell.show_usage())
311 """
312 if not content:
313 return 'get_ipython().show_usage()'
314
315 return _make_help_call(content, '??')
316
317 def _tr_magic(content):
318 "Translate lines escaped with a percent sign: %"
319 name, _, args = content.partition(' ')
320 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
321
322 def _tr_quote(content):
323 "Translate lines escaped with a comma: ,"
324 name, _, args = content.partition(' ')
325 return '%s("%s")' % (name, '", "'.join(args.split()) )
326
327 def _tr_quote2(content):
328 "Translate lines escaped with a semicolon: ;"
329 name, _, args = content.partition(' ')
330 return '%s("%s")' % (name, args)
331
332 def _tr_paren(content):
333 "Translate lines escaped with a slash: /"
334 name, _, args = content.partition(' ')
335 return '%s(%s)' % (name, ", ".join(args.split()))
336
337 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
338 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
339 ESC_HELP : _tr_help,
340 ESC_HELP2 : _tr_help2,
341 ESC_MAGIC : _tr_magic,
342 ESC_QUOTE : _tr_quote,
343 ESC_QUOTE2 : _tr_quote2,
344 ESC_PAREN : _tr_paren }
345
346 class EscapedCommand(TokenTransformBase):
347 """Transformer for escaped commands like %foo, !foo, or /foo"""
348 @classmethod
349 def find(cls, tokens_by_line):
350 """Find the first escaped command (%foo, !foo, etc.) in the cell.
351 """
352 for line in tokens_by_line:
353 ix = 0
354 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
355 ix += 1
356 if line[ix].string in ESCAPE_SINGLES:
357 return cls(line[ix].start)
358
359 def transform(self, lines):
360 """Transform an escaped line found by the ``find()`` classmethod.
361 """
362 start_line, start_col = self.start_line, self.start_col
363
364 indent = lines[start_line][:start_col]
365 end_line = find_end_of_continued_line(lines, start_line)
366 line = assemble_continued_line(lines, (start_line, start_col), end_line)
367
368 if line[:2] in ESCAPE_DOUBLES:
369 escape, content = line[:2], line[2:]
370 else:
371 escape, content = line[:1], line[1:]
372 call = tr[escape](content)
373
374 lines_before = lines[:start_line]
375 new_line = indent + call + '\n'
376 lines_after = lines[end_line + 1:]
377
378 return lines_before + [new_line] + lines_after
379
380 _help_end_re = re.compile(r"""(%{0,2}
381 [a-zA-Z_*][\w*]* # Variable name
382 (\.[a-zA-Z_*][\w*]*)* # .etc.etc
383 )
384 (\?\??)$ # ? or ??
385 """,
386 re.VERBOSE)
387
388 class HelpEnd(TokenTransformBase):
389 """Transformer for help syntax: obj? and obj??"""
390 # This needs to be higher priority (lower number) than EscapedCommand so
391 # that inspecting magics (%foo?) works.
392 priority = 5
393
394 def __init__(self, start, q_locn):
395 super().__init__(start)
396 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
397 self.q_col = q_locn[1]
398
399 @classmethod
400 def find(cls, tokens_by_line):
401 """Find the first help command (foo?) in the cell.
402 """
403 for line in tokens_by_line:
404 # Last token is NEWLINE; look at last but one
405 if len(line) > 2 and line[-2].string == '?':
406 # Find the first token that's not INDENT/DEDENT
407 ix = 0
408 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
409 ix += 1
410 return cls(line[ix].start, line[-2].start)
411
412 def transform(self, lines):
413 """Transform a help command found by the ``find()`` classmethod.
414 """
415 piece = ''.join(lines[self.start_line:self.q_line+1])
416 indent, content = piece[:self.start_col], piece[self.start_col:]
417 lines_before = lines[:self.start_line]
418 lines_after = lines[self.q_line + 1:]
419
420 m = _help_end_re.search(content)
421 assert m is not None, content
422 target = m.group(1)
423 esc = m.group(3)
424
425 # If we're mid-command, put it back on the next prompt for the user.
426 next_input = None
427 if (not lines_before) and (not lines_after) \
428 and content.strip() != m.group(0):
429 next_input = content.rstrip('?\n')
430
431 call = _make_help_call(target, esc, next_input=next_input)
432 new_line = indent + call + '\n'
433
434 return lines_before + [new_line] + lines_after
435
436 def make_tokens_by_line(lines):
437 """Tokenize a series of lines and group tokens by line.
438
439 The tokens for a multiline Python string or expression are
440 grouped as one line.
441 """
442 # NL tokens are used inside multiline expressions, but also after blank
443 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
444 # We want to group the former case together but split the latter, so we
445 # track parentheses level, similar to the internals of tokenize.
446 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL
447 tokens_by_line = [[]]
448 parenlev = 0
449 try:
450 for token in tokenize.generate_tokens(iter(lines).__next__):
451 tokens_by_line[-1].append(token)
452 if (token.type == NEWLINE) \
453 or ((token.type == NL) and (parenlev <= 0)):
454 tokens_by_line.append([])
455 elif token.string in {'(', '[', '{'}:
456 parenlev += 1
457 elif token.string in {')', ']', '}'}:
458 if parenlev > 0:
459 parenlev -= 1
460 except tokenize.TokenError:
461 # Input ended in a multiline string or expression. That's OK for us.
462 pass
463
464 return tokens_by_line
465
466 def show_linewise_tokens(s: str):
467 """For investigation and debugging"""
468 if not s.endswith('\n'):
469 s += '\n'
470 lines = s.splitlines(keepends=True)
471 for line in make_tokens_by_line(lines):
472 print("Line -------")
473 for tokinfo in line:
474 print(" ", tokinfo)
475
476 # Arbitrary limit to prevent getting stuck in infinite loops
477 TRANSFORM_LOOP_LIMIT = 500
478
479 class TransformerManager:
480 """Applies various transformations to a cell or code block.
481
482 The key methods for external use are ``transform_cell()``
483 and ``check_complete()``.
484 """
485 def __init__(self):
486 self.cleanup_transforms = [
487 leading_indent,
488 classic_prompt,
489 ipython_prompt,
490 ]
491 self.line_transforms = [
492 cell_magic,
493 ]
494 self.token_transformers = [
495 MagicAssign,
496 SystemAssign,
497 EscapedCommand,
498 HelpEnd,
499 ]
500
501 def do_one_token_transform(self, lines):
502 """Find and run the transform earliest in the code.
503
504 Returns (changed, lines).
505
506 This method is called repeatedly until changed is False, indicating
507 that all available transformations are complete.
508
509 The tokens following IPython special syntax might not be valid, so
510 the transformed code is retokenised every time to identify the next
511 piece of special syntax. Hopefully long code cells are mostly valid
512 Python, not using lots of IPython special syntax, so this shouldn't be
513 a performance issue.
514 """
515 tokens_by_line = make_tokens_by_line(lines)
516 candidates = []
517 for transformer_cls in self.token_transformers:
518 transformer = transformer_cls.find(tokens_by_line)
519 if transformer:
520 candidates.append(transformer)
521
522 if not candidates:
523 # Nothing to transform
524 return False, lines
525
526 transformer = min(candidates, key=TokenTransformBase.sortby)
527 return True, transformer.transform(lines)
528
529 def do_token_transforms(self, lines):
530 for _ in range(TRANSFORM_LOOP_LIMIT):
531 changed, lines = self.do_one_token_transform(lines)
532 if not changed:
533 return lines
534
535 raise RuntimeError("Input transformation still changing after "
536 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
537
538 def transform_cell(self, cell: str) -> str:
539 """Transforms a cell of input code"""
540 if not cell.endswith('\n'):
541 cell += '\n' # Ensure the cell has a trailing newline
542 lines = cell.splitlines(keepends=True)
543 for transform in self.cleanup_transforms + self.line_transforms:
544 #print(transform, lines)
545 lines = transform(lines)
546
547 lines = self.do_token_transforms(lines)
548 return ''.join(lines)
549
550 def check_complete(self, cell: str):
551 """Return whether a block of code is ready to execute, or should be continued
552
553 Parameters
554 ----------
555 source : string
556 Python input code, which can be multiline.
557
558 Returns
559 -------
560 status : str
561 One of 'complete', 'incomplete', or 'invalid' if source is not a
562 prefix of valid code.
563 indent_spaces : int or None
564 The number of spaces by which to indent the next line of code. If
565 status is not 'incomplete', this is None.
566 """
567 if not cell.endswith('\n'):
568 cell += '\n' # Ensure the cell has a trailing newline
569 lines = cell.splitlines(keepends=True)
570 if lines[-1][:-1].endswith('\\'):
571 # Explicit backslash continuation
572 return 'incomplete', find_last_indent(lines)
573
574 try:
575 for transform in self.cleanup_transforms:
576 lines = transform(lines)
577 except SyntaxError:
578 return 'invalid', None
579
580 if lines[0].startswith('%%'):
581 # Special case for cell magics - completion marked by blank line
582 if lines[-1].strip():
583 return 'incomplete', find_last_indent(lines)
584 else:
585 return 'complete', None
586
587 try:
588 for transform in self.line_transforms:
589 lines = transform(lines)
590 lines = self.do_token_transforms(lines)
591 except SyntaxError:
592 return 'invalid', None
593
594 tokens_by_line = make_tokens_by_line(lines)
595 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
596 # We're in a multiline string or expression
597 return 'incomplete', find_last_indent(lines)
598
599 # Find the last token on the previous line that's not NEWLINE or COMMENT
600 toks_last_line = tokens_by_line[-2]
601 ix = len(toks_last_line) - 1
602 while ix >= 0 and toks_last_line[ix].type in {tokenize.NEWLINE,
603 tokenize.COMMENT}:
604 ix -= 1
605
606 if toks_last_line[ix].string == ':':
607 # The last line starts a block (e.g. 'if foo:')
608 ix = 0
609 while toks_last_line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
610 ix += 1
611 indent = toks_last_line[ix].start[1]
612 return 'incomplete', indent + 4
613
614 # If there's a blank line at the end, assume we're ready to execute.
615 if not lines[-1].strip():
616 return 'complete', None
617
618 # At this point, our checks think the code is complete (or invalid).
619 # We'll use codeop.compile_command to check this with the real parser.
620
621 try:
622 with warnings.catch_warnings():
623 warnings.simplefilter('error', SyntaxWarning)
624 res = compile_command(''.join(lines), symbol='exec')
625 except (SyntaxError, OverflowError, ValueError, TypeError,
626 MemoryError, SyntaxWarning):
627 return 'invalid', None
628 else:
629 if res is None:
630 return 'incomplete', find_last_indent(lines)
631 return 'complete', None
632
633
634 def find_last_indent(lines):
635 m = _indent_re.match(lines[-1])
636 if not m:
637 return 0
638 return len(m.group(0).replace('\t', ' '*4))
@@ -0,0 +1,195 b''
1 """Tests for the token-based transformers in IPython.core.inputtransformer2
2
3 Line-based transformers are the simpler ones; token-based transformers are
4 more complex. See test_inputtransformer2_line for tests for line-based
5 transformations.
6 """
7 import nose.tools as nt
8
9 from IPython.core import inputtransformer2 as ipt2
10 from IPython.core.inputtransformer2 import make_tokens_by_line
11
12 MULTILINE_MAGIC = ("""\
13 a = f()
14 %foo \\
15 bar
16 g()
17 """.splitlines(keepends=True), (2, 0), """\
18 a = f()
19 get_ipython().run_line_magic('foo', ' bar')
20 g()
21 """.splitlines(keepends=True))
22
23 INDENTED_MAGIC = ("""\
24 for a in range(5):
25 %ls
26 """.splitlines(keepends=True), (2, 4), """\
27 for a in range(5):
28 get_ipython().run_line_magic('ls', '')
29 """.splitlines(keepends=True))
30
31 MULTILINE_MAGIC_ASSIGN = ("""\
32 a = f()
33 b = %foo \\
34 bar
35 g()
36 """.splitlines(keepends=True), (2, 4), """\
37 a = f()
38 b = get_ipython().run_line_magic('foo', ' bar')
39 g()
40 """.splitlines(keepends=True))
41
42 MULTILINE_SYSTEM_ASSIGN = ("""\
43 a = f()
44 b = !foo \\
45 bar
46 g()
47 """.splitlines(keepends=True), (2, 4), """\
48 a = f()
49 b = get_ipython().getoutput('foo bar')
50 g()
51 """.splitlines(keepends=True))
52
53 AUTOCALL_QUOTE = (
54 [",f 1 2 3\n"], (1, 0),
55 ['f("1", "2", "3")\n']
56 )
57
58 AUTOCALL_QUOTE2 = (
59 [";f 1 2 3\n"], (1, 0),
60 ['f("1 2 3")\n']
61 )
62
63 AUTOCALL_PAREN = (
64 ["/f 1 2 3\n"], (1, 0),
65 ['f(1, 2, 3)\n']
66 )
67
68 SIMPLE_HELP = (
69 ["foo?\n"], (1, 0),
70 ["get_ipython().run_line_magic('pinfo', 'foo')\n"]
71 )
72
73 DETAILED_HELP = (
74 ["foo??\n"], (1, 0),
75 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"]
76 )
77
78 MAGIC_HELP = (
79 ["%foo?\n"], (1, 0),
80 ["get_ipython().run_line_magic('pinfo', '%foo')\n"]
81 )
82
83 HELP_IN_EXPR = (
84 ["a = b + c?\n"], (1, 0),
85 ["get_ipython().set_next_input('a = b + c');"
86 "get_ipython().run_line_magic('pinfo', 'c')\n"]
87 )
88
89 HELP_CONTINUED_LINE = ("""\
90 a = \\
91 zip?
92 """.splitlines(keepends=True), (1, 0),
93 [r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
94 )
95
96 HELP_MULTILINE = ("""\
97 (a,
98 b) = zip?
99 """.splitlines(keepends=True), (1, 0),
100 [r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
101 )
102
103 def check_find(transformer, case, match=True):
104 sample, expected_start, _ = case
105 tbl = make_tokens_by_line(sample)
106 res = transformer.find(tbl)
107 if match:
108 # start_line is stored 0-indexed, expected values are 1-indexed
109 nt.assert_equal((res.start_line+1, res.start_col), expected_start)
110 return res
111 else:
112 nt.assert_is(res, None)
113
114 def check_transform(transformer_cls, case):
115 lines, start, expected = case
116 transformer = transformer_cls(start)
117 nt.assert_equal(transformer.transform(lines), expected)
118
119 def test_continued_line():
120 lines = MULTILINE_MAGIC_ASSIGN[0]
121 nt.assert_equal(ipt2.find_end_of_continued_line(lines, 1), 2)
122
123 nt.assert_equal(ipt2.assemble_continued_line(lines, (1, 5), 2), "foo bar")
124
125 def test_find_assign_magic():
126 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
127 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
128
129 def test_transform_assign_magic():
130 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
131
132 def test_find_assign_system():
133 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
134 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
135 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
136 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
137
138 def test_transform_assign_system():
139 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
140
141 def test_find_magic_escape():
142 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
143 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
144 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
145
146 def test_transform_magic_escape():
147 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
148 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
149
150 def test_find_autocalls():
151 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
152 print("Testing %r" % case[0])
153 check_find(ipt2.EscapedCommand, case)
154
155 def test_transform_autocall():
156 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
157 print("Testing %r" % case[0])
158 check_transform(ipt2.EscapedCommand, case)
159
160 def test_find_help():
161 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
162 check_find(ipt2.HelpEnd, case)
163
164 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
165 nt.assert_equal(tf.q_line, 1)
166 nt.assert_equal(tf.q_col, 3)
167
168 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
169 nt.assert_equal(tf.q_line, 1)
170 nt.assert_equal(tf.q_col, 8)
171
172 # ? in a comment does not trigger help
173 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
174 # Nor in a string
175 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
176
177 def test_transform_help():
178 tf = ipt2.HelpEnd((1, 0), (1, 9))
179 nt.assert_equal(tf.transform(HELP_IN_EXPR[0]), HELP_IN_EXPR[2])
180
181 tf = ipt2.HelpEnd((1, 0), (2, 3))
182 nt.assert_equal(tf.transform(HELP_CONTINUED_LINE[0]), HELP_CONTINUED_LINE[2])
183
184 tf = ipt2.HelpEnd((1, 0), (2, 8))
185 nt.assert_equal(tf.transform(HELP_MULTILINE[0]), HELP_MULTILINE[2])
186
187 def test_check_complete():
188 cc = ipt2.TransformerManager().check_complete
189 nt.assert_equal(cc("a = 1"), ('complete', None))
190 nt.assert_equal(cc("for a in range(5):"), ('incomplete', 4))
191 nt.assert_equal(cc("raise = 2"), ('invalid', None))
192 nt.assert_equal(cc("a = [1,\n2,"), ('incomplete', 0))
193 nt.assert_equal(cc("a = '''\n hi"), ('incomplete', 3))
194 nt.assert_equal(cc("def a():\n x=1\n global x"), ('invalid', None))
195 nt.assert_equal(cc("a \\ "), ('invalid', None)) # Nothing allowed after backslash
@@ -0,0 +1,88 b''
1 """Tests for the line-based transformers in IPython.core.inputtransformer2
2
3 Line-based transformers are the simpler ones; token-based transformers are
4 more complex. See test_inputtransformer2 for tests for token-based transformers.
5 """
6 import nose.tools as nt
7
8 from IPython.core import inputtransformer2 as ipt2
9
10 CELL_MAGIC = ("""\
11 %%foo arg
12 body 1
13 body 2
14 """, """\
15 get_ipython().run_cell_magic('foo', 'arg', 'body 1\\nbody 2\\n')
16 """)
17
18 def test_cell_magic():
19 for sample, expected in [CELL_MAGIC]:
20 nt.assert_equal(ipt2.cell_magic(sample.splitlines(keepends=True)),
21 expected.splitlines(keepends=True))
22
23 CLASSIC_PROMPT = ("""\
24 >>> for a in range(5):
25 ... print(a)
26 """, """\
27 for a in range(5):
28 print(a)
29 """)
30
31 CLASSIC_PROMPT_L2 = ("""\
32 for a in range(5):
33 ... print(a)
34 ... print(a ** 2)
35 """, """\
36 for a in range(5):
37 print(a)
38 print(a ** 2)
39 """)
40
41 def test_classic_prompt():
42 for sample, expected in [CLASSIC_PROMPT, CLASSIC_PROMPT_L2]:
43 nt.assert_equal(ipt2.classic_prompt(sample.splitlines(keepends=True)),
44 expected.splitlines(keepends=True))
45
46 IPYTHON_PROMPT = ("""\
47 In [1]: for a in range(5):
48 ...: print(a)
49 """, """\
50 for a in range(5):
51 print(a)
52 """)
53
54 IPYTHON_PROMPT_L2 = ("""\
55 for a in range(5):
56 ...: print(a)
57 ...: print(a ** 2)
58 """, """\
59 for a in range(5):
60 print(a)
61 print(a ** 2)
62 """)
63
64 def test_ipython_prompt():
65 for sample, expected in [IPYTHON_PROMPT, IPYTHON_PROMPT_L2]:
66 nt.assert_equal(ipt2.ipython_prompt(sample.splitlines(keepends=True)),
67 expected.splitlines(keepends=True))
68
69 INDENT_SPACES = ("""\
70 if True:
71 a = 3
72 """, """\
73 if True:
74 a = 3
75 """)
76
77 INDENT_TABS = ("""\
78 \tif True:
79 \t\tb = 4
80 """, """\
81 if True:
82 \tb = 4
83 """)
84
85 def test_leading_indent():
86 for sample, expected in [INDENT_SPACES, INDENT_TABS]:
87 nt.assert_equal(ipt2.leading_indent(sample.splitlines(keepends=True)),
88 expected.splitlines(keepends=True))
@@ -0,0 +1,3 b''
1 * The API for transforming input before it is parsed as Python code has been
2 completely redesigned, and any custom input transformations will need to be
3 rewritten. See :doc:`/config/inputtransforms` for details of the new API.
@@ -131,7 +131,7 b' from types import SimpleNamespace'
131
131
132 from traitlets.config.configurable import Configurable
132 from traitlets.config.configurable import Configurable
133 from IPython.core.error import TryNext
133 from IPython.core.error import TryNext
134 from IPython.core.inputsplitter import ESC_MAGIC
134 from IPython.core.inputtransformer2 import ESC_MAGIC
135 from IPython.core.latex_symbols import latex_symbols, reverse_latex_symbol
135 from IPython.core.latex_symbols import latex_symbols, reverse_latex_symbol
136 from IPython.core.oinspect import InspectColors
136 from IPython.core.oinspect import InspectColors
137 from IPython.utils import generics
137 from IPython.utils import generics
@@ -1,4 +1,6 b''
1 """Input handling and transformation machinery.
1 """DEPRECATED: Input handling and transformation machinery.
2
3 This module was deprecated in IPython 7.0, in favour of inputtransformer2.
2
4
3 The first class in this module, :class:`InputSplitter`, is designed to tell when
5 The first class in this module, :class:`InputSplitter`, is designed to tell when
4 input from a line-oriented frontend is complete and should be executed, and when
6 input from a line-oriented frontend is complete and should be executed, and when
@@ -14,6 +16,11 b' and stores the results.'
14 For more details, see the class docstrings below.
16 For more details, see the class docstrings below.
15 """
17 """
16
18
19 from warnings import warn
20
21 warn('IPython.core.inputsplitter is deprecated since IPython 7 in favor of `IPython.core.inputtransformer2`',
22 DeprecationWarning)
23
17 # Copyright (c) IPython Development Team.
24 # Copyright (c) IPython Development Team.
18 # Distributed under the terms of the Modified BSD License.
25 # Distributed under the terms of the Modified BSD License.
19 import ast
26 import ast
@@ -1,4 +1,6 b''
1 """Input transformer classes to support IPython special syntax.
1 """DEPRECATED: Input transformer classes to support IPython special syntax.
2
3 This module was deprecated in IPython 7.0, in favour of inputtransformer2.
2
4
3 This includes the machinery to recognise and transform ``%magic`` commands,
5 This includes the machinery to recognise and transform ``%magic`` commands,
4 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
6 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
@@ -6,11 +8,11 b' This includes the machinery to recognise and transform ``%magic`` commands,'
6 import abc
8 import abc
7 import functools
9 import functools
8 import re
10 import re
11 import tokenize
12 from tokenize import generate_tokens, untokenize, TokenError
9 from io import StringIO
13 from io import StringIO
10
14
11 from IPython.core.splitinput import LineInfo
15 from IPython.core.splitinput import LineInfo
12 from IPython.utils import tokenize2
13 from IPython.utils.tokenize2 import generate_tokens, untokenize, TokenError
14
16
15 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
16 # Globals
18 # Globals
@@ -138,10 +140,10 b' class TokenInputTransformer(InputTransformer):'
138 for intok in self.tokenizer:
140 for intok in self.tokenizer:
139 tokens.append(intok)
141 tokens.append(intok)
140 t = intok[0]
142 t = intok[0]
141 if t == tokenize2.NEWLINE or (stop_at_NL and t == tokenize2.NL):
143 if t == tokenize.NEWLINE or (stop_at_NL and t == tokenize.NL):
142 # Stop before we try to pull a line we don't have yet
144 # Stop before we try to pull a line we don't have yet
143 break
145 break
144 elif t == tokenize2.ERRORTOKEN:
146 elif t == tokenize.ERRORTOKEN:
145 stop_at_NL = True
147 stop_at_NL = True
146 except TokenError:
148 except TokenError:
147 # Multi-line statement - stop and try again with the next line
149 # Multi-line statement - stop and try again with the next line
@@ -317,7 +319,7 b' def has_comment(src):'
317 comment : bool
319 comment : bool
318 True if source has a comment.
320 True if source has a comment.
319 """
321 """
320 return (tokenize2.COMMENT in _line_tokens(src))
322 return (tokenize.COMMENT in _line_tokens(src))
321
323
322 def ends_in_comment_or_string(src):
324 def ends_in_comment_or_string(src):
323 """Indicates whether or not an input line ends in a comment or within
325 """Indicates whether or not an input line ends in a comment or within
@@ -334,7 +336,7 b' def ends_in_comment_or_string(src):'
334 True if source ends in a comment or multiline string.
336 True if source ends in a comment or multiline string.
335 """
337 """
336 toktypes = _line_tokens(src)
338 toktypes = _line_tokens(src)
337 return (tokenize2.COMMENT in toktypes) or (_MULTILINE_STRING in toktypes)
339 return (tokenize.COMMENT in toktypes) or (_MULTILINE_STRING in toktypes)
338
340
339
341
340 @StatelessInputTransformer.wrap
342 @StatelessInputTransformer.wrap
@@ -48,7 +48,7 b' from IPython.core.error import InputRejected, UsageError'
48 from IPython.core.extensions import ExtensionManager
48 from IPython.core.extensions import ExtensionManager
49 from IPython.core.formatters import DisplayFormatter
49 from IPython.core.formatters import DisplayFormatter
50 from IPython.core.history import HistoryManager
50 from IPython.core.history import HistoryManager
51 from IPython.core.inputsplitter import ESC_MAGIC, ESC_MAGIC2
51 from IPython.core.inputtransformer2 import ESC_MAGIC, ESC_MAGIC2
52 from IPython.core.logger import Logger
52 from IPython.core.logger import Logger
53 from IPython.core.macro import Macro
53 from IPython.core.macro import Macro
54 from IPython.core.payload import PayloadManager
54 from IPython.core.payload import PayloadManager
@@ -333,15 +333,31 b' class InteractiveShell(SingletonConfigurable):'
333 filename = Unicode("<ipython console>")
333 filename = Unicode("<ipython console>")
334 ipython_dir= Unicode('').tag(config=True) # Set to get_ipython_dir() in __init__
334 ipython_dir= Unicode('').tag(config=True) # Set to get_ipython_dir() in __init__
335
335
336 # Input splitter, to transform input line by line and detect when a block
336 # Used to transform cells before running them, and check whether code is complete
337 # is ready to be executed.
337 input_transformer_manager = Instance('IPython.core.inputtransformer2.TransformerManager',
338 input_splitter = Instance('IPython.core.inputsplitter.IPythonInputSplitter',
338 ())
339 (), {'line_input_checker': True})
340
339
341 # This InputSplitter instance is used to transform completed cells before
340 @property
342 # running them. It allows cell magics to contain blank lines.
341 def input_transformers_cleanup(self):
343 input_transformer_manager = Instance('IPython.core.inputsplitter.IPythonInputSplitter',
342 return self.input_transformer_manager.cleanup_transforms
344 (), {'line_input_checker': False})
343
344 input_transformers_post = List([],
345 help="A list of string input transformers, to be applied after IPython's "
346 "own input transformations."
347 )
348
349 @property
350 def input_splitter(self):
351 """Make this available for backward compatibility (pre-7.0 release) with existing code.
352
353 For example, ipykernel ipykernel currently uses
354 `shell.input_splitter.check_complete`
355 """
356 from warnings import warn
357 warn("`input_splitter` is deprecated since IPython 7.0, prefer `input_transformer_manager`.",
358 DeprecationWarning, stacklevel=2
359 )
360 return self.input_transformer_manager
345
361
346 logstart = Bool(False, help=
362 logstart = Bool(False, help=
347 """
363 """
@@ -2656,6 +2672,7 b' class InteractiveShell(SingletonConfigurable):'
2656 -------
2672 -------
2657 result : :class:`ExecutionResult`
2673 result : :class:`ExecutionResult`
2658 """
2674 """
2675 result = None
2659 try:
2676 try:
2660 result = self._run_cell(
2677 result = self._run_cell(
2661 raw_cell, store_history, silent, shell_futures)
2678 raw_cell, store_history, silent, shell_futures)
@@ -2713,21 +2730,10 b' class InteractiveShell(SingletonConfigurable):'
2713 preprocessing_exc_tuple = None
2730 preprocessing_exc_tuple = None
2714 try:
2731 try:
2715 # Static input transformations
2732 # Static input transformations
2716 cell = self.input_transformer_manager.transform_cell(raw_cell)
2733 cell = self.transform_cell(raw_cell)
2717 except SyntaxError:
2734 except Exception:
2718 preprocessing_exc_tuple = sys.exc_info()
2735 preprocessing_exc_tuple = sys.exc_info()
2719 cell = raw_cell # cell has to exist so it can be stored/logged
2736 cell = raw_cell # cell has to exist so it can be stored/logged
2720 else:
2721 if len(cell.splitlines()) == 1:
2722 # Dynamic transformations - only applied for single line commands
2723 with self.builtin_trap:
2724 try:
2725 # use prefilter_lines to handle trailing newlines
2726 # restore trailing newline for ast.parse
2727 cell = self.prefilter_manager.prefilter_lines(cell) + '\n'
2728 except Exception:
2729 # don't allow prefilter errors to crash IPython
2730 preprocessing_exc_tuple = sys.exc_info()
2731
2737
2732 # Store raw and processed history
2738 # Store raw and processed history
2733 if store_history:
2739 if store_history:
@@ -2798,6 +2804,36 b' class InteractiveShell(SingletonConfigurable):'
2798 self.execution_count += 1
2804 self.execution_count += 1
2799
2805
2800 return result
2806 return result
2807
2808 def transform_cell(self, raw_cell):
2809 """Transform an input cell before parsing it.
2810
2811 Static transformations, implemented in IPython.core.inputtransformer2,
2812 deal with things like ``%magic`` and ``!system`` commands.
2813 These run on all input.
2814 Dynamic transformations, for things like unescaped magics and the exit
2815 autocall, depend on the state of the interpreter.
2816 These only apply to single line inputs.
2817
2818 These string-based transformations are followed by AST transformations;
2819 see :meth:`transform_ast`.
2820 """
2821 # Static input transformations
2822 cell = self.input_transformer_manager.transform_cell(raw_cell)
2823
2824 if len(cell.splitlines()) == 1:
2825 # Dynamic transformations - only applied for single line commands
2826 with self.builtin_trap:
2827 # use prefilter_lines to handle trailing newlines
2828 # restore trailing newline for ast.parse
2829 cell = self.prefilter_manager.prefilter_lines(cell) + '\n'
2830
2831 lines = cell.splitlines(keepends=True)
2832 for transform in self.input_transformers_post:
2833 lines = transform(lines)
2834 cell = ''.join(lines)
2835
2836 return cell
2801
2837
2802 def transform_ast(self, node):
2838 def transform_ast(self, node):
2803 """Apply the AST transformations from self.ast_transformers
2839 """Apply the AST transformations from self.ast_transformers
@@ -2999,7 +3035,7 b' class InteractiveShell(SingletonConfigurable):'
2999 When status is 'incomplete', this is some whitespace to insert on
3035 When status is 'incomplete', this is some whitespace to insert on
3000 the next line of the prompt.
3036 the next line of the prompt.
3001 """
3037 """
3002 status, nspaces = self.input_splitter.check_complete(code)
3038 status, nspaces = self.input_transformer_manager.check_complete(code)
3003 return status, ' ' * (nspaces or 0)
3039 return status, ' ' * (nspaces or 0)
3004
3040
3005 #-------------------------------------------------------------------------
3041 #-------------------------------------------------------------------------
@@ -19,7 +19,7 b' from getopt import getopt, GetoptError'
19 from traitlets.config.configurable import Configurable
19 from traitlets.config.configurable import Configurable
20 from IPython.core import oinspect
20 from IPython.core import oinspect
21 from IPython.core.error import UsageError
21 from IPython.core.error import UsageError
22 from IPython.core.inputsplitter import ESC_MAGIC, ESC_MAGIC2
22 from IPython.core.inputtransformer2 import ESC_MAGIC, ESC_MAGIC2
23 from decorator import decorator
23 from decorator import decorator
24 from IPython.utils.ipstruct import Struct
24 from IPython.utils.ipstruct import Struct
25 from IPython.utils.process import arg_split
25 from IPython.utils.process import arg_split
@@ -297,7 +297,7 b' python-profiler package from non-free.""")'
297 list_all=True, posix=False)
297 list_all=True, posix=False)
298 if cell is not None:
298 if cell is not None:
299 arg_str += '\n' + cell
299 arg_str += '\n' + cell
300 arg_str = self.shell.input_splitter.transform_cell(arg_str)
300 arg_str = self.shell.transform_cell(arg_str)
301 return self._run_with_profiler(arg_str, opts, self.shell.user_ns)
301 return self._run_with_profiler(arg_str, opts, self.shell.user_ns)
302
302
303 def _run_with_profiler(self, code, opts, namespace):
303 def _run_with_profiler(self, code, opts, namespace):
@@ -1033,7 +1033,7 b' python-profiler package from non-free.""")'
1033 # this code has tight coupling to the inner workings of timeit.Timer,
1033 # this code has tight coupling to the inner workings of timeit.Timer,
1034 # but is there a better way to achieve that the code stmt has access
1034 # but is there a better way to achieve that the code stmt has access
1035 # to the shell namespace?
1035 # to the shell namespace?
1036 transform = self.shell.input_splitter.transform_cell
1036 transform = self.shell.transform_cell
1037
1037
1038 if cell is None:
1038 if cell is None:
1039 # called as line magic
1039 # called as line magic
@@ -1191,9 +1191,9 b' python-profiler package from non-free.""")'
1191 raise UsageError("Can't use statement directly after '%%time'!")
1191 raise UsageError("Can't use statement directly after '%%time'!")
1192
1192
1193 if cell:
1193 if cell:
1194 expr = self.shell.input_transformer_manager.transform_cell(cell)
1194 expr = self.shell.transform_cell(cell)
1195 else:
1195 else:
1196 expr = self.shell.input_transformer_manager.transform_cell(line)
1196 expr = self.shell.transform_cell(line)
1197
1197
1198 # Minimum time above which parse time will be reported
1198 # Minimum time above which parse time will be reported
1199 tp_min = 0.1
1199 tp_min = 0.1
@@ -14,7 +14,7 b' import re'
14
14
15 from IPython.core.autocall import IPyAutocall
15 from IPython.core.autocall import IPyAutocall
16 from traitlets.config.configurable import Configurable
16 from traitlets.config.configurable import Configurable
17 from IPython.core.inputsplitter import (
17 from IPython.core.inputtransformer2 import (
18 ESC_MAGIC,
18 ESC_MAGIC,
19 ESC_QUOTE,
19 ESC_QUOTE,
20 ESC_QUOTE2,
20 ESC_QUOTE2,
@@ -833,28 +833,22 b' def test_user_expression():'
833 class TestSyntaxErrorTransformer(unittest.TestCase):
833 class TestSyntaxErrorTransformer(unittest.TestCase):
834 """Check that SyntaxError raised by an input transformer is handled by run_cell()"""
834 """Check that SyntaxError raised by an input transformer is handled by run_cell()"""
835
835
836 class SyntaxErrorTransformer(InputTransformer):
836 @staticmethod
837
837 def transformer(lines):
838 def push(self, line):
838 for line in lines:
839 pos = line.find('syntaxerror')
839 pos = line.find('syntaxerror')
840 if pos >= 0:
840 if pos >= 0:
841 e = SyntaxError('input contains "syntaxerror"')
841 e = SyntaxError('input contains "syntaxerror"')
842 e.text = line
842 e.text = line
843 e.offset = pos + 1
843 e.offset = pos + 1
844 raise e
844 raise e
845 return line
845 return lines
846
847 def reset(self):
848 pass
849
846
850 def setUp(self):
847 def setUp(self):
851 self.transformer = TestSyntaxErrorTransformer.SyntaxErrorTransformer()
848 ip.input_transformers_post.append(self.transformer)
852 ip.input_splitter.python_line_transforms.append(self.transformer)
853 ip.input_transformer_manager.python_line_transforms.append(self.transformer)
854
849
855 def tearDown(self):
850 def tearDown(self):
856 ip.input_splitter.python_line_transforms.remove(self.transformer)
851 ip.input_transformers_post.remove(self.transformer)
857 ip.input_transformer_manager.python_line_transforms.remove(self.transformer)
858
852
859 def test_syntaxerror_input_transformer(self):
853 def test_syntaxerror_input_transformer(self):
860 with tt.AssertPrints('1234'):
854 with tt.AssertPrints('1234'):
@@ -691,8 +691,8 b' class CellMagicTestCase(TestCase):'
691 out = _ip.run_cell_magic(magic, 'a', 'b')
691 out = _ip.run_cell_magic(magic, 'a', 'b')
692 nt.assert_equal(out, ('a','b'))
692 nt.assert_equal(out, ('a','b'))
693 # Via run_cell, it goes into the user's namespace via displayhook
693 # Via run_cell, it goes into the user's namespace via displayhook
694 _ip.run_cell('%%' + magic +' c\nd')
694 _ip.run_cell('%%' + magic +' c\nd\n')
695 nt.assert_equal(_ip.user_ns['_'], ('c','d'))
695 nt.assert_equal(_ip.user_ns['_'], ('c','d\n'))
696
696
697 def test_cell_magic_func_deco(self):
697 def test_cell_magic_func_deco(self):
698 "Cell magic using simple decorator"
698 "Cell magic using simple decorator"
@@ -52,5 +52,9 b' class TestFileToRun(tt.TempFileMixin, unittest.TestCase):'
52 self.mktmp(src)
52 self.mktmp(src)
53
53
54 out, err = tt.ipexec(self.fname, options=['-i'],
54 out, err = tt.ipexec(self.fname, options=['-i'],
55 commands=['"__file__" in globals()', 'exit()'])
55 commands=['"__file__" in globals()', 'print(123)', 'exit()'])
56 self.assertIn("False", out)
56 if 'False' not in out:
57 print("Subprocess stderr:")
58 print(err)
59 print('-----')
60 raise AssertionError("'False' not found in %r" % out)
@@ -292,6 +292,7 b' class EmbeddedSphinxShell(object):'
292 self.user_ns = self.IP.user_ns
292 self.user_ns = self.IP.user_ns
293 self.user_global_ns = self.IP.user_global_ns
293 self.user_global_ns = self.IP.user_global_ns
294
294
295 self.lines_waiting = []
295 self.input = ''
296 self.input = ''
296 self.output = ''
297 self.output = ''
297 self.tmp_profile_dir = tmp_profile_dir
298 self.tmp_profile_dir = tmp_profile_dir
@@ -326,13 +327,12 b' class EmbeddedSphinxShell(object):'
326 """process the input, capturing stdout"""
327 """process the input, capturing stdout"""
327
328
328 stdout = sys.stdout
329 stdout = sys.stdout
329 splitter = self.IP.input_splitter
330 try:
330 try:
331 sys.stdout = self.cout
331 sys.stdout = self.cout
332 splitter.push(line)
332 self.lines_waiting.append(line)
333 more = splitter.push_accepts_more()
333 if self.IP.check_complete()[0] != 'incomplete':
334 if not more:
334 source_raw = ''.join(self.lines_waiting)
335 source_raw = splitter.raw_reset()
335 self.lines_waiting = []
336 self.IP.run_cell(source_raw, store_history=store_history)
336 self.IP.run_cell(source_raw, store_history=store_history)
337 finally:
337 finally:
338 sys.stdout = stdout
338 sys.stdout = stdout
@@ -226,16 +226,14 b' class TerminalInteractiveShell(InteractiveShell):'
226 def init_prompt_toolkit_cli(self):
226 def init_prompt_toolkit_cli(self):
227 if self.simple_prompt:
227 if self.simple_prompt:
228 # Fall back to plain non-interactive output for tests.
228 # Fall back to plain non-interactive output for tests.
229 # This is very limited, and only accepts a single line.
229 # This is very limited.
230 def prompt():
230 def prompt():
231 isp = self.input_splitter
232 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
231 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
232 lines = [input(prompt_text)]
233 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
233 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
234 while isp.push_accepts_more():
234 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
235 line = input(prompt_text)
235 lines.append( input(prompt_continuation) )
236 isp.push(line)
236 return '\n'.join(lines)
237 prompt_text = prompt_continuation
238 return isp.source_reset()
239 self.prompt_for_code = prompt
237 self.prompt_for_code = prompt
240 return
238 return
241
239
@@ -9,7 +9,6 b' import os'
9 import sys
9 import sys
10
10
11 from IPython.core.error import TryNext, UsageError
11 from IPython.core.error import TryNext, UsageError
12 from IPython.core.inputsplitter import IPythonInputSplitter
13 from IPython.core.magic import Magics, magics_class, line_magic
12 from IPython.core.magic import Magics, magics_class, line_magic
14 from IPython.lib.clipboard import ClipboardEmpty
13 from IPython.lib.clipboard import ClipboardEmpty
15 from IPython.utils.text import SList, strip_email_quotes
14 from IPython.utils.text import SList, strip_email_quotes
@@ -40,7 +39,6 b' def get_pasted_lines(sentinel, l_input=py3compat.input, quiet=False):'
40 class TerminalMagics(Magics):
39 class TerminalMagics(Magics):
41 def __init__(self, shell):
40 def __init__(self, shell):
42 super(TerminalMagics, self).__init__(shell)
41 super(TerminalMagics, self).__init__(shell)
43 self.input_splitter = IPythonInputSplitter()
44
42
45 def store_or_execute(self, block, name):
43 def store_or_execute(self, block, name):
46 """ Execute a block, or store it in a variable, per the user's request.
44 """ Execute a block, or store it in a variable, per the user's request.
@@ -68,9 +68,8 b' def create_ipython_shortcuts(shell):'
68 & insert_mode
68 & insert_mode
69 & cursor_in_leading_ws
69 & cursor_in_leading_ws
70 ))(indent_buffer)
70 ))(indent_buffer)
71
71 kb.add('c-o', filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode)
72 kb.add('c-o', filter=(has_focus(DEFAULT_BUFFER)
72 )(newline_autoindent_outer(shell.input_transformer_manager))
73 & emacs_insert_mode))(newline_autoindent_outer(shell.input_splitter))
74
73
75 kb.add('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor)
74 kb.add('f2', filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor)
76
75
@@ -107,13 +106,13 b' def newline_or_execute_outer(shell):'
107 check_text = d.text
106 check_text = d.text
108 else:
107 else:
109 check_text = d.text[:d.cursor_position]
108 check_text = d.text[:d.cursor_position]
110 status, indent = shell.input_splitter.check_complete(check_text + '\n')
109 status, indent = shell.check_complete(check_text)
111
110
112 if not (d.on_last_line or
111 if not (d.on_last_line or
113 d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
112 d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
114 ):
113 ):
115 if shell.autoindent:
114 if shell.autoindent:
116 b.insert_text('\n' + (' ' * (indent or 0)))
115 b.insert_text('\n' + indent)
117 else:
116 else:
118 b.insert_text('\n')
117 b.insert_text('\n')
119 return
118 return
@@ -122,7 +121,7 b' def newline_or_execute_outer(shell):'
122 b.validate_and_handle()
121 b.validate_and_handle()
123 else:
122 else:
124 if shell.autoindent:
123 if shell.autoindent:
125 b.insert_text('\n' + (' ' * (indent or 0)))
124 b.insert_text('\n' + indent)
126 else:
125 else:
127 b.insert_text('\n')
126 b.insert_text('\n')
128 return newline_or_execute
127 return newline_or_execute
@@ -96,9 +96,7 b' class InteractiveShellTestCase(unittest.TestCase):'
96 @mock_input
96 @mock_input
97 def test_inputtransformer_syntaxerror(self):
97 def test_inputtransformer_syntaxerror(self):
98 ip = get_ipython()
98 ip = get_ipython()
99 transformer = SyntaxErrorTransformer()
99 ip.input_transformers_post.append(syntax_error_transformer)
100 ip.input_splitter.python_line_transforms.append(transformer)
101 ip.input_transformer_manager.python_line_transforms.append(transformer)
102
100
103 try:
101 try:
104 #raise Exception
102 #raise Exception
@@ -112,8 +110,7 b' class InteractiveShellTestCase(unittest.TestCase):'
112 yield u'print(4*4)'
110 yield u'print(4*4)'
113
111
114 finally:
112 finally:
115 ip.input_splitter.python_line_transforms.remove(transformer)
113 ip.input_transformers_post.remove(syntax_error_transformer)
116 ip.input_transformer_manager.python_line_transforms.remove(transformer)
117
114
118 def test_plain_text_only(self):
115 def test_plain_text_only(self):
119 ip = get_ipython()
116 ip = get_ipython()
@@ -146,20 +143,17 b' class InteractiveShellTestCase(unittest.TestCase):'
146 self.assertEqual(data, {'text/plain': repr(obj)})
143 self.assertEqual(data, {'text/plain': repr(obj)})
147 assert captured.stdout == ''
144 assert captured.stdout == ''
148
145
149
146 def syntax_error_transformer(lines):
150
147 """Transformer that throws SyntaxError if 'syntaxerror' is in the code."""
151 class SyntaxErrorTransformer(InputTransformer):
148 for line in lines:
152 def push(self, line):
153 pos = line.find('syntaxerror')
149 pos = line.find('syntaxerror')
154 if pos >= 0:
150 if pos >= 0:
155 e = SyntaxError('input contains "syntaxerror"')
151 e = SyntaxError('input contains "syntaxerror"')
156 e.text = line
152 e.text = line
157 e.offset = pos + 1
153 e.offset = pos + 1
158 raise e
154 raise e
159 return line
155 return lines
160
156
161 def reset(self):
162 pass
163
157
164 class TerminalMagicsTestCase(unittest.TestCase):
158 class TerminalMagicsTestCase(unittest.TestCase):
165 def test_paste_magics_blankline(self):
159 def test_paste_magics_blankline(self):
@@ -7,7 +7,7 b' from collections import namedtuple'
7 from io import StringIO
7 from io import StringIO
8 from keyword import iskeyword
8 from keyword import iskeyword
9
9
10 from . import tokenize2
10 import tokenize
11
11
12
12
13 Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line'])
13 Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line'])
@@ -15,9 +15,9 b" Token = namedtuple('Token', ['token', 'text', 'start', 'end', 'line'])"
15 def generate_tokens(readline):
15 def generate_tokens(readline):
16 """wrap generate_tokens to catch EOF errors"""
16 """wrap generate_tokens to catch EOF errors"""
17 try:
17 try:
18 for token in tokenize2.generate_tokens(readline):
18 for token in tokenize.generate_tokens(readline):
19 yield token
19 yield token
20 except tokenize2.TokenError:
20 except tokenize.TokenError:
21 # catch EOF error
21 # catch EOF error
22 return
22 return
23
23
@@ -99,12 +99,12 b' def token_at_cursor(cell, cursor_pos=0):'
99 # don't consume it
99 # don't consume it
100 break
100 break
101
101
102 if tok.token == tokenize2.NAME and not iskeyword(tok.text):
102 if tok.token == tokenize.NAME and not iskeyword(tok.text):
103 if names and tokens and tokens[-1].token == tokenize2.OP and tokens[-1].text == '.':
103 if names and tokens and tokens[-1].token == tokenize.OP and tokens[-1].text == '.':
104 names[-1] = "%s.%s" % (names[-1], tok.text)
104 names[-1] = "%s.%s" % (names[-1], tok.text)
105 else:
105 else:
106 names.append(tok.text)
106 names.append(tok.text)
107 elif tok.token == tokenize2.OP:
107 elif tok.token == tokenize.OP:
108 if tok.text == '=' and names:
108 if tok.text == '=' and names:
109 # don't inspect the lhs of an assignment
109 # don't inspect the lhs of an assignment
110 names.pop(-1)
110 names.pop(-1)
@@ -45,7 +45,7 b' class _DummyTerminal(object):'
45 """Used as a buffer to get prompt_toolkit bindings
45 """Used as a buffer to get prompt_toolkit bindings
46 """
46 """
47 handle_return = None
47 handle_return = None
48 input_splitter = None
48 input_transformer_manager = None
49 display_completions = None
49 display_completions = None
50
50
51 ipy_bindings = create_ipython_shortcuts(_DummyTerminal()).bindings
51 ipy_bindings = create_ipython_shortcuts(_DummyTerminal()).bindings
@@ -282,8 +282,8 b' IPython configuration::'
282 else: # insert a newline with auto-indentation...
282 else: # insert a newline with auto-indentation...
283
283
284 if document.line_count > 1: text = text[:document.cursor_position]
284 if document.line_count > 1: text = text[:document.cursor_position]
285 indent = shell.input_splitter.check_complete(text + '\n')[1] or 0
285 indent = shell.check_complete(text)[1]
286 buffer.insert_text('\n' + ' ' * indent)
286 buffer.insert_text('\n' + indent)
287
287
288 # if you just wanted a plain newline without any indentation, you
288 # if you just wanted a plain newline without any indentation, you
289 # could use `buffer.insert_text('\n')` instead of the lines above
289 # could use `buffer.insert_text('\n')` instead of the lines above
@@ -15,163 +15,52 b' String based transformations'
15
15
16 .. currentmodule:: IPython.core.inputtransforms
16 .. currentmodule:: IPython.core.inputtransforms
17
17
18 When the user enters a line of code, it is first processed as a string. By the
18 When the user enters code, it is first processed as a string. By the
19 end of this stage, it must be valid Python syntax.
19 end of this stage, it must be valid Python syntax.
20
20
21 These transformers all subclass :class:`IPython.core.inputtransformer.InputTransformer`,
21 .. versionchanged:: 7.0
22 and are used by :class:`IPython.core.inputsplitter.IPythonInputSplitter`.
23
24 These transformers act in three groups, stored separately as lists of instances
25 in attributes of :class:`~IPython.core.inputsplitter.IPythonInputSplitter`:
26
27 * ``physical_line_transforms`` act on the lines as the user enters them. For
28 example, these strip Python prompts from examples pasted in.
29 * ``logical_line_transforms`` act on lines as connected by explicit line
30 continuations, i.e. ``\`` at the end of physical lines. They are skipped
31 inside multiline Python statements. This is the point where IPython recognises
32 ``%magic`` commands, for instance.
33 * ``python_line_transforms`` act on blocks containing complete Python statements.
34 Multi-line strings, lists and function calls are reassembled before being
35 passed to these, but note that function and class *definitions* are still a
36 series of separate statements. IPython does not use any of these by default.
37
38 An InteractiveShell instance actually has two
39 :class:`~IPython.core.inputsplitter.IPythonInputSplitter` instances, as the
40 attributes :attr:`~IPython.core.interactiveshell.InteractiveShell.input_splitter`,
41 to tell when a block of input is complete, and
42 :attr:`~IPython.core.interactiveshell.InteractiveShell.input_transformer_manager`,
43 to transform complete cells. If you add a transformer, you should make sure that
44 it gets added to both, e.g.::
45
46 ip.input_splitter.logical_line_transforms.append(my_transformer())
47 ip.input_transformer_manager.logical_line_transforms.append(my_transformer())
48
22
49 These transformers may raise :exc:`SyntaxError` if the input code is invalid, but
23 The API for string and token-based transformations has been completely
50 in most cases it is clearer to pass unrecognised code through unmodified and let
24 redesigned. Any third party code extending input transformation will need to
51 Python's own parser decide whether it is valid.
25 be rewritten. The new API is, hopefully, simpler.
52
53 .. versionchanged:: 2.0
54
55 Added the option to raise :exc:`SyntaxError`.
56
57 Stateless transformations
58 -------------------------
59
60 The simplest kind of transformations work one line at a time. Write a function
61 which takes a line and returns a line, and decorate it with
62 :meth:`StatelessInputTransformer.wrap`::
63
64 @StatelessInputTransformer.wrap
65 def my_special_commands(line):
66 if line.startswith("¬"):
67 return "specialcommand(" + repr(line) + ")"
68 return line
69
70 The decorator returns a factory function which will produce instances of
71 :class:`~IPython.core.inputtransformer.StatelessInputTransformer` using your
72 function.
73
74 Transforming a full block
75 -------------------------
76
77 .. warning::
78
26
79 Transforming a full block at once will break the automatic detection of
27 String based transformations are functions which accept a list of strings:
80 whether a block of code is complete in interfaces relying on this
28 each string is a single line of the input cell, including its line ending.
81 functionality, such as terminal IPython. You will need to use a
29 The transformation function should return output in the same structure.
82 shortcut to force-execute your cells.
83
30
84 Transforming a full block of python code is possible by implementing a
31 These transformations are in two groups, accessible as attributes of
85 :class:`~IPython.core.inputtransformer.Inputtransformer` and overwriting the
32 the :class:`~IPython.core.interactiveshell.InteractiveShell` instance.
86 ``push`` and ``reset`` methods. The reset method should send the full block of
33 Each group is a list of transformation functions.
87 transformed text. As an example a transformer the reversed the lines from last
88 to first.
89
34
90 from IPython.core.inputtransformer import InputTransformer
35 * ``input_transformers_cleanup`` run first on input, to do things like stripping
36 prompts and leading indents from copied code. It may not be possible at this
37 stage to parse the input as valid Python code.
38 * Then IPython runs its own transformations to handle its special syntax, like
39 ``%magics`` and ``!system`` commands. This part does not expose extension
40 points.
41 * ``input_transformers_post`` run as the last step, to do things like converting
42 float literals into decimal objects. These may attempt to parse the input as
43 Python code.
91
44
92 class ReverseLineTransformer(InputTransformer):
45 These transformers may raise :exc:`SyntaxError` if the input code is invalid, but
93
46 in most cases it is clearer to pass unrecognised code through unmodified and let
94 def __init__(self):
47 Python's own parser decide whether it is valid.
95 self.acc = []
96
97 def push(self, line):
98 self.acc.append(line)
99 return None
100
101 def reset(self):
102 ret = '\n'.join(self.acc[::-1])
103 self.acc = []
104 return ret
105
106
107 Coroutine transformers
108 ----------------------
109
110 More advanced transformers can be written as coroutines. The coroutine will be
111 sent each line in turn, followed by ``None`` to reset it. It can yield lines, or
112 ``None`` if it is accumulating text to yield at a later point. When reset, it
113 should give up any code it has accumulated.
114
115 You may use :meth:`CoroutineInputTransformer.wrap` to simplify the creation of
116 such a transformer.
117
48
118 Here is a simple :class:`CoroutineInputTransformer` that can be thought of
49 For example, imagine we want to obfuscate our code by reversing each line, so
119 being the identity::
50 we'd write ``)5(f =+ a`` instead of ``a += f(5)``. Here's how we could swap it
51 back the right way before IPython tries to run it::
120
52
121 from IPython.core.inputtransformer import CoroutineInputTransformer
53 def reverse_line_chars(lines):
54 new_lines = []
55 for line in lines:
56 chars = line[:-1] # the newline needs to stay at the end
57 new_lines.append(chars[::-1] + '\n')
58 return new_lines
122
59
123 @CoroutineInputTransformer.wrap
60 To start using this::
124 def noop():
125 line = ''
126 while True:
127 line = (yield line)
128
61
129 ip = get_ipython()
62 ip = get_ipython()
130
63 ip.input_transformers_cleanup.append(reverse_line_chars)
131 ip.input_splitter.logical_line_transforms.append(noop())
132 ip.input_transformer_manager.logical_line_transforms.append(noop())
133
134 This code in IPython strips a constant amount of leading indentation from each
135 line in a cell::
136
137 from IPython.core.inputtransformer import CoroutineInputTransformer
138
139 @CoroutineInputTransformer.wrap
140 def leading_indent():
141 """Remove leading indentation.
142
143 If the first line starts with a spaces or tabs, the same whitespace will be
144 removed from each following line until it is reset.
145 """
146 space_re = re.compile(r'^[ \t]+')
147 line = ''
148 while True:
149 line = (yield line)
150
151 if line is None:
152 continue
153
154 m = space_re.match(line)
155 if m:
156 space = m.group(0)
157 while line is not None:
158 if line.startswith(space):
159 line = line[len(space):]
160 line = (yield line)
161 else:
162 # No leading spaces - wait for reset
163 while line is not None:
164 line = (yield line)
165
166
167 Token-based transformers
168 ------------------------
169
170 There is an experimental framework that takes care of tokenizing and
171 untokenizing lines of code. Define a function that accepts a list of tokens, and
172 returns an iterable of output tokens, and decorate it with
173 :meth:`TokenInputTransformer.wrap`. These should only be used in
174 ``python_line_transforms``.
175
64
176 AST transformations
65 AST transformations
177 ===================
66 ===================
1 NO CONTENT: file was removed
NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (590 lines changed) Show them Hide them
General Comments 0
You need to be logged in to leave comments. Login now