Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/IPython/core/inputtransformer2.py: 23%

380 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-20 06:09 +0000

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. 

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 tokenize 

17from typing import List, Tuple, Optional, Any 

18import warnings 

19 

20_indent_re = re.compile(r'^[ \t]+') 

21 

22def leading_empty_lines(lines): 

23 """Remove leading empty lines 

24 

25 If the leading lines are empty or contain only whitespace, they will be 

26 removed. 

27 """ 

28 if not lines: 

29 return lines 

30 for i, line in enumerate(lines): 

31 if line and not line.isspace(): 

32 return lines[i:] 

33 return lines 

34 

35def leading_indent(lines): 

36 """Remove leading indentation. 

37 

38 If the first line starts with a spaces or tabs, the same whitespace will be 

39 removed from each following line in the cell. 

40 """ 

41 if not lines: 

42 return lines 

43 m = _indent_re.match(lines[0]) 

44 if not m: 

45 return lines 

46 space = m.group(0) 

47 n = len(space) 

48 return [l[n:] if l.startswith(space) else l 

49 for l in lines] 

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 def __init__(self, prompt_re, initial_re=None): 

74 self.prompt_re = prompt_re 

75 self.initial_re = initial_re or prompt_re 

76 

77 def _strip(self, lines): 

78 return [self.prompt_re.sub('', l, count=1) for l in lines] 

79 

80 def __call__(self, lines): 

81 if not lines: 

82 return lines 

83 if self.initial_re.match(lines[0]) or \ 

84 (len(lines) > 1 and self.prompt_re.match(lines[1])): 

85 return self._strip(lines) 

86 return lines 

87 

88classic_prompt = PromptStripper( 

89 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'), 

90 initial_re=re.compile(r'^>>>( |$)') 

91) 

92 

93ipython_prompt = PromptStripper( 

94 re.compile( 

95 r""" 

96 ^( # Match from the beginning of a line, either: 

97 

98 # 1. First-line prompt: 

99 ((\[nav\]|\[ins\])?\ )? # Vi editing mode prompt, if it's there 

100 In\ # The 'In' of the prompt, with a space 

101 \[\d+\]: # Command index, as displayed in the prompt 

102 \ # With a mandatory trailing space 

103 

104 | # ... or ... 

105 

106 # 2. The three dots of the multiline prompt 

107 \s* # All leading whitespace characters 

108 \.{3,}: # The three (or more) dots 

109 \ ? # With an optional trailing space 

110 

111 ) 

112 """, 

113 re.VERBOSE, 

114 ) 

115) 

116 

117 

118def cell_magic(lines): 

119 if not lines or not lines[0].startswith('%%'): 

120 return lines 

121 if re.match(r'%%\w+\?', lines[0]): 

122 # This case will be handled by help_end 

123 return lines 

124 magic_name, _, first_line = lines[0][2:].rstrip().partition(' ') 

125 body = ''.join(lines[1:]) 

126 return ['get_ipython().run_cell_magic(%r, %r, %r)\n' 

127 % (magic_name, first_line, body)] 

128 

129 

130def _find_assign_op(token_line) -> Optional[int]: 

131 """Get the index of the first assignment in the line ('=' not inside brackets) 

132 

133 Note: We don't try to support multiple special assignment (a = b = %foo) 

134 """ 

135 paren_level = 0 

136 for i, ti in enumerate(token_line): 

137 s = ti.string 

138 if s == '=' and paren_level == 0: 

139 return i 

140 if s in {'(','[','{'}: 

141 paren_level += 1 

142 elif s in {')', ']', '}'}: 

143 if paren_level > 0: 

144 paren_level -= 1 

145 return None 

146 

147def find_end_of_continued_line(lines, start_line: int): 

148 """Find the last line of a line explicitly extended using backslashes. 

149 

150 Uses 0-indexed line numbers. 

151 """ 

152 end_line = start_line 

153 while lines[end_line].endswith('\\\n'): 

154 end_line += 1 

155 if end_line >= len(lines): 

156 break 

157 return end_line 

158 

159def assemble_continued_line(lines, start: Tuple[int, int], end_line: int): 

160 r"""Assemble a single line from multiple continued line pieces 

161 

162 Continued lines are lines ending in ``\``, and the line following the last 

163 ``\`` in the block. 

164 

165 For example, this code continues over multiple lines:: 

166 

167 if (assign_ix is not None) \ 

168 and (len(line) >= assign_ix + 2) \ 

169 and (line[assign_ix+1].string == '%') \ 

170 and (line[assign_ix+2].type == tokenize.NAME): 

171 

172 This statement contains four continued line pieces. 

173 Assembling these pieces into a single line would give:: 

174 

175 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[... 

176 

177 This uses 0-indexed line numbers. *start* is (lineno, colno). 

178 

179 Used to allow ``%magic`` and ``!system`` commands to be continued over 

180 multiple lines. 

181 """ 

182 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1] 

183 return ' '.join([p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline 

184 + [parts[-1].rstrip()]) # Strip newline from last line 

185 

186class TokenTransformBase: 

187 """Base class for transformations which examine tokens. 

188 

189 Special syntax should not be transformed when it occurs inside strings or 

190 comments. This is hard to reliably avoid with regexes. The solution is to 

191 tokenise the code as Python, and recognise the special syntax in the tokens. 

192 

193 IPython's special syntax is not valid Python syntax, so tokenising may go 

194 wrong after the special syntax starts. These classes therefore find and 

195 transform *one* instance of special syntax at a time into regular Python 

196 syntax. After each transformation, tokens are regenerated to find the next 

197 piece of special syntax. 

198 

199 Subclasses need to implement one class method (find) 

200 and one regular method (transform). 

201 

202 The priority attribute can select which transformation to apply if multiple 

203 transformers match in the same place. Lower numbers have higher priority. 

204 This allows "%magic?" to be turned into a help call rather than a magic call. 

205 """ 

206 # Lower numbers -> higher priority (for matches in the same location) 

207 priority = 10 

208 

209 def sortby(self): 

210 return self.start_line, self.start_col, self.priority 

211 

212 def __init__(self, start): 

213 self.start_line = start[0] - 1 # Shift from 1-index to 0-index 

214 self.start_col = start[1] 

215 

216 @classmethod 

217 def find(cls, tokens_by_line): 

218 """Find one instance of special syntax in the provided tokens. 

219 

220 Tokens are grouped into logical lines for convenience, 

221 so it is easy to e.g. look at the first token of each line. 

222 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects. 

223 

224 This should return an instance of its class, pointing to the start 

225 position it has found, or None if it found no match. 

226 """ 

227 raise NotImplementedError 

228 

229 def transform(self, lines: List[str]): 

230 """Transform one instance of special syntax found by ``find()`` 

231 

232 Takes a list of strings representing physical lines, 

233 returns a similar list of transformed lines. 

234 """ 

235 raise NotImplementedError 

236 

237class MagicAssign(TokenTransformBase): 

238 """Transformer for assignments from magics (a = %foo)""" 

239 @classmethod 

240 def find(cls, tokens_by_line): 

241 """Find the first magic assignment (a = %foo) in the cell. 

242 """ 

243 for line in tokens_by_line: 

244 assign_ix = _find_assign_op(line) 

245 if (assign_ix is not None) \ 

246 and (len(line) >= assign_ix + 2) \ 

247 and (line[assign_ix+1].string == '%') \ 

248 and (line[assign_ix+2].type == tokenize.NAME): 

249 return cls(line[assign_ix+1].start) 

250 

251 def transform(self, lines: List[str]): 

252 """Transform a magic assignment found by the ``find()`` classmethod. 

253 """ 

254 start_line, start_col = self.start_line, self.start_col 

255 lhs = lines[start_line][:start_col] 

256 end_line = find_end_of_continued_line(lines, start_line) 

257 rhs = assemble_continued_line(lines, (start_line, start_col), end_line) 

258 assert rhs.startswith('%'), rhs 

259 magic_name, _, args = rhs[1:].partition(' ') 

260 

261 lines_before = lines[:start_line] 

262 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args) 

263 new_line = lhs + call + '\n' 

264 lines_after = lines[end_line+1:] 

265 

266 return lines_before + [new_line] + lines_after 

267 

268 

269class SystemAssign(TokenTransformBase): 

270 """Transformer for assignments from system commands (a = !foo)""" 

271 @classmethod 

272 def find(cls, tokens_by_line): 

273 """Find the first system assignment (a = !foo) in the cell. 

274 """ 

275 for line in tokens_by_line: 

276 assign_ix = _find_assign_op(line) 

277 if (assign_ix is not None) \ 

278 and not line[assign_ix].line.strip().startswith('=') \ 

279 and (len(line) >= assign_ix + 2) \ 

280 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN): 

281 ix = assign_ix + 1 

282 

283 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN: 

284 if line[ix].string == '!': 

285 return cls(line[ix].start) 

286 elif not line[ix].string.isspace(): 

287 break 

288 ix += 1 

289 

290 def transform(self, lines: List[str]): 

291 """Transform a system assignment found by the ``find()`` classmethod. 

292 """ 

293 start_line, start_col = self.start_line, self.start_col 

294 

295 lhs = lines[start_line][:start_col] 

296 end_line = find_end_of_continued_line(lines, start_line) 

297 rhs = assemble_continued_line(lines, (start_line, start_col), end_line) 

298 assert rhs.startswith('!'), rhs 

299 cmd = rhs[1:] 

300 

301 lines_before = lines[:start_line] 

302 call = "get_ipython().getoutput({!r})".format(cmd) 

303 new_line = lhs + call + '\n' 

304 lines_after = lines[end_line + 1:] 

305 

306 return lines_before + [new_line] + lines_after 

307 

308# The escape sequences that define the syntax transformations IPython will 

309# apply to user input. These can NOT be just changed here: many regular 

310# expressions and other parts of the code may use their hardcoded values, and 

311# for all intents and purposes they constitute the 'IPython syntax', so they 

312# should be considered fixed. 

313 

314ESC_SHELL = '!' # Send line to underlying system shell 

315ESC_SH_CAP = '!!' # Send line to system shell and capture output 

316ESC_HELP = '?' # Find information about object 

317ESC_HELP2 = '??' # Find extra-detailed information about object 

318ESC_MAGIC = '%' # Call magic function 

319ESC_MAGIC2 = '%%' # Call cell-magic function 

320ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call 

321ESC_QUOTE2 = ';' # Quote all args as a single string, call 

322ESC_PAREN = '/' # Call first argument with rest of line as arguments 

323 

324ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'} 

325ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately 

326 

327def _make_help_call(target, esc): 

328 """Prepares a pinfo(2)/psearch call from a target name and the escape 

329 (i.e. ? or ??)""" 

330 method = 'pinfo2' if esc == '??' \ 

331 else 'psearch' if '*' in target \ 

332 else 'pinfo' 

333 arg = " ".join([method, target]) 

334 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args) 

335 t_magic_name, _, t_magic_arg_s = arg.partition(' ') 

336 t_magic_name = t_magic_name.lstrip(ESC_MAGIC) 

337 return "get_ipython().run_line_magic(%r, %r)" % (t_magic_name, t_magic_arg_s) 

338 

339 

340def _tr_help(content): 

341 """Translate lines escaped with: ? 

342 

343 A naked help line should fire the intro help screen (shell.show_usage()) 

344 """ 

345 if not content: 

346 return 'get_ipython().show_usage()' 

347 

348 return _make_help_call(content, '?') 

349 

350def _tr_help2(content): 

351 """Translate lines escaped with: ?? 

352 

353 A naked help line should fire the intro help screen (shell.show_usage()) 

354 """ 

355 if not content: 

356 return 'get_ipython().show_usage()' 

357 

358 return _make_help_call(content, '??') 

359 

360def _tr_magic(content): 

361 "Translate lines escaped with a percent sign: %" 

362 name, _, args = content.partition(' ') 

363 return 'get_ipython().run_line_magic(%r, %r)' % (name, args) 

364 

365def _tr_quote(content): 

366 "Translate lines escaped with a comma: ," 

367 name, _, args = content.partition(' ') 

368 return '%s("%s")' % (name, '", "'.join(args.split()) ) 

369 

370def _tr_quote2(content): 

371 "Translate lines escaped with a semicolon: ;" 

372 name, _, args = content.partition(' ') 

373 return '%s("%s")' % (name, args) 

374 

375def _tr_paren(content): 

376 "Translate lines escaped with a slash: /" 

377 name, _, args = content.partition(' ') 

378 return '%s(%s)' % (name, ", ".join(args.split())) 

379 

380tr = { ESC_SHELL : 'get_ipython().system({!r})'.format, 

381 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format, 

382 ESC_HELP : _tr_help, 

383 ESC_HELP2 : _tr_help2, 

384 ESC_MAGIC : _tr_magic, 

385 ESC_QUOTE : _tr_quote, 

386 ESC_QUOTE2 : _tr_quote2, 

387 ESC_PAREN : _tr_paren } 

388 

389class EscapedCommand(TokenTransformBase): 

390 """Transformer for escaped commands like %foo, !foo, or /foo""" 

391 @classmethod 

392 def find(cls, tokens_by_line): 

393 """Find the first escaped command (%foo, !foo, etc.) in the cell. 

394 """ 

395 for line in tokens_by_line: 

396 if not line: 

397 continue 

398 ix = 0 

399 ll = len(line) 

400 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}: 

401 ix += 1 

402 if ix >= ll: 

403 continue 

404 if line[ix].string in ESCAPE_SINGLES: 

405 return cls(line[ix].start) 

406 

407 def transform(self, lines): 

408 """Transform an escaped line found by the ``find()`` classmethod. 

409 """ 

410 start_line, start_col = self.start_line, self.start_col 

411 

412 indent = lines[start_line][:start_col] 

413 end_line = find_end_of_continued_line(lines, start_line) 

414 line = assemble_continued_line(lines, (start_line, start_col), end_line) 

415 

416 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES: 

417 escape, content = line[:2], line[2:] 

418 else: 

419 escape, content = line[:1], line[1:] 

420 

421 if escape in tr: 

422 call = tr[escape](content) 

423 else: 

424 call = '' 

425 

426 lines_before = lines[:start_line] 

427 new_line = indent + call + '\n' 

428 lines_after = lines[end_line + 1:] 

429 

430 return lines_before + [new_line] + lines_after 

431 

432 

433_help_end_re = re.compile( 

434 r"""(%{0,2} 

435 (?!\d)[\w*]+ # Variable name 

436 (\.(?!\d)[\w*]+|\[-?[0-9]+\])* # .etc.etc or [0], we only support literal integers. 

437 ) 

438 (\?\??)$ # ? or ?? 

439 """, 

440 re.VERBOSE, 

441) 

442 

443 

444class HelpEnd(TokenTransformBase): 

445 """Transformer for help syntax: obj? and obj??""" 

446 # This needs to be higher priority (lower number) than EscapedCommand so 

447 # that inspecting magics (%foo?) works. 

448 priority = 5 

449 

450 def __init__(self, start, q_locn): 

451 super().__init__(start) 

452 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed 

453 self.q_col = q_locn[1] 

454 

455 @classmethod 

456 def find(cls, tokens_by_line): 

457 """Find the first help command (foo?) in the cell. 

458 """ 

459 for line in tokens_by_line: 

460 # Last token is NEWLINE; look at last but one 

461 if len(line) > 2 and line[-2].string == '?': 

462 # Find the first token that's not INDENT/DEDENT 

463 ix = 0 

464 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}: 

465 ix += 1 

466 return cls(line[ix].start, line[-2].start) 

467 

468 def transform(self, lines): 

469 """Transform a help command found by the ``find()`` classmethod. 

470 """ 

471 

472 piece = "".join(lines[self.start_line : self.q_line + 1]) 

473 indent, content = piece[: self.start_col], piece[self.start_col :] 

474 lines_before = lines[: self.start_line] 

475 lines_after = lines[self.q_line + 1 :] 

476 

477 m = _help_end_re.search(content) 

478 if not m: 

479 raise SyntaxError(content) 

480 assert m is not None, content 

481 target = m.group(1) 

482 esc = m.group(3) 

483 

484 

485 call = _make_help_call(target, esc) 

486 new_line = indent + call + '\n' 

487 

488 return lines_before + [new_line] + lines_after 

489 

490def make_tokens_by_line(lines:List[str]): 

491 """Tokenize a series of lines and group tokens by line. 

492 

493 The tokens for a multiline Python string or expression are grouped as one 

494 line. All lines except the last lines should keep their line ending ('\\n', 

495 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)` 

496 for example when passing block of text to this function. 

497 

498 """ 

499 # NL tokens are used inside multiline expressions, but also after blank 

500 # lines or comments. This is intentional - see https://bugs.python.org/issue17061 

501 # We want to group the former case together but split the latter, so we 

502 # track parentheses level, similar to the internals of tokenize. 

503 

504 # reexported from token on 3.7+ 

505 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL # type: ignore 

506 tokens_by_line: List[List[Any]] = [[]] 

507 if len(lines) > 1 and not lines[0].endswith(("\n", "\r", "\r\n", "\x0b", "\x0c")): 

508 warnings.warn( 

509 "`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", 

510 stacklevel=2, 

511 ) 

512 parenlev = 0 

513 try: 

514 for token in tokenize.generate_tokens(iter(lines).__next__): 

515 tokens_by_line[-1].append(token) 

516 if (token.type == NEWLINE) \ 

517 or ((token.type == NL) and (parenlev <= 0)): 

518 tokens_by_line.append([]) 

519 elif token.string in {'(', '[', '{'}: 

520 parenlev += 1 

521 elif token.string in {')', ']', '}'}: 

522 if parenlev > 0: 

523 parenlev -= 1 

524 except tokenize.TokenError: 

525 # Input ended in a multiline string or expression. That's OK for us. 

526 pass 

527 

528 

529 if not tokens_by_line[-1]: 

530 tokens_by_line.pop() 

531 

532 

533 return tokens_by_line 

534 

535 

536def has_sunken_brackets(tokens: List[tokenize.TokenInfo]): 

537 """Check if the depth of brackets in the list of tokens drops below 0""" 

538 parenlev = 0 

539 for token in tokens: 

540 if token.string in {"(", "[", "{"}: 

541 parenlev += 1 

542 elif token.string in {")", "]", "}"}: 

543 parenlev -= 1 

544 if parenlev < 0: 

545 return True 

546 return False 

547 

548 

549def show_linewise_tokens(s: str): 

550 """For investigation and debugging""" 

551 warnings.warn( 

552 "show_linewise_tokens is deprecated since IPython 8.6", 

553 DeprecationWarning, 

554 stacklevel=2, 

555 ) 

556 if not s.endswith("\n"): 

557 s += "\n" 

558 lines = s.splitlines(keepends=True) 

559 for line in make_tokens_by_line(lines): 

560 print("Line -------") 

561 for tokinfo in line: 

562 print(" ", tokinfo) 

563 

564# Arbitrary limit to prevent getting stuck in infinite loops 

565TRANSFORM_LOOP_LIMIT = 500 

566 

567class TransformerManager: 

568 """Applies various transformations to a cell or code block. 

569 

570 The key methods for external use are ``transform_cell()`` 

571 and ``check_complete()``. 

572 """ 

573 def __init__(self): 

574 self.cleanup_transforms = [ 

575 leading_empty_lines, 

576 leading_indent, 

577 classic_prompt, 

578 ipython_prompt, 

579 ] 

580 self.line_transforms = [ 

581 cell_magic, 

582 ] 

583 self.token_transformers = [ 

584 MagicAssign, 

585 SystemAssign, 

586 EscapedCommand, 

587 HelpEnd, 

588 ] 

589 

590 def do_one_token_transform(self, lines): 

591 """Find and run the transform earliest in the code. 

592 

593 Returns (changed, lines). 

594 

595 This method is called repeatedly until changed is False, indicating 

596 that all available transformations are complete. 

597 

598 The tokens following IPython special syntax might not be valid, so 

599 the transformed code is retokenised every time to identify the next 

600 piece of special syntax. Hopefully long code cells are mostly valid 

601 Python, not using lots of IPython special syntax, so this shouldn't be 

602 a performance issue. 

603 """ 

604 tokens_by_line = make_tokens_by_line(lines) 

605 candidates = [] 

606 for transformer_cls in self.token_transformers: 

607 transformer = transformer_cls.find(tokens_by_line) 

608 if transformer: 

609 candidates.append(transformer) 

610 

611 if not candidates: 

612 # Nothing to transform 

613 return False, lines 

614 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby) 

615 for transformer in ordered_transformers: 

616 try: 

617 return True, transformer.transform(lines) 

618 except SyntaxError: 

619 pass 

620 return False, lines 

621 

622 def do_token_transforms(self, lines): 

623 for _ in range(TRANSFORM_LOOP_LIMIT): 

624 changed, lines = self.do_one_token_transform(lines) 

625 if not changed: 

626 return lines 

627 

628 raise RuntimeError("Input transformation still changing after " 

629 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT) 

630 

631 def transform_cell(self, cell: str) -> str: 

632 """Transforms a cell of input code""" 

633 if not cell.endswith('\n'): 

634 cell += '\n' # Ensure the cell has a trailing newline 

635 lines = cell.splitlines(keepends=True) 

636 for transform in self.cleanup_transforms + self.line_transforms: 

637 lines = transform(lines) 

638 

639 lines = self.do_token_transforms(lines) 

640 return ''.join(lines) 

641 

642 def check_complete(self, cell: str): 

643 """Return whether a block of code is ready to execute, or should be continued 

644 

645 Parameters 

646 ---------- 

647 cell : string 

648 Python input code, which can be multiline. 

649 

650 Returns 

651 ------- 

652 status : str 

653 One of 'complete', 'incomplete', or 'invalid' if source is not a 

654 prefix of valid code. 

655 indent_spaces : int or None 

656 The number of spaces by which to indent the next line of code. If 

657 status is not 'incomplete', this is None. 

658 """ 

659 # Remember if the lines ends in a new line. 

660 ends_with_newline = False 

661 for character in reversed(cell): 

662 if character == '\n': 

663 ends_with_newline = True 

664 break 

665 elif character.strip(): 

666 break 

667 else: 

668 continue 

669 

670 if not ends_with_newline: 

671 # Append an newline for consistent tokenization 

672 # See https://bugs.python.org/issue33899 

673 cell += '\n' 

674 

675 lines = cell.splitlines(keepends=True) 

676 

677 if not lines: 

678 return 'complete', None 

679 

680 if lines[-1].endswith('\\'): 

681 # Explicit backslash continuation 

682 return 'incomplete', find_last_indent(lines) 

683 

684 try: 

685 for transform in self.cleanup_transforms: 

686 if not getattr(transform, 'has_side_effects', False): 

687 lines = transform(lines) 

688 except SyntaxError: 

689 return 'invalid', None 

690 

691 if lines[0].startswith('%%'): 

692 # Special case for cell magics - completion marked by blank line 

693 if lines[-1].strip(): 

694 return 'incomplete', find_last_indent(lines) 

695 else: 

696 return 'complete', None 

697 

698 try: 

699 for transform in self.line_transforms: 

700 if not getattr(transform, 'has_side_effects', False): 

701 lines = transform(lines) 

702 lines = self.do_token_transforms(lines) 

703 except SyntaxError: 

704 return 'invalid', None 

705 

706 tokens_by_line = make_tokens_by_line(lines) 

707 

708 # Bail if we got one line and there are more closing parentheses than 

709 # the opening ones 

710 if ( 

711 len(lines) == 1 

712 and tokens_by_line 

713 and has_sunken_brackets(tokens_by_line[0]) 

714 ): 

715 return "invalid", None 

716 

717 if not tokens_by_line: 

718 return 'incomplete', find_last_indent(lines) 

719 

720 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER: 

721 # We're in a multiline string or expression 

722 return 'incomplete', find_last_indent(lines) 

723 

724 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER} # type: ignore 

725 

726 # Pop the last line which only contains DEDENTs and ENDMARKER 

727 last_token_line = None 

728 if {t.type for t in tokens_by_line[-1]} in [ 

729 {tokenize.DEDENT, tokenize.ENDMARKER}, 

730 {tokenize.ENDMARKER} 

731 ] and len(tokens_by_line) > 1: 

732 last_token_line = tokens_by_line.pop() 

733 

734 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types: 

735 tokens_by_line[-1].pop() 

736 

737 if not tokens_by_line[-1]: 

738 return 'incomplete', find_last_indent(lines) 

739 

740 if tokens_by_line[-1][-1].string == ':': 

741 # The last line starts a block (e.g. 'if foo:') 

742 ix = 0 

743 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}: 

744 ix += 1 

745 

746 indent = tokens_by_line[-1][ix].start[1] 

747 return 'incomplete', indent + 4 

748 

749 if tokens_by_line[-1][0].line.endswith('\\'): 

750 return 'incomplete', None 

751 

752 # At this point, our checks think the code is complete (or invalid). 

753 # We'll use codeop.compile_command to check this with the real parser 

754 try: 

755 with warnings.catch_warnings(): 

756 warnings.simplefilter('error', SyntaxWarning) 

757 res = compile_command(''.join(lines), symbol='exec') 

758 except (SyntaxError, OverflowError, ValueError, TypeError, 

759 MemoryError, SyntaxWarning): 

760 return 'invalid', None 

761 else: 

762 if res is None: 

763 return 'incomplete', find_last_indent(lines) 

764 

765 if last_token_line and last_token_line[0].type == tokenize.DEDENT: 

766 if ends_with_newline: 

767 return 'complete', None 

768 return 'incomplete', find_last_indent(lines) 

769 

770 # If there's a blank line at the end, assume we're ready to execute 

771 if not lines[-1].strip(): 

772 return 'complete', None 

773 

774 return 'complete', None 

775 

776 

777def find_last_indent(lines): 

778 m = _indent_re.match(lines[-1]) 

779 if not m: 

780 return 0 

781 return len(m.group(0).replace('\t', ' '*4)) 

782 

783 

784class MaybeAsyncCompile(Compile): 

785 def __init__(self, extra_flags=0): 

786 super().__init__() 

787 self.flags |= extra_flags 

788 

789 

790class MaybeAsyncCommandCompiler(CommandCompiler): 

791 def __init__(self, extra_flags=0): 

792 self.compiler = MaybeAsyncCompile(extra_flags=extra_flags) 

793 

794 

795_extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT 

796 

797compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags)