1"""Input transformer machinery to support IPython special syntax.
2
3This includes the machinery to recognise and transform ``%magic`` commands,
4``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
5
6Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
7deprecated in 7.0 and removed in 9.0
8"""
9
10# Copyright (c) IPython Development Team.
11# Distributed under the terms of the Modified BSD License.
12
13import ast
14from codeop import CommandCompiler, Compile
15import re
16import sys
17import tokenize
18from typing import Any
19import warnings
20from textwrap import dedent
21
22from IPython.utils import tokenutil
23
24_indent_re = re.compile(r"^[ \t]+")
25
26
27def leading_empty_lines(lines):
28 """Remove leading empty lines
29
30 If the leading lines are empty or contain only whitespace, they will be
31 removed.
32 """
33 if not lines:
34 return lines
35 for i, line in enumerate(lines):
36 if line and not line.isspace():
37 return lines[i:]
38 return lines
39
40
41def leading_indent(lines):
42 """Remove leading indentation.
43
44 Removes the minimum common leading indentation from all lines.
45 """
46 if not lines:
47 return lines
48 return dedent("".join(lines)).splitlines(keepends=True)
49
50
51class PromptStripper:
52 """Remove matching input prompts from a block of input.
53
54 Parameters
55 ----------
56 prompt_re : regular expression
57 A regular expression matching any input prompt (including continuation,
58 e.g. ``...``)
59 initial_re : regular expression, optional
60 A regular expression matching only the initial prompt, but not continuation.
61 If no initial expression is given, prompt_re will be used everywhere.
62 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
63 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
64
65 Notes
66 -----
67
68 If initial_re and prompt_re differ,
69 only initial_re will be tested against the first line.
70 If any prompt is found on the first two lines,
71 prompts will be stripped from the rest of the block.
72 """
73
74 def __init__(self, prompt_re, initial_re=None, *, doctest=False):
75 self.prompt_re = prompt_re
76 self.initial_re = initial_re or prompt_re
77 self.doctest = doctest
78 if doctest:
79 # Doctest/xdoctest prompts may be indented (e.g. " >>>").
80 # We only treat "..." as a continuation prompt when the same pasted
81 # block contains at least one ">>>" line, to avoid ambiguity with the
82 # Python Ellipsis literal.
83 self._doctest_initial_re = re.compile(r"^\s*>>>")
84 self._doctest_ps1_re = re.compile(r"^\s*>>>[ \t]?")
85 self._doctest_ps2_re = re.compile(r"^\s*\.\.\.[ \t]?")
86
87 # Very small state machine to detect triple-quoted strings in the
88 # *same* input block (e.g. user typed """ then pasted doctest).
89 # We preserve literal >>> / ... inside triple-quoted strings.
90 self._triple_quote_re = re.compile(r"(?<!\\)(\"\"\"|''')")
91
92 def _triple_quote_mask(self, lines: list[str]) -> list[bool]:
93 """
94 Return a boolean mask: True if the corresponding line is considered
95 inside a triple-quoted string literal.
96
97 This is intentionally heuristic (fast + good enough for paste handling).
98 """
99 mask: list[bool] = []
100 in_triple: str | None = None # either ''' or """
101 preserve_prompt = False
102 seen_prompt = False
103 string_prefix_re = re.compile(r"(?i)^[rubf]*$")
104
105 for line in lines:
106 mask.append(in_triple is not None and preserve_prompt)
107 # Toggle state for each occurrence of """ or ''' in the line.
108 for m in self._triple_quote_re.finditer(line):
109 q = m.group(1)
110 if in_triple is None:
111 in_triple = q
112 before_quote = line[: m.start()]
113 stripped = self._doctest_ps1_re.sub("", before_quote, count=1)
114 stripped = self._doctest_ps2_re.sub("", stripped, count=1)
115 had_prompt = stripped != before_quote
116 prompted_code = had_prompt and seen_prompt
117 preserve_prompt = bool(
118 not prompted_code and string_prefix_re.match(stripped.strip())
119 )
120 mask[-1] = preserve_prompt
121 elif in_triple == q:
122 in_triple = None
123 preserve_prompt = False
124 # else: ignore mismatched triple quote while inside
125 seen_prompt = seen_prompt or bool(self._doctest_initial_re.match(line))
126 return mask
127
128 def _strip(self, lines):
129 return [self.prompt_re.sub("", l, count=1) for l in lines]
130
131 def __call__(self, lines):
132 if not lines:
133 return lines
134
135 if self.doctest:
136 triple_mask = self._triple_quote_mask(lines)
137
138 # Detect doctest prompts only outside triple-quoted strings.
139 has_doctest_outside = any(
140 (not in_triple) and self._doctest_initial_re.match(l)
141 for l, in_triple in zip(lines, triple_mask)
142 )
143 if not has_doctest_outside:
144 return lines
145
146 out_lines: list[str] = []
147 stripped_mask: list[bool] = []
148
149 for l, in_triple in zip(lines, triple_mask):
150 if in_triple:
151 out_lines.append(l)
152 stripped_mask.append(False)
153 continue
154
155 if self._doctest_ps1_re.match(l):
156 new_l = self._doctest_ps1_re.sub("", l, count=1)
157 elif self._doctest_ps2_re.match(l):
158 new_l = self._doctest_ps2_re.sub("", l, count=1)
159 else:
160 new_l = l
161 out_lines.append(new_l)
162 stripped_mask.append(new_l != l)
163
164 # Dedent only the non-triple-quoted segments where stripping occurred.
165 dedented: list[str] = []
166 i = 0
167 while i < len(out_lines):
168 j = i
169 in_triple = triple_mask[i]
170 while j < len(out_lines) and triple_mask[j] == in_triple:
171 j += 1
172
173 segment = out_lines[i:j]
174 seg_stripped = any(stripped_mask[i:j])
175
176 if (not in_triple) and seg_stripped:
177 dedented.extend(dedent("".join(segment)).splitlines(keepends=True))
178 else:
179 dedented.extend(segment)
180
181 i = j
182
183 return dedented
184
185 if self.initial_re.match(lines[0]) or (
186 len(lines) > 1 and self.prompt_re.match(lines[1])
187 ):
188 return self._strip(lines)
189 return lines
190
191
192classic_prompt = PromptStripper(
193 prompt_re=re.compile(r"^(>>>|\.\.\.)( |$)"),
194 initial_re=re.compile(r"^>>>( |$)"),
195 doctest=True,
196)
197
198ipython_prompt = PromptStripper(
199 re.compile(
200 r"""
201 ^( # Match from the beginning of a line, either:
202
203 # 1. First-line prompt:
204 ((\[nav\]|\[ins\])?\ )? # Vi editing mode prompt, if it's there
205 In\ # The 'In' of the prompt, with a space
206 \[\d+\]: # Command index, as displayed in the prompt
207 \ # With a mandatory trailing space
208
209 | # ... or ...
210
211 # 2. The three dots of the multiline prompt
212 \s* # All leading whitespace characters
213 \.{3,}: # The three (or more) dots
214 \ ? # With an optional trailing space
215
216 )
217 """,
218 re.VERBOSE,
219 )
220)
221
222
223def cell_magic(lines):
224 if not lines or not lines[0].startswith("%%"):
225 return lines
226 if re.match(r"%%\w+\?", lines[0]):
227 # This case will be handled by help_end
228 return lines
229 magic_name, _, first_line = lines[0][2:].rstrip().partition(" ")
230 body = "".join(lines[1:])
231 return [
232 "get_ipython().run_cell_magic(%r, %r, %r)\n" % (magic_name, first_line, body)
233 ]
234
235
236def _find_assign_op(token_line) -> int | None:
237 """Get the index of the first assignment in the line ('=' not inside brackets)
238
239 Note: We don't try to support multiple special assignment (a = b = %foo)
240 """
241 paren_level = 0
242 for i, ti in enumerate(token_line):
243 s = ti.string
244 if s == "=" and paren_level == 0:
245 return i
246 if s in {"(", "[", "{"}:
247 paren_level += 1
248 elif s in {")", "]", "}"}:
249 if paren_level > 0:
250 paren_level -= 1
251 return None
252
253
254def find_end_of_continued_line(lines, start_line: int):
255 """Find the last line of a line explicitly extended using backslashes.
256
257 Uses 0-indexed line numbers.
258 """
259 end_line = start_line
260 while lines[end_line].endswith("\\\n"):
261 end_line += 1
262 if end_line >= len(lines):
263 break
264 return end_line
265
266
267def assemble_continued_line(lines, start: tuple[int, int], end_line: int):
268 r"""Assemble a single line from multiple continued line pieces
269
270 Continued lines are lines ending in ``\``, and the line following the last
271 ``\`` in the block.
272
273 For example, this code continues over multiple lines::
274
275 if (assign_ix is not None) \
276 and (len(line) >= assign_ix + 2) \
277 and (line[assign_ix+1].string == '%') \
278 and (line[assign_ix+2].type == tokenize.NAME):
279
280 This statement contains four continued line pieces.
281 Assembling these pieces into a single line would give::
282
283 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
284
285 This uses 0-indexed line numbers. *start* is (lineno, colno).
286
287 Used to allow ``%magic`` and ``!system`` commands to be continued over
288 multiple lines.
289 """
290 parts = [lines[start[0]][start[1] :]] + lines[start[0] + 1 : end_line + 1]
291 return " ".join(
292 [p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline
293 + [parts[-1].rstrip()]
294 ) # Strip newline from last line
295
296
297class TokenTransformBase:
298 """Base class for transformations which examine tokens.
299
300 Special syntax should not be transformed when it occurs inside strings or
301 comments. This is hard to reliably avoid with regexes. The solution is to
302 tokenise the code as Python, and recognise the special syntax in the tokens.
303
304 IPython's special syntax is not valid Python syntax, so tokenising may go
305 wrong after the special syntax starts. These classes therefore find and
306 transform *one* instance of special syntax at a time into regular Python
307 syntax. After each transformation, tokens are regenerated to find the next
308 piece of special syntax.
309
310 Subclasses need to implement one class method (find)
311 and one regular method (transform).
312
313 The priority attribute can select which transformation to apply if multiple
314 transformers match in the same place. Lower numbers have higher priority.
315 This allows "%magic?" to be turned into a help call rather than a magic call.
316 """
317
318 # Lower numbers -> higher priority (for matches in the same location)
319 priority = 10
320
321 def sortby(self):
322 return self.start_line, self.start_col, self.priority
323
324 def __init__(self, start):
325 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
326 self.start_col = start[1]
327
328 @classmethod
329 def find(cls, tokens_by_line):
330 """Find one instance of special syntax in the provided tokens.
331
332 Tokens are grouped into logical lines for convenience,
333 so it is easy to e.g. look at the first token of each line.
334 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
335
336 This should return an instance of its class, pointing to the start
337 position it has found, or None if it found no match.
338 """
339 raise NotImplementedError
340
341 def transform(self, lines: list[str]):
342 """Transform one instance of special syntax found by ``find()``
343
344 Takes a list of strings representing physical lines,
345 returns a similar list of transformed lines.
346 """
347 raise NotImplementedError
348
349
350class MagicAssign(TokenTransformBase):
351 """Transformer for assignments from magics (a = %foo)"""
352
353 @classmethod
354 def find(cls, tokens_by_line):
355 """Find the first magic assignment (a = %foo) in the cell."""
356 for line in tokens_by_line:
357 assign_ix = _find_assign_op(line)
358 if (
359 (assign_ix is not None)
360 and (len(line) >= assign_ix + 2)
361 and (line[assign_ix + 1].string == "%")
362 and (line[assign_ix + 2].type == tokenize.NAME)
363 ):
364 return cls(line[assign_ix + 1].start)
365
366 def transform(self, lines: list[str]):
367 """Transform a magic assignment found by the ``find()`` classmethod."""
368 start_line, start_col = self.start_line, self.start_col
369 lhs = lines[start_line][:start_col]
370 end_line = find_end_of_continued_line(lines, start_line)
371 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
372 assert rhs.startswith("%"), rhs
373 magic_name, _, args = rhs[1:].partition(" ")
374
375 lines_before = lines[:start_line]
376 call = f"get_ipython().run_line_magic({magic_name!r}, {args!r})"
377 new_line = lhs + call + "\n"
378 lines_after = lines[end_line + 1 :]
379
380 return lines_before + [new_line] + lines_after
381
382
383class SystemAssign(TokenTransformBase):
384 """Transformer for assignments from system commands (a = !foo)"""
385
386 @classmethod
387 def find_pre_312(cls, tokens_by_line):
388 for line in tokens_by_line:
389 assign_ix = _find_assign_op(line)
390 if (
391 (assign_ix is not None)
392 and not line[assign_ix].line.strip().startswith("=")
393 and (len(line) >= assign_ix + 2)
394 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN)
395 ):
396 ix = assign_ix + 1
397
398 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
399 if line[ix].string == "!":
400 return cls(line[ix].start)
401 elif not line[ix].string.isspace():
402 break
403 ix += 1
404
405 @classmethod
406 def find_post_312(cls, tokens_by_line):
407 for line in tokens_by_line:
408 assign_ix = _find_assign_op(line)
409 if (
410 (assign_ix is not None)
411 and not line[assign_ix].line.strip().startswith("=")
412 and (len(line) >= assign_ix + 2)
413 and (line[assign_ix + 1].type == tokenize.OP)
414 and (line[assign_ix + 1].string == "!")
415 ):
416 return cls(line[assign_ix + 1].start)
417
418 @classmethod
419 def find(cls, tokens_by_line):
420 """Find the first system assignment (a = !foo) in the cell."""
421 if sys.version_info < (3, 12):
422 return cls.find_pre_312(tokens_by_line)
423 return cls.find_post_312(tokens_by_line)
424
425 def transform(self, lines: list[str]):
426 """Transform a system assignment found by the ``find()`` classmethod."""
427 start_line, start_col = self.start_line, self.start_col
428
429 lhs = lines[start_line][:start_col]
430 end_line = find_end_of_continued_line(lines, start_line)
431 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
432 assert rhs.startswith("!"), rhs
433 cmd = rhs[1:]
434
435 lines_before = lines[:start_line]
436 call = f"get_ipython().getoutput({cmd!r})"
437 new_line = lhs + call + "\n"
438 lines_after = lines[end_line + 1 :]
439
440 return lines_before + [new_line] + lines_after
441
442
443# The escape sequences that define the syntax transformations IPython will
444# apply to user input. These can NOT be just changed here: many regular
445# expressions and other parts of the code may use their hardcoded values, and
446# for all intents and purposes they constitute the 'IPython syntax', so they
447# should be considered fixed.
448
449ESC_SHELL = "!" # Send line to underlying system shell
450ESC_SH_CAP = "!!" # Send line to system shell and capture output
451ESC_HELP = "?" # Find information about object
452ESC_HELP2 = "??" # Find extra-detailed information about object
453ESC_MAGIC = "%" # Call magic function
454ESC_MAGIC2 = "%%" # Call cell-magic function
455ESC_QUOTE = "," # Split args on whitespace, quote each as string and call
456ESC_QUOTE2 = ";" # Quote all args as a single string, call
457ESC_PAREN = "/" # Call first argument with rest of line as arguments
458
459ESCAPE_SINGLES = {"!", "?", "%", ",", ";", "/"}
460ESCAPE_DOUBLES = {"!!", "??"} # %% (cell magic) is handled separately
461
462
463def _make_help_call(target, esc):
464 """Prepares a pinfo(2)/psearch call from a target name and the escape
465 (i.e. ? or ??)"""
466 method = "pinfo2" if esc == "??" else "psearch" if "*" in target else "pinfo"
467 arg = " ".join([method, target])
468 # Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
469 t_magic_name, _, t_magic_arg_s = arg.partition(" ")
470 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
471 return "get_ipython().run_line_magic({!r}, {!r})".format(
472 t_magic_name, t_magic_arg_s
473 )
474
475
476def _tr_help(content):
477 """Translate lines escaped with: ?
478
479 A naked help line should fire the intro help screen (shell.show_usage())
480 """
481 if not content:
482 return "get_ipython().show_usage()"
483
484 return _make_help_call(content, "?")
485
486
487def _tr_help2(content):
488 """Translate lines escaped with: ??
489
490 A naked help line should fire the intro help screen (shell.show_usage())
491 """
492 if not content:
493 return "get_ipython().show_usage()"
494
495 return _make_help_call(content, "??")
496
497
498def _tr_magic(content):
499 "Translate lines escaped with a percent sign: %"
500 name, _, args = content.partition(" ")
501 return "get_ipython().run_line_magic({!r}, {!r})".format(name, args)
502
503
504def _tr_quote(content):
505 "Translate lines escaped with a comma: ,"
506 name, _, args = content.partition(" ")
507 return '{}("{}")'.format(name, '", "'.join(args.split()))
508
509
510def _tr_quote2(content):
511 "Translate lines escaped with a semicolon: ;"
512 name, _, args = content.partition(" ")
513 return '{}("{}")'.format(name, args)
514
515
516def _tr_paren(content):
517 "Translate lines escaped with a slash: /"
518 name, _, args = content.partition(" ")
519 if name == "":
520 raise SyntaxError(f'"{ESC_SHELL}" must be followed by a callable name')
521
522 return "{}({})".format(name, ", ".join(args.split()))
523
524
525tr = {
526 ESC_SHELL: "get_ipython().system({!r})".format,
527 ESC_SH_CAP: "get_ipython().getoutput({!r})".format,
528 ESC_HELP: _tr_help,
529 ESC_HELP2: _tr_help2,
530 ESC_MAGIC: _tr_magic,
531 ESC_QUOTE: _tr_quote,
532 ESC_QUOTE2: _tr_quote2,
533 ESC_PAREN: _tr_paren,
534}
535
536
537class EscapedCommand(TokenTransformBase):
538 """Transformer for escaped commands like %foo, !foo, or /foo"""
539
540 @classmethod
541 def find(cls, tokens_by_line):
542 """Find the first escaped command (%foo, !foo, etc.) in the cell."""
543 for line in tokens_by_line:
544 if not line:
545 continue
546 ix = 0
547 ll = len(line)
548 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
549 ix += 1
550 if ix >= ll:
551 continue
552 if line[ix].string in ESCAPE_SINGLES:
553 return cls(line[ix].start)
554
555 def transform(self, lines):
556 """Transform an escaped line found by the ``find()`` classmethod."""
557 start_line, start_col = self.start_line, self.start_col
558
559 indent = lines[start_line][:start_col]
560 end_line = find_end_of_continued_line(lines, start_line)
561 line = assemble_continued_line(lines, (start_line, start_col), end_line)
562
563 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
564 escape, content = line[:2], line[2:]
565 else:
566 escape, content = line[:1], line[1:]
567
568 if escape in tr:
569 call = tr[escape](content)
570 else:
571 call = ""
572
573 lines_before = lines[:start_line]
574 new_line = indent + call + "\n"
575 lines_after = lines[end_line + 1 :]
576
577 return lines_before + [new_line] + lines_after
578
579
580_help_end_re = re.compile(
581 r"""(%{0,2}
582 (?!\d)[\w*]+ # Variable name
583 (\.(?!\d)[\w*]+|\[-?[0-9]+\])* # .etc.etc or [0], we only support literal integers.
584 )
585 (\?\??)$ # ? or ??
586 """,
587 re.VERBOSE,
588)
589
590
591class HelpEnd(TokenTransformBase):
592 """Transformer for help syntax: obj? and obj??"""
593
594 # This needs to be higher priority (lower number) than EscapedCommand so
595 # that inspecting magics (%foo?) works.
596 priority = 5
597
598 def __init__(self, start, q_locn):
599 super().__init__(start)
600 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
601 self.q_col = q_locn[1]
602
603 @classmethod
604 def find(cls, tokens_by_line):
605 """Find the first help command (foo?) in the cell."""
606 for line in tokens_by_line:
607 # Last token is NEWLINE; look at last but one
608 if len(line) > 2 and line[-2].string == "?":
609 # Find the first token that's not INDENT/DEDENT
610 ix = 0
611 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
612 ix += 1
613 return cls(line[ix].start, line[-2].start)
614
615 def transform(self, lines):
616 """Transform a help command found by the ``find()`` classmethod."""
617
618 piece = "".join(lines[self.start_line : self.q_line + 1])
619 indent, content = piece[: self.start_col], piece[self.start_col :]
620 lines_before = lines[: self.start_line]
621 lines_after = lines[self.q_line + 1 :]
622
623 m = _help_end_re.search(content)
624 if not m:
625 raise SyntaxError(content)
626 assert m is not None, content
627 target = m.group(1)
628 esc = m.group(3)
629
630 call = _make_help_call(target, esc)
631 new_line = indent + call + "\n"
632
633 return lines_before + [new_line] + lines_after
634
635
636def make_tokens_by_line(lines: list[str]):
637 """Tokenize a series of lines and group tokens by line.
638
639 The tokens for a multiline Python string or expression are grouped as one
640 line. All lines except the last lines should keep their line ending ('\\n',
641 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
642 for example when passing block of text to this function.
643
644 """
645 # NL tokens are used inside multiline expressions, but also after blank
646 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
647 # We want to group the former case together but split the latter, so we
648 # track parentheses level, similar to the internals of tokenize.
649
650 # reexported from token on 3.7+
651 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL # type: ignore
652 tokens_by_line: list[list[Any]] = [[]]
653 if len(lines) > 1 and not lines[0].endswith(("\n", "\r", "\r\n", "\x0b", "\x0c")):
654 warnings.warn(
655 "`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",
656 stacklevel=2,
657 )
658 parenlev = 0
659 try:
660 for token in tokenutil.generate_tokens_catch_errors(
661 iter(lines).__next__, extra_errors_to_catch=["expected EOF"]
662 ):
663 tokens_by_line[-1].append(token)
664 if (token.type == NEWLINE) or ((token.type == NL) and (parenlev <= 0)):
665 tokens_by_line.append([])
666 elif token.type == tokenize.OP and token.string in {"(", "[", "{"}:
667 # Only OP tokens are real brackets; e.g. on Python 3.12+
668 # FSTRING_MIDDLE tokens may contain bracket characters as
669 # literal text (refs #15436).
670 parenlev += 1
671 elif token.type == tokenize.OP and token.string in {")", "]", "}"}:
672 if parenlev > 0:
673 parenlev -= 1
674 except tokenize.TokenError:
675 # Input ended in a multiline string or expression. That's OK for us.
676 pass
677
678 if not tokens_by_line[-1]:
679 tokens_by_line.pop()
680
681 return tokens_by_line
682
683
684def has_sunken_brackets(tokens: list[tokenize.TokenInfo]):
685 """Check if the depth of brackets in the list of tokens drops below 0"""
686 parenlev = 0
687 for token in tokens:
688 # Only OP tokens are real brackets; e.g. on Python 3.12+
689 # FSTRING_MIDDLE tokens may contain bracket characters as
690 # literal text (refs #15436).
691 if token.type == tokenize.OP and token.string in {"(", "[", "{"}:
692 parenlev += 1
693 elif token.type == tokenize.OP and token.string in {")", "]", "}"}:
694 parenlev -= 1
695 if parenlev < 0:
696 return True
697 return False
698
699
700# Arbitrary limit to prevent getting stuck in infinite loops
701TRANSFORM_LOOP_LIMIT = 500
702
703
704class TransformerManager:
705 """Applies various transformations to a cell or code block.
706
707 The key methods for external use are ``transform_cell()``
708 and ``check_complete()``.
709 """
710
711 def __init__(self):
712 self.cleanup_transforms = [
713 leading_empty_lines,
714 leading_indent,
715 classic_prompt,
716 ipython_prompt,
717 ]
718 self.line_transforms = [
719 cell_magic,
720 ]
721 self.token_transformers = [
722 MagicAssign,
723 SystemAssign,
724 EscapedCommand,
725 HelpEnd,
726 ]
727
728 def do_one_token_transform(self, lines):
729 """Find and run the transform earliest in the code.
730
731 Returns (changed, lines).
732
733 This method is called repeatedly until changed is False, indicating
734 that all available transformations are complete.
735
736 The tokens following IPython special syntax might not be valid, so
737 the transformed code is retokenised every time to identify the next
738 piece of special syntax. Hopefully long code cells are mostly valid
739 Python, not using lots of IPython special syntax, so this shouldn't be
740 a performance issue.
741 """
742 tokens_by_line = make_tokens_by_line(lines)
743 candidates = []
744 for transformer_cls in self.token_transformers:
745 transformer = transformer_cls.find(tokens_by_line)
746 if transformer:
747 candidates.append(transformer)
748
749 if not candidates:
750 # Nothing to transform
751 return False, lines
752 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
753 for transformer in ordered_transformers:
754 try:
755 return True, transformer.transform(lines)
756 except SyntaxError:
757 pass
758 return False, lines
759
760 def do_token_transforms(self, lines):
761 for _ in range(TRANSFORM_LOOP_LIMIT):
762 changed, lines = self.do_one_token_transform(lines)
763 if not changed:
764 return lines
765
766 raise RuntimeError(
767 "Input transformation still changing after "
768 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT
769 )
770
771 def transform_cell(self, cell: str) -> str:
772 """Transforms a cell of input code"""
773 if not cell.endswith("\n"):
774 cell += "\n" # Ensure the cell has a trailing newline
775 lines = cell.splitlines(keepends=True)
776 for transform in self.cleanup_transforms + self.line_transforms:
777 lines = transform(lines)
778
779 lines = self.do_token_transforms(lines)
780 return "".join(lines)
781
782 def check_complete(self, cell: str):
783 """Return whether a block of code is ready to execute, or should be continued
784
785 Parameters
786 ----------
787 cell : string
788 Python input code, which can be multiline.
789
790 Returns
791 -------
792 status : str
793 One of 'complete', 'incomplete', or 'invalid' if source is not a
794 prefix of valid code.
795 indent_spaces : int or None
796 The number of spaces by which to indent the next line of code. If
797 status is not 'incomplete', this is None.
798 """
799 # Remember if the lines ends in a new line.
800 ends_with_newline = False
801 for character in reversed(cell):
802 if character == "\n":
803 ends_with_newline = True
804 break
805 elif character.strip():
806 break
807 else:
808 continue
809
810 if not ends_with_newline:
811 # Append an newline for consistent tokenization
812 # See https://bugs.python.org/issue33899
813 cell += "\n"
814
815 lines = cell.splitlines(keepends=True)
816
817 if not lines:
818 return "complete", None
819
820 for line in reversed(lines):
821 if not line.strip():
822 continue
823 elif line.strip("\n").endswith("\\"):
824 return "incomplete", find_last_indent(lines)
825 else:
826 break
827
828 try:
829 for transform in self.cleanup_transforms:
830 if not getattr(transform, "has_side_effects", False):
831 lines = transform(lines)
832 except SyntaxError:
833 return "invalid", None
834
835 if lines[0].startswith("%%"):
836 # Special case for cell magics - completion marked by blank line
837 if lines[-1].strip():
838 return "incomplete", find_last_indent(lines)
839 else:
840 return "complete", None
841
842 try:
843 for transform in self.line_transforms:
844 if not getattr(transform, "has_side_effects", False):
845 lines = transform(lines)
846 lines = self.do_token_transforms(lines)
847 except SyntaxError:
848 return "invalid", None
849
850 tokens_by_line = make_tokens_by_line(lines)
851
852 # Bail if we got one line and there are more closing parentheses than
853 # the opening ones
854 if (
855 len(lines) == 1
856 and tokens_by_line
857 and has_sunken_brackets(tokens_by_line[0])
858 ):
859 return "invalid", None
860
861 if not tokens_by_line:
862 return "incomplete", find_last_indent(lines)
863
864 if (
865 tokens_by_line[-1][-1].type != tokenize.ENDMARKER
866 and tokens_by_line[-1][-1].type != tokenize.ERRORTOKEN
867 ):
868 # We're in a multiline string or expression
869 return "incomplete", find_last_indent(lines)
870
871 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER} # type: ignore
872
873 # Pop the last line which only contains DEDENTs and ENDMARKER
874 last_token_line = None
875 if {t.type for t in tokens_by_line[-1]} in [
876 {tokenize.DEDENT, tokenize.ENDMARKER},
877 {tokenize.ENDMARKER},
878 ] and len(tokens_by_line) > 1:
879 last_token_line = tokens_by_line.pop()
880
881 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
882 tokens_by_line[-1].pop()
883
884 if not tokens_by_line[-1]:
885 return "incomplete", find_last_indent(lines)
886
887 if tokens_by_line[-1][-1].string == ":":
888 # The last line starts a block (e.g. 'if foo:')
889 ix = 0
890 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
891 ix += 1
892
893 indent = tokens_by_line[-1][ix].start[1]
894 return "incomplete", indent + 4
895
896 if tokens_by_line[-1][0].line.endswith("\\"):
897 return "incomplete", None
898
899 # At this point, our checks think the code is complete (or invalid).
900 # We'll use codeop.compile_command to check this with the real parser
901 try:
902 with warnings.catch_warnings():
903 warnings.simplefilter("error", SyntaxWarning)
904 res = compile_command("".join(lines), symbol="exec")
905 except (
906 SyntaxError,
907 OverflowError,
908 ValueError,
909 TypeError,
910 MemoryError,
911 SyntaxWarning,
912 ):
913 return "invalid", None
914 else:
915 if res is None:
916 return "incomplete", find_last_indent(lines)
917
918 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
919 if ends_with_newline:
920 return "complete", None
921 return "incomplete", find_last_indent(lines)
922
923 # If there's a blank line at the end, assume we're ready to execute
924 if not lines[-1].strip():
925 return "complete", None
926
927 return "complete", None
928
929
930def find_last_indent(lines):
931 m = _indent_re.match(lines[-1])
932 if not m:
933 return 0
934 return len(m.group(0).replace("\t", " " * 4))
935
936
937class MaybeAsyncCompile(Compile):
938 def __init__(self, extra_flags=0):
939 super().__init__()
940 self.flags |= extra_flags
941
942
943class MaybeAsyncCommandCompiler(CommandCompiler):
944 def __init__(self, extra_flags=0):
945 self.compiler = MaybeAsyncCompile(extra_flags=extra_flags)
946
947
948_extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
949
950compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags)