##// END OF EJS Templates
Merge pull request #13629 from Carreau/auto-backport-of-pr-13625-on-7.x...
Matthias Bussonnier -
r27633:4828b80e merge
parent child Browse files
Show More
@@ -1,536 +1,534 b''
1 1 """DEPRECATED: Input transformer classes to support IPython special syntax.
2 2
3 3 This module was deprecated in IPython 7.0, in favour of inputtransformer2.
4 4
5 5 This includes the machinery to recognise and transform ``%magic`` commands,
6 6 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
7 7 """
8 8 import abc
9 9 import functools
10 10 import re
11 11 import tokenize
12 12 from tokenize import generate_tokens, untokenize, TokenError
13 13 from io import StringIO
14 14
15 15 from IPython.core.splitinput import LineInfo
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Globals
19 19 #-----------------------------------------------------------------------------
20 20
21 21 # The escape sequences that define the syntax transformations IPython will
22 22 # apply to user input. These can NOT be just changed here: many regular
23 23 # expressions and other parts of the code may use their hardcoded values, and
24 24 # for all intents and purposes they constitute the 'IPython syntax', so they
25 25 # should be considered fixed.
26 26
27 27 ESC_SHELL = '!' # Send line to underlying system shell
28 28 ESC_SH_CAP = '!!' # Send line to system shell and capture output
29 29 ESC_HELP = '?' # Find information about object
30 30 ESC_HELP2 = '??' # Find extra-detailed information about object
31 31 ESC_MAGIC = '%' # Call magic function
32 32 ESC_MAGIC2 = '%%' # Call cell-magic function
33 33 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
34 34 ESC_QUOTE2 = ';' # Quote all args as a single string, call
35 35 ESC_PAREN = '/' # Call first argument with rest of line as arguments
36 36
37 37 ESC_SEQUENCES = [ESC_SHELL, ESC_SH_CAP, ESC_HELP ,\
38 38 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,\
39 39 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN ]
40 40
41 41
42 42 class InputTransformer(metaclass=abc.ABCMeta):
43 43 """Abstract base class for line-based input transformers."""
44 44
45 45 @abc.abstractmethod
46 46 def push(self, line):
47 47 """Send a line of input to the transformer, returning the transformed
48 48 input or None if the transformer is waiting for more input.
49 49
50 50 Must be overridden by subclasses.
51 51
52 52 Implementations may raise ``SyntaxError`` if the input is invalid. No
53 53 other exceptions may be raised.
54 54 """
55 55 pass
56 56
57 57 @abc.abstractmethod
58 58 def reset(self):
59 59 """Return, transformed any lines that the transformer has accumulated,
60 60 and reset its internal state.
61 61
62 62 Must be overridden by subclasses.
63 63 """
64 64 pass
65 65
66 66 @classmethod
67 67 def wrap(cls, func):
68 68 """Can be used by subclasses as a decorator, to return a factory that
69 69 will allow instantiation with the decorated object.
70 70 """
71 71 @functools.wraps(func)
72 72 def transformer_factory(**kwargs):
73 73 return cls(func, **kwargs)
74 74
75 75 return transformer_factory
76 76
77 77 class StatelessInputTransformer(InputTransformer):
78 78 """Wrapper for a stateless input transformer implemented as a function."""
79 79 def __init__(self, func):
80 80 self.func = func
81 81
82 82 def __repr__(self):
83 83 return "StatelessInputTransformer(func={0!r})".format(self.func)
84 84
85 85 def push(self, line):
86 86 """Send a line of input to the transformer, returning the
87 87 transformed input."""
88 88 return self.func(line)
89 89
90 90 def reset(self):
91 91 """No-op - exists for compatibility."""
92 92 pass
93 93
94 94 class CoroutineInputTransformer(InputTransformer):
95 95 """Wrapper for an input transformer implemented as a coroutine."""
96 96 def __init__(self, coro, **kwargs):
97 97 # Prime it
98 98 self.coro = coro(**kwargs)
99 99 next(self.coro)
100 100
101 101 def __repr__(self):
102 102 return "CoroutineInputTransformer(coro={0!r})".format(self.coro)
103 103
104 104 def push(self, line):
105 105 """Send a line of input to the transformer, returning the
106 106 transformed input or None if the transformer is waiting for more
107 107 input.
108 108 """
109 109 return self.coro.send(line)
110 110
111 111 def reset(self):
112 112 """Return, transformed any lines that the transformer has
113 113 accumulated, and reset its internal state.
114 114 """
115 115 return self.coro.send(None)
116 116
117 117 class TokenInputTransformer(InputTransformer):
118 118 """Wrapper for a token-based input transformer.
119 119
120 120 func should accept a list of tokens (5-tuples, see tokenize docs), and
121 121 return an iterable which can be passed to tokenize.untokenize().
122 122 """
123 123 def __init__(self, func):
124 124 self.func = func
125 125 self.buf = []
126 126 self.reset_tokenizer()
127 127
128 128 def reset_tokenizer(self):
129 129 it = iter(self.buf)
130 130 self.tokenizer = generate_tokens(it.__next__)
131 131
132 132 def push(self, line):
133 133 self.buf.append(line + '\n')
134 134 if all(l.isspace() for l in self.buf):
135 135 return self.reset()
136 136
137 137 tokens = []
138 138 stop_at_NL = False
139 139 try:
140 140 for intok in self.tokenizer:
141 141 tokens.append(intok)
142 142 t = intok[0]
143 143 if t == tokenize.NEWLINE or (stop_at_NL and t == tokenize.NL):
144 144 # Stop before we try to pull a line we don't have yet
145 145 break
146 146 elif t == tokenize.ERRORTOKEN:
147 147 stop_at_NL = True
148 148 except TokenError:
149 149 # Multi-line statement - stop and try again with the next line
150 150 self.reset_tokenizer()
151 151 return None
152 152
153 153 return self.output(tokens)
154 154
155 155 def output(self, tokens):
156 156 self.buf.clear()
157 157 self.reset_tokenizer()
158 158 return untokenize(self.func(tokens)).rstrip('\n')
159 159
160 160 def reset(self):
161 161 l = ''.join(self.buf)
162 162 self.buf.clear()
163 163 self.reset_tokenizer()
164 164 if l:
165 165 return l.rstrip('\n')
166 166
167 167 class assemble_python_lines(TokenInputTransformer):
168 168 def __init__(self):
169 169 super(assemble_python_lines, self).__init__(None)
170 170
171 171 def output(self, tokens):
172 172 return self.reset()
173 173
174 174 @CoroutineInputTransformer.wrap
175 175 def assemble_logical_lines():
176 176 r"""Join lines following explicit line continuations (\)"""
177 177 line = ''
178 178 while True:
179 179 line = (yield line)
180 180 if not line or line.isspace():
181 181 continue
182 182
183 183 parts = []
184 184 while line is not None:
185 185 if line.endswith('\\') and (not has_comment(line)):
186 186 parts.append(line[:-1])
187 187 line = (yield None) # Get another line
188 188 else:
189 189 parts.append(line)
190 190 break
191 191
192 192 # Output
193 193 line = ''.join(parts)
194 194
195 195 # Utilities
196 def _make_help_call(target, esc, lspace, next_input=None):
196 def _make_help_call(target, esc, lspace):
197 197 """Prepares a pinfo(2)/psearch call from a target name and the escape
198 198 (i.e. ? or ??)"""
199 199 method = 'pinfo2' if esc == '??' \
200 200 else 'psearch' if '*' in target \
201 201 else 'pinfo'
202 202 arg = " ".join([method, target])
203 203 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
204 204 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
205 205 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
206 if next_input is None:
207 return '%sget_ipython().run_line_magic(%r, %r)' % (lspace, t_magic_name, t_magic_arg_s)
208 else:
209 return '%sget_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
210 (lspace, next_input, t_magic_name, t_magic_arg_s)
211
206 return "%sget_ipython().run_line_magic(%r, %r)" % (
207 lspace,
208 t_magic_name,
209 t_magic_arg_s,
210 )
211
212
212 213 # These define the transformations for the different escape characters.
213 214 def _tr_system(line_info):
214 215 "Translate lines escaped with: !"
215 216 cmd = line_info.line.lstrip().lstrip(ESC_SHELL)
216 217 return '%sget_ipython().system(%r)' % (line_info.pre, cmd)
217 218
218 219 def _tr_system2(line_info):
219 220 "Translate lines escaped with: !!"
220 221 cmd = line_info.line.lstrip()[2:]
221 222 return '%sget_ipython().getoutput(%r)' % (line_info.pre, cmd)
222 223
223 224 def _tr_help(line_info):
224 225 "Translate lines escaped with: ?/??"
225 226 # A naked help line should just fire the intro help screen
226 227 if not line_info.line[1:]:
227 228 return 'get_ipython().show_usage()'
228 229
229 230 return _make_help_call(line_info.ifun, line_info.esc, line_info.pre)
230 231
231 232 def _tr_magic(line_info):
232 233 "Translate lines escaped with: %"
233 234 tpl = '%sget_ipython().run_line_magic(%r, %r)'
234 235 if line_info.line.startswith(ESC_MAGIC2):
235 236 return line_info.line
236 237 cmd = ' '.join([line_info.ifun, line_info.the_rest]).strip()
237 238 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
238 239 t_magic_name, _, t_magic_arg_s = cmd.partition(' ')
239 240 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
240 241 return tpl % (line_info.pre, t_magic_name, t_magic_arg_s)
241 242
242 243 def _tr_quote(line_info):
243 244 "Translate lines escaped with: ,"
244 245 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
245 246 '", "'.join(line_info.the_rest.split()) )
246 247
247 248 def _tr_quote2(line_info):
248 249 "Translate lines escaped with: ;"
249 250 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
250 251 line_info.the_rest)
251 252
252 253 def _tr_paren(line_info):
253 254 "Translate lines escaped with: /"
254 255 return '%s%s(%s)' % (line_info.pre, line_info.ifun,
255 256 ", ".join(line_info.the_rest.split()))
256 257
257 258 tr = { ESC_SHELL : _tr_system,
258 259 ESC_SH_CAP : _tr_system2,
259 260 ESC_HELP : _tr_help,
260 261 ESC_HELP2 : _tr_help,
261 262 ESC_MAGIC : _tr_magic,
262 263 ESC_QUOTE : _tr_quote,
263 264 ESC_QUOTE2 : _tr_quote2,
264 265 ESC_PAREN : _tr_paren }
265 266
266 267 @StatelessInputTransformer.wrap
267 268 def escaped_commands(line):
268 269 """Transform escaped commands - %magic, !system, ?help + various autocalls.
269 270 """
270 271 if not line or line.isspace():
271 272 return line
272 273 lineinf = LineInfo(line)
273 274 if lineinf.esc not in tr:
274 275 return line
275 276
276 277 return tr[lineinf.esc](lineinf)
277 278
278 279 _initial_space_re = re.compile(r'\s*')
279 280
280 281 _help_end_re = re.compile(r"""(%{0,2}
281 282 (?!\d)[\w*]+ # Variable name
282 283 (\.(?!\d)[\w*]+)* # .etc.etc
283 284 )
284 285 (\?\??)$ # ? or ??
285 286 """,
286 287 re.VERBOSE)
287 288
288 289 # Extra pseudotokens for multiline strings and data structures
289 290 _MULTILINE_STRING = object()
290 291 _MULTILINE_STRUCTURE = object()
291 292
292 293 def _line_tokens(line):
293 294 """Helper for has_comment and ends_in_comment_or_string."""
294 295 readline = StringIO(line).readline
295 296 toktypes = set()
296 297 try:
297 298 for t in generate_tokens(readline):
298 299 toktypes.add(t[0])
299 300 except TokenError as e:
300 301 # There are only two cases where a TokenError is raised.
301 302 if 'multi-line string' in e.args[0]:
302 303 toktypes.add(_MULTILINE_STRING)
303 304 else:
304 305 toktypes.add(_MULTILINE_STRUCTURE)
305 306 return toktypes
306 307
307 308 def has_comment(src):
308 309 """Indicate whether an input line has (i.e. ends in, or is) a comment.
309 310
310 311 This uses tokenize, so it can distinguish comments from # inside strings.
311 312
312 313 Parameters
313 314 ----------
314 315 src : string
315 316 A single line input string.
316 317
317 318 Returns
318 319 -------
319 320 comment : bool
320 321 True if source has a comment.
321 322 """
322 323 return (tokenize.COMMENT in _line_tokens(src))
323 324
324 325 def ends_in_comment_or_string(src):
325 326 """Indicates whether or not an input line ends in a comment or within
326 327 a multiline string.
327 328
328 329 Parameters
329 330 ----------
330 331 src : string
331 332 A single line input string.
332 333
333 334 Returns
334 335 -------
335 336 comment : bool
336 337 True if source ends in a comment or multiline string.
337 338 """
338 339 toktypes = _line_tokens(src)
339 340 return (tokenize.COMMENT in toktypes) or (_MULTILINE_STRING in toktypes)
340 341
341 342
342 343 @StatelessInputTransformer.wrap
343 344 def help_end(line):
344 345 """Translate lines with ?/?? at the end"""
345 346 m = _help_end_re.search(line)
346 347 if m is None or ends_in_comment_or_string(line):
347 348 return line
348 349 target = m.group(1)
349 350 esc = m.group(3)
350 351 lspace = _initial_space_re.match(line).group(0)
351 352
352 # If we're mid-command, put it back on the next prompt for the user.
353 next_input = line.rstrip('?') if line.strip() != m.group(0) else None
354
355 return _make_help_call(target, esc, lspace, next_input)
353 return _make_help_call(target, esc, lspace)
356 354
357 355
358 356 @CoroutineInputTransformer.wrap
359 357 def cellmagic(end_on_blank_line=False):
360 358 """Captures & transforms cell magics.
361 359
362 360 After a cell magic is started, this stores up any lines it gets until it is
363 361 reset (sent None).
364 362 """
365 363 tpl = 'get_ipython().run_cell_magic(%r, %r, %r)'
366 364 cellmagic_help_re = re.compile(r'%%\w+\?')
367 365 line = ''
368 366 while True:
369 367 line = (yield line)
370 368 # consume leading empty lines
371 369 while not line:
372 370 line = (yield line)
373 371
374 372 if not line.startswith(ESC_MAGIC2):
375 373 # This isn't a cell magic, idle waiting for reset then start over
376 374 while line is not None:
377 375 line = (yield line)
378 376 continue
379 377
380 378 if cellmagic_help_re.match(line):
381 379 # This case will be handled by help_end
382 380 continue
383 381
384 382 first = line
385 383 body = []
386 384 line = (yield None)
387 385 while (line is not None) and \
388 386 ((line.strip() != '') or not end_on_blank_line):
389 387 body.append(line)
390 388 line = (yield None)
391 389
392 390 # Output
393 391 magic_name, _, first = first.partition(' ')
394 392 magic_name = magic_name.lstrip(ESC_MAGIC2)
395 393 line = tpl % (magic_name, first, u'\n'.join(body))
396 394
397 395
398 396 def _strip_prompts(prompt_re, initial_re=None, turnoff_re=None):
399 397 """Remove matching input prompts from a block of input.
400 398
401 399 Parameters
402 400 ----------
403 401 prompt_re : regular expression
404 402 A regular expression matching any input prompt (including continuation)
405 403 initial_re : regular expression, optional
406 404 A regular expression matching only the initial prompt, but not continuation.
407 405 If no initial expression is given, prompt_re will be used everywhere.
408 406 Used mainly for plain Python prompts, where the continuation prompt
409 407 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
410 408
411 409 If initial_re and prompt_re differ,
412 410 only initial_re will be tested against the first line.
413 411 If any prompt is found on the first two lines,
414 412 prompts will be stripped from the rest of the block.
415 413 """
416 414 if initial_re is None:
417 415 initial_re = prompt_re
418 416 line = ''
419 417 while True:
420 418 line = (yield line)
421 419
422 420 # First line of cell
423 421 if line is None:
424 422 continue
425 423 out, n1 = initial_re.subn('', line, count=1)
426 424 if turnoff_re and not n1:
427 425 if turnoff_re.match(line):
428 426 # We're in e.g. a cell magic; disable this transformer for
429 427 # the rest of the cell.
430 428 while line is not None:
431 429 line = (yield line)
432 430 continue
433 431
434 432 line = (yield out)
435 433
436 434 if line is None:
437 435 continue
438 436 # check for any prompt on the second line of the cell,
439 437 # because people often copy from just after the first prompt,
440 438 # so we might not see it in the first line.
441 439 out, n2 = prompt_re.subn('', line, count=1)
442 440 line = (yield out)
443 441
444 442 if n1 or n2:
445 443 # Found a prompt in the first two lines - check for it in
446 444 # the rest of the cell as well.
447 445 while line is not None:
448 446 line = (yield prompt_re.sub('', line, count=1))
449 447
450 448 else:
451 449 # Prompts not in input - wait for reset
452 450 while line is not None:
453 451 line = (yield line)
454 452
455 453 @CoroutineInputTransformer.wrap
456 454 def classic_prompt():
457 455 """Strip the >>>/... prompts of the Python interactive shell."""
458 456 # FIXME: non-capturing version (?:...) usable?
459 457 prompt_re = re.compile(r'^(>>>|\.\.\.)( |$)')
460 458 initial_re = re.compile(r'^>>>( |$)')
461 459 # Any %magic/!system is IPython syntax, so we needn't look for >>> prompts
462 460 turnoff_re = re.compile(r'^[%!]')
463 461 return _strip_prompts(prompt_re, initial_re, turnoff_re)
464 462
465 463 @CoroutineInputTransformer.wrap
466 464 def ipy_prompt():
467 465 """Strip IPython's In [1]:/...: prompts."""
468 466 # FIXME: non-capturing version (?:...) usable?
469 467 prompt_re = re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)')
470 468 # Disable prompt stripping inside cell magics
471 469 turnoff_re = re.compile(r'^%%')
472 470 return _strip_prompts(prompt_re, turnoff_re=turnoff_re)
473 471
474 472
475 473 @CoroutineInputTransformer.wrap
476 474 def leading_indent():
477 475 """Remove leading indentation.
478 476
479 477 If the first line starts with a spaces or tabs, the same whitespace will be
480 478 removed from each following line until it is reset.
481 479 """
482 480 space_re = re.compile(r'^[ \t]+')
483 481 line = ''
484 482 while True:
485 483 line = (yield line)
486 484
487 485 if line is None:
488 486 continue
489 487
490 488 m = space_re.match(line)
491 489 if m:
492 490 space = m.group(0)
493 491 while line is not None:
494 492 if line.startswith(space):
495 493 line = line[len(space):]
496 494 line = (yield line)
497 495 else:
498 496 # No leading spaces - wait for reset
499 497 while line is not None:
500 498 line = (yield line)
501 499
502 500
503 501 _assign_pat = \
504 502 r'''(?P<lhs>(\s*)
505 503 ([\w\.]+) # Initial identifier
506 504 (\s*,\s*
507 505 \*?[\w\.]+)* # Further identifiers for unpacking
508 506 \s*?,? # Trailing comma
509 507 )
510 508 \s*=\s*
511 509 '''
512 510
513 511 assign_system_re = re.compile(r'{}!\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
514 512 assign_system_template = '%s = get_ipython().getoutput(%r)'
515 513 @StatelessInputTransformer.wrap
516 514 def assign_from_system(line):
517 515 """Transform assignment from system commands (e.g. files = !ls)"""
518 516 m = assign_system_re.match(line)
519 517 if m is None:
520 518 return line
521 519
522 520 return assign_system_template % m.group('lhs', 'cmd')
523 521
524 522 assign_magic_re = re.compile(r'{}%\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
525 523 assign_magic_template = '%s = get_ipython().run_line_magic(%r, %r)'
526 524 @StatelessInputTransformer.wrap
527 525 def assign_from_magic(line):
528 526 """Transform assignment from magic commands (e.g. a = %who_ls)"""
529 527 m = assign_magic_re.match(line)
530 528 if m is None:
531 529 return line
532 530 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
533 531 m_lhs, m_cmd = m.group('lhs', 'cmd')
534 532 t_magic_name, _, t_magic_arg_s = m_cmd.partition(' ')
535 533 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
536 534 return assign_magic_template % (m_lhs, t_magic_name, t_magic_arg_s)
@@ -1,752 +1,744 b''
1 1 """Input transformer machinery to support IPython special syntax.
2 2
3 3 This includes the machinery to recognise and transform ``%magic`` commands,
4 4 ``!system`` commands, ``help?`` querying, prompt stripping, and so forth.
5 5
6 6 Added: IPython 7.0. Replaces inputsplitter and inputtransformer which were
7 7 deprecated in 7.0.
8 8 """
9 9
10 10 # Copyright (c) IPython Development Team.
11 11 # Distributed under the terms of the Modified BSD License.
12 12
13 13 import ast
14 14 import sys
15 15 from codeop import CommandCompiler, Compile
16 16 import re
17 17 import tokenize
18 18 from typing import List, Tuple, Union
19 19 import warnings
20 20
21 21 _indent_re = re.compile(r'^[ \t]+')
22 22
23 23 def leading_empty_lines(lines):
24 24 """Remove leading empty lines
25 25
26 26 If the leading lines are empty or contain only whitespace, they will be
27 27 removed.
28 28 """
29 29 if not lines:
30 30 return lines
31 31 for i, line in enumerate(lines):
32 32 if line and not line.isspace():
33 33 return lines[i:]
34 34 return lines
35 35
36 36 def leading_indent(lines):
37 37 """Remove leading indentation.
38 38
39 39 If the first line starts with a spaces or tabs, the same whitespace will be
40 40 removed from each following line in the cell.
41 41 """
42 42 if not lines:
43 43 return lines
44 44 m = _indent_re.match(lines[0])
45 45 if not m:
46 46 return lines
47 47 space = m.group(0)
48 48 n = len(space)
49 49 return [l[n:] if l.startswith(space) else l
50 50 for l in lines]
51 51
52 52 class PromptStripper:
53 53 """Remove matching input prompts from a block of input.
54 54
55 55 Parameters
56 56 ----------
57 57 prompt_re : regular expression
58 58 A regular expression matching any input prompt (including continuation,
59 59 e.g. ``...``)
60 60 initial_re : regular expression, optional
61 61 A regular expression matching only the initial prompt, but not continuation.
62 62 If no initial expression is given, prompt_re will be used everywhere.
63 63 Used mainly for plain Python prompts (``>>>``), where the continuation prompt
64 64 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
65 65
66 66 Notes
67 67 -----
68 68
69 69 If initial_re and prompt_re differ,
70 70 only initial_re will be tested against the first line.
71 71 If any prompt is found on the first two lines,
72 72 prompts will be stripped from the rest of the block.
73 73 """
74 74 def __init__(self, prompt_re, initial_re=None):
75 75 self.prompt_re = prompt_re
76 76 self.initial_re = initial_re or prompt_re
77 77
78 78 def _strip(self, lines):
79 79 return [self.prompt_re.sub('', l, count=1) for l in lines]
80 80
81 81 def __call__(self, lines):
82 82 if not lines:
83 83 return lines
84 84 if self.initial_re.match(lines[0]) or \
85 85 (len(lines) > 1 and self.prompt_re.match(lines[1])):
86 86 return self._strip(lines)
87 87 return lines
88 88
89 89 classic_prompt = PromptStripper(
90 90 prompt_re=re.compile(r'^(>>>|\.\.\.)( |$)'),
91 91 initial_re=re.compile(r'^>>>( |$)')
92 92 )
93 93
94 94 ipython_prompt = PromptStripper(re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)'))
95 95
96 96 def cell_magic(lines):
97 97 if not lines or not lines[0].startswith('%%'):
98 98 return lines
99 99 if re.match(r'%%\w+\?', lines[0]):
100 100 # This case will be handled by help_end
101 101 return lines
102 102 magic_name, _, first_line = lines[0][2:].rstrip().partition(' ')
103 103 body = ''.join(lines[1:])
104 104 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
105 105 % (magic_name, first_line, body)]
106 106
107 107
108 108 def _find_assign_op(token_line) -> Union[int, None]:
109 109 """Get the index of the first assignment in the line ('=' not inside brackets)
110 110
111 111 Note: We don't try to support multiple special assignment (a = b = %foo)
112 112 """
113 113 paren_level = 0
114 114 for i, ti in enumerate(token_line):
115 115 s = ti.string
116 116 if s == '=' and paren_level == 0:
117 117 return i
118 118 if s in {'(','[','{'}:
119 119 paren_level += 1
120 120 elif s in {')', ']', '}'}:
121 121 if paren_level > 0:
122 122 paren_level -= 1
123 123
124 124 def find_end_of_continued_line(lines, start_line: int):
125 125 """Find the last line of a line explicitly extended using backslashes.
126 126
127 127 Uses 0-indexed line numbers.
128 128 """
129 129 end_line = start_line
130 130 while lines[end_line].endswith('\\\n'):
131 131 end_line += 1
132 132 if end_line >= len(lines):
133 133 break
134 134 return end_line
135 135
136 136 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
137 137 r"""Assemble a single line from multiple continued line pieces
138 138
139 139 Continued lines are lines ending in ``\``, and the line following the last
140 140 ``\`` in the block.
141 141
142 142 For example, this code continues over multiple lines::
143 143
144 144 if (assign_ix is not None) \
145 145 and (len(line) >= assign_ix + 2) \
146 146 and (line[assign_ix+1].string == '%') \
147 147 and (line[assign_ix+2].type == tokenize.NAME):
148 148
149 149 This statement contains four continued line pieces.
150 150 Assembling these pieces into a single line would give::
151 151
152 152 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
153 153
154 154 This uses 0-indexed line numbers. *start* is (lineno, colno).
155 155
156 156 Used to allow ``%magic`` and ``!system`` commands to be continued over
157 157 multiple lines.
158 158 """
159 159 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
160 160 return ' '.join([p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline
161 161 + [parts[-1].rstrip()]) # Strip newline from last line
162 162
163 163 class TokenTransformBase:
164 164 """Base class for transformations which examine tokens.
165 165
166 166 Special syntax should not be transformed when it occurs inside strings or
167 167 comments. This is hard to reliably avoid with regexes. The solution is to
168 168 tokenise the code as Python, and recognise the special syntax in the tokens.
169 169
170 170 IPython's special syntax is not valid Python syntax, so tokenising may go
171 171 wrong after the special syntax starts. These classes therefore find and
172 172 transform *one* instance of special syntax at a time into regular Python
173 173 syntax. After each transformation, tokens are regenerated to find the next
174 174 piece of special syntax.
175 175
176 176 Subclasses need to implement one class method (find)
177 177 and one regular method (transform).
178 178
179 179 The priority attribute can select which transformation to apply if multiple
180 180 transformers match in the same place. Lower numbers have higher priority.
181 181 This allows "%magic?" to be turned into a help call rather than a magic call.
182 182 """
183 183 # Lower numbers -> higher priority (for matches in the same location)
184 184 priority = 10
185 185
186 186 def sortby(self):
187 187 return self.start_line, self.start_col, self.priority
188 188
189 189 def __init__(self, start):
190 190 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
191 191 self.start_col = start[1]
192 192
193 193 @classmethod
194 194 def find(cls, tokens_by_line):
195 195 """Find one instance of special syntax in the provided tokens.
196 196
197 197 Tokens are grouped into logical lines for convenience,
198 198 so it is easy to e.g. look at the first token of each line.
199 199 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
200 200
201 201 This should return an instance of its class, pointing to the start
202 202 position it has found, or None if it found no match.
203 203 """
204 204 raise NotImplementedError
205 205
206 206 def transform(self, lines: List[str]):
207 207 """Transform one instance of special syntax found by ``find()``
208 208
209 209 Takes a list of strings representing physical lines,
210 210 returns a similar list of transformed lines.
211 211 """
212 212 raise NotImplementedError
213 213
214 214 class MagicAssign(TokenTransformBase):
215 215 """Transformer for assignments from magics (a = %foo)"""
216 216 @classmethod
217 217 def find(cls, tokens_by_line):
218 218 """Find the first magic assignment (a = %foo) in the cell.
219 219 """
220 220 for line in tokens_by_line:
221 221 assign_ix = _find_assign_op(line)
222 222 if (assign_ix is not None) \
223 223 and (len(line) >= assign_ix + 2) \
224 224 and (line[assign_ix+1].string == '%') \
225 225 and (line[assign_ix+2].type == tokenize.NAME):
226 226 return cls(line[assign_ix+1].start)
227 227
228 228 def transform(self, lines: List[str]):
229 229 """Transform a magic assignment found by the ``find()`` classmethod.
230 230 """
231 231 start_line, start_col = self.start_line, self.start_col
232 232 lhs = lines[start_line][:start_col]
233 233 end_line = find_end_of_continued_line(lines, start_line)
234 234 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
235 235 assert rhs.startswith('%'), rhs
236 236 magic_name, _, args = rhs[1:].partition(' ')
237 237
238 238 lines_before = lines[:start_line]
239 239 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
240 240 new_line = lhs + call + '\n'
241 241 lines_after = lines[end_line+1:]
242 242
243 243 return lines_before + [new_line] + lines_after
244 244
245 245
246 246 class SystemAssign(TokenTransformBase):
247 247 """Transformer for assignments from system commands (a = !foo)"""
248 248 @classmethod
249 249 def find(cls, tokens_by_line):
250 250 """Find the first system assignment (a = !foo) in the cell.
251 251 """
252 252 for line in tokens_by_line:
253 253 assign_ix = _find_assign_op(line)
254 254 if (assign_ix is not None) \
255 255 and not line[assign_ix].line.strip().startswith('=') \
256 256 and (len(line) >= assign_ix + 2) \
257 257 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
258 258 ix = assign_ix + 1
259 259
260 260 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
261 261 if line[ix].string == '!':
262 262 return cls(line[ix].start)
263 263 elif not line[ix].string.isspace():
264 264 break
265 265 ix += 1
266 266
267 267 def transform(self, lines: List[str]):
268 268 """Transform a system assignment found by the ``find()`` classmethod.
269 269 """
270 270 start_line, start_col = self.start_line, self.start_col
271 271
272 272 lhs = lines[start_line][:start_col]
273 273 end_line = find_end_of_continued_line(lines, start_line)
274 274 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
275 275 assert rhs.startswith('!'), rhs
276 276 cmd = rhs[1:]
277 277
278 278 lines_before = lines[:start_line]
279 279 call = "get_ipython().getoutput({!r})".format(cmd)
280 280 new_line = lhs + call + '\n'
281 281 lines_after = lines[end_line + 1:]
282 282
283 283 return lines_before + [new_line] + lines_after
284 284
285 285 # The escape sequences that define the syntax transformations IPython will
286 286 # apply to user input. These can NOT be just changed here: many regular
287 287 # expressions and other parts of the code may use their hardcoded values, and
288 288 # for all intents and purposes they constitute the 'IPython syntax', so they
289 289 # should be considered fixed.
290 290
291 291 ESC_SHELL = '!' # Send line to underlying system shell
292 292 ESC_SH_CAP = '!!' # Send line to system shell and capture output
293 293 ESC_HELP = '?' # Find information about object
294 294 ESC_HELP2 = '??' # Find extra-detailed information about object
295 295 ESC_MAGIC = '%' # Call magic function
296 296 ESC_MAGIC2 = '%%' # Call cell-magic function
297 297 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
298 298 ESC_QUOTE2 = ';' # Quote all args as a single string, call
299 299 ESC_PAREN = '/' # Call first argument with rest of line as arguments
300 300
301 301 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
302 302 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
303 303
304 def _make_help_call(target, esc, next_input=None):
304 def _make_help_call(target, esc):
305 305 """Prepares a pinfo(2)/psearch call from a target name and the escape
306 306 (i.e. ? or ??)"""
307 307 method = 'pinfo2' if esc == '??' \
308 308 else 'psearch' if '*' in target \
309 309 else 'pinfo'
310 310 arg = " ".join([method, target])
311 311 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
312 312 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
313 313 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
314 if next_input is None:
315 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
316 else:
317 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
318 (next_input, t_magic_name, t_magic_arg_s)
314 return "get_ipython().run_line_magic(%r, %r)" % (t_magic_name, t_magic_arg_s)
315
319 316
320 317 def _tr_help(content):
321 318 """Translate lines escaped with: ?
322 319
323 320 A naked help line should fire the intro help screen (shell.show_usage())
324 321 """
325 322 if not content:
326 323 return 'get_ipython().show_usage()'
327 324
328 325 return _make_help_call(content, '?')
329 326
330 327 def _tr_help2(content):
331 328 """Translate lines escaped with: ??
332 329
333 330 A naked help line should fire the intro help screen (shell.show_usage())
334 331 """
335 332 if not content:
336 333 return 'get_ipython().show_usage()'
337 334
338 335 return _make_help_call(content, '??')
339 336
340 337 def _tr_magic(content):
341 338 "Translate lines escaped with a percent sign: %"
342 339 name, _, args = content.partition(' ')
343 340 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
344 341
345 342 def _tr_quote(content):
346 343 "Translate lines escaped with a comma: ,"
347 344 name, _, args = content.partition(' ')
348 345 return '%s("%s")' % (name, '", "'.join(args.split()) )
349 346
350 347 def _tr_quote2(content):
351 348 "Translate lines escaped with a semicolon: ;"
352 349 name, _, args = content.partition(' ')
353 350 return '%s("%s")' % (name, args)
354 351
355 352 def _tr_paren(content):
356 353 "Translate lines escaped with a slash: /"
357 354 name, _, args = content.partition(' ')
358 355 return '%s(%s)' % (name, ", ".join(args.split()))
359 356
360 357 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
361 358 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
362 359 ESC_HELP : _tr_help,
363 360 ESC_HELP2 : _tr_help2,
364 361 ESC_MAGIC : _tr_magic,
365 362 ESC_QUOTE : _tr_quote,
366 363 ESC_QUOTE2 : _tr_quote2,
367 364 ESC_PAREN : _tr_paren }
368 365
369 366 class EscapedCommand(TokenTransformBase):
370 367 """Transformer for escaped commands like %foo, !foo, or /foo"""
371 368 @classmethod
372 369 def find(cls, tokens_by_line):
373 370 """Find the first escaped command (%foo, !foo, etc.) in the cell.
374 371 """
375 372 for line in tokens_by_line:
376 373 if not line:
377 374 continue
378 375 ix = 0
379 376 ll = len(line)
380 377 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
381 378 ix += 1
382 379 if ix >= ll:
383 380 continue
384 381 if line[ix].string in ESCAPE_SINGLES:
385 382 return cls(line[ix].start)
386 383
387 384 def transform(self, lines):
388 385 """Transform an escaped line found by the ``find()`` classmethod.
389 386 """
390 387 start_line, start_col = self.start_line, self.start_col
391 388
392 389 indent = lines[start_line][:start_col]
393 390 end_line = find_end_of_continued_line(lines, start_line)
394 391 line = assemble_continued_line(lines, (start_line, start_col), end_line)
395 392
396 393 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
397 394 escape, content = line[:2], line[2:]
398 395 else:
399 396 escape, content = line[:1], line[1:]
400 397
401 398 if escape in tr:
402 399 call = tr[escape](content)
403 400 else:
404 401 call = ''
405 402
406 403 lines_before = lines[:start_line]
407 404 new_line = indent + call + '\n'
408 405 lines_after = lines[end_line + 1:]
409 406
410 407 return lines_before + [new_line] + lines_after
411 408
412 409 _help_end_re = re.compile(r"""(%{0,2}
413 410 (?!\d)[\w*]+ # Variable name
414 411 (\.(?!\d)[\w*]+)* # .etc.etc
415 412 )
416 413 (\?\??)$ # ? or ??
417 414 """,
418 415 re.VERBOSE)
419 416
420 417 class HelpEnd(TokenTransformBase):
421 418 """Transformer for help syntax: obj? and obj??"""
422 419 # This needs to be higher priority (lower number) than EscapedCommand so
423 420 # that inspecting magics (%foo?) works.
424 421 priority = 5
425 422
426 423 def __init__(self, start, q_locn):
427 424 super().__init__(start)
428 425 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
429 426 self.q_col = q_locn[1]
430 427
431 428 @classmethod
432 429 def find(cls, tokens_by_line):
433 430 """Find the first help command (foo?) in the cell.
434 431 """
435 432 for line in tokens_by_line:
436 433 # Last token is NEWLINE; look at last but one
437 434 if len(line) > 2 and line[-2].string == '?':
438 435 # Find the first token that's not INDENT/DEDENT
439 436 ix = 0
440 437 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
441 438 ix += 1
442 439 return cls(line[ix].start, line[-2].start)
443 440
444 441 def transform(self, lines):
445 442 """Transform a help command found by the ``find()`` classmethod.
446 443 """
447 444 piece = ''.join(lines[self.start_line:self.q_line+1])
448 445 indent, content = piece[:self.start_col], piece[self.start_col:]
449 446 lines_before = lines[:self.start_line]
450 447 lines_after = lines[self.q_line + 1:]
451 448
452 449 m = _help_end_re.search(content)
453 450 if not m:
454 451 raise SyntaxError(content)
455 452 assert m is not None, content
456 453 target = m.group(1)
457 454 esc = m.group(3)
458 455
459 # If we're mid-command, put it back on the next prompt for the user.
460 next_input = None
461 if (not lines_before) and (not lines_after) \
462 and content.strip() != m.group(0):
463 next_input = content.rstrip('?\n')
464 456
465 call = _make_help_call(target, esc, next_input=next_input)
457 call = _make_help_call(target, esc)
466 458 new_line = indent + call + '\n'
467 459
468 460 return lines_before + [new_line] + lines_after
469 461
470 462 def make_tokens_by_line(lines:List[str]):
471 463 """Tokenize a series of lines and group tokens by line.
472 464
473 465 The tokens for a multiline Python string or expression are grouped as one
474 466 line. All lines except the last lines should keep their line ending ('\\n',
475 467 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
476 468 for example when passing block of text to this function.
477 469
478 470 """
479 471 # NL tokens are used inside multiline expressions, but also after blank
480 472 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
481 473 # We want to group the former case together but split the latter, so we
482 474 # track parentheses level, similar to the internals of tokenize.
483 475 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL
484 476 tokens_by_line = [[]]
485 477 if len(lines) > 1 and not lines[0].endswith(('\n', '\r', '\r\n', '\x0b', '\x0c')):
486 478 warnings.warn("`make_tokens_by_line` received a list of lines which do not have lineending markers ('\\n', '\\r', '\\r\\n', '\\x0b', '\\x0c'), behavior will be unspecified")
487 479 parenlev = 0
488 480 try:
489 481 for token in tokenize.generate_tokens(iter(lines).__next__):
490 482 tokens_by_line[-1].append(token)
491 483 if (token.type == NEWLINE) \
492 484 or ((token.type == NL) and (parenlev <= 0)):
493 485 tokens_by_line.append([])
494 486 elif token.string in {'(', '[', '{'}:
495 487 parenlev += 1
496 488 elif token.string in {')', ']', '}'}:
497 489 if parenlev > 0:
498 490 parenlev -= 1
499 491 except tokenize.TokenError:
500 492 # Input ended in a multiline string or expression. That's OK for us.
501 493 pass
502 494
503 495
504 496 if not tokens_by_line[-1]:
505 497 tokens_by_line.pop()
506 498
507 499
508 500 return tokens_by_line
509 501
510 502 def show_linewise_tokens(s: str):
511 503 """For investigation and debugging"""
512 504 if not s.endswith('\n'):
513 505 s += '\n'
514 506 lines = s.splitlines(keepends=True)
515 507 for line in make_tokens_by_line(lines):
516 508 print("Line -------")
517 509 for tokinfo in line:
518 510 print(" ", tokinfo)
519 511
520 512 # Arbitrary limit to prevent getting stuck in infinite loops
521 513 TRANSFORM_LOOP_LIMIT = 500
522 514
523 515 class TransformerManager:
524 516 """Applies various transformations to a cell or code block.
525 517
526 518 The key methods for external use are ``transform_cell()``
527 519 and ``check_complete()``.
528 520 """
529 521 def __init__(self):
530 522 self.cleanup_transforms = [
531 523 leading_empty_lines,
532 524 leading_indent,
533 525 classic_prompt,
534 526 ipython_prompt,
535 527 ]
536 528 self.line_transforms = [
537 529 cell_magic,
538 530 ]
539 531 self.token_transformers = [
540 532 MagicAssign,
541 533 SystemAssign,
542 534 EscapedCommand,
543 535 HelpEnd,
544 536 ]
545 537
546 538 def do_one_token_transform(self, lines):
547 539 """Find and run the transform earliest in the code.
548 540
549 541 Returns (changed, lines).
550 542
551 543 This method is called repeatedly until changed is False, indicating
552 544 that all available transformations are complete.
553 545
554 546 The tokens following IPython special syntax might not be valid, so
555 547 the transformed code is retokenised every time to identify the next
556 548 piece of special syntax. Hopefully long code cells are mostly valid
557 549 Python, not using lots of IPython special syntax, so this shouldn't be
558 550 a performance issue.
559 551 """
560 552 tokens_by_line = make_tokens_by_line(lines)
561 553 candidates = []
562 554 for transformer_cls in self.token_transformers:
563 555 transformer = transformer_cls.find(tokens_by_line)
564 556 if transformer:
565 557 candidates.append(transformer)
566 558
567 559 if not candidates:
568 560 # Nothing to transform
569 561 return False, lines
570 562 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
571 563 for transformer in ordered_transformers:
572 564 try:
573 565 return True, transformer.transform(lines)
574 566 except SyntaxError:
575 567 pass
576 568 return False, lines
577 569
578 570 def do_token_transforms(self, lines):
579 571 for _ in range(TRANSFORM_LOOP_LIMIT):
580 572 changed, lines = self.do_one_token_transform(lines)
581 573 if not changed:
582 574 return lines
583 575
584 576 raise RuntimeError("Input transformation still changing after "
585 577 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
586 578
587 579 def transform_cell(self, cell: str) -> str:
588 580 """Transforms a cell of input code"""
589 581 if not cell.endswith('\n'):
590 582 cell += '\n' # Ensure the cell has a trailing newline
591 583 lines = cell.splitlines(keepends=True)
592 584 for transform in self.cleanup_transforms + self.line_transforms:
593 585 lines = transform(lines)
594 586
595 587 lines = self.do_token_transforms(lines)
596 588 return ''.join(lines)
597 589
598 590 def check_complete(self, cell: str):
599 591 """Return whether a block of code is ready to execute, or should be continued
600 592
601 593 Parameters
602 594 ----------
603 595 source : string
604 596 Python input code, which can be multiline.
605 597
606 598 Returns
607 599 -------
608 600 status : str
609 601 One of 'complete', 'incomplete', or 'invalid' if source is not a
610 602 prefix of valid code.
611 603 indent_spaces : int or None
612 604 The number of spaces by which to indent the next line of code. If
613 605 status is not 'incomplete', this is None.
614 606 """
615 607 # Remember if the lines ends in a new line.
616 608 ends_with_newline = False
617 609 for character in reversed(cell):
618 610 if character == '\n':
619 611 ends_with_newline = True
620 612 break
621 613 elif character.strip():
622 614 break
623 615 else:
624 616 continue
625 617
626 618 if not ends_with_newline:
627 619 # Append an newline for consistent tokenization
628 620 # See https://bugs.python.org/issue33899
629 621 cell += '\n'
630 622
631 623 lines = cell.splitlines(keepends=True)
632 624
633 625 if not lines:
634 626 return 'complete', None
635 627
636 628 if lines[-1].endswith('\\'):
637 629 # Explicit backslash continuation
638 630 return 'incomplete', find_last_indent(lines)
639 631
640 632 try:
641 633 for transform in self.cleanup_transforms:
642 634 if not getattr(transform, 'has_side_effects', False):
643 635 lines = transform(lines)
644 636 except SyntaxError:
645 637 return 'invalid', None
646 638
647 639 if lines[0].startswith('%%'):
648 640 # Special case for cell magics - completion marked by blank line
649 641 if lines[-1].strip():
650 642 return 'incomplete', find_last_indent(lines)
651 643 else:
652 644 return 'complete', None
653 645
654 646 try:
655 647 for transform in self.line_transforms:
656 648 if not getattr(transform, 'has_side_effects', False):
657 649 lines = transform(lines)
658 650 lines = self.do_token_transforms(lines)
659 651 except SyntaxError:
660 652 return 'invalid', None
661 653
662 654 tokens_by_line = make_tokens_by_line(lines)
663 655
664 656 if not tokens_by_line:
665 657 return 'incomplete', find_last_indent(lines)
666 658
667 659 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
668 660 # We're in a multiline string or expression
669 661 return 'incomplete', find_last_indent(lines)
670 662
671 663 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER}
672 664
673 665 # Pop the last line which only contains DEDENTs and ENDMARKER
674 666 last_token_line = None
675 667 if {t.type for t in tokens_by_line[-1]} in [
676 668 {tokenize.DEDENT, tokenize.ENDMARKER},
677 669 {tokenize.ENDMARKER}
678 670 ] and len(tokens_by_line) > 1:
679 671 last_token_line = tokens_by_line.pop()
680 672
681 673 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
682 674 tokens_by_line[-1].pop()
683 675
684 676 if not tokens_by_line[-1]:
685 677 return 'incomplete', find_last_indent(lines)
686 678
687 679 if tokens_by_line[-1][-1].string == ':':
688 680 # The last line starts a block (e.g. 'if foo:')
689 681 ix = 0
690 682 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
691 683 ix += 1
692 684
693 685 indent = tokens_by_line[-1][ix].start[1]
694 686 return 'incomplete', indent + 4
695 687
696 688 if tokens_by_line[-1][0].line.endswith('\\'):
697 689 return 'incomplete', None
698 690
699 691 # At this point, our checks think the code is complete (or invalid).
700 692 # We'll use codeop.compile_command to check this with the real parser
701 693 try:
702 694 with warnings.catch_warnings():
703 695 warnings.simplefilter('error', SyntaxWarning)
704 696 res = compile_command(''.join(lines), symbol='exec')
705 697 except (SyntaxError, OverflowError, ValueError, TypeError,
706 698 MemoryError, SyntaxWarning):
707 699 return 'invalid', None
708 700 else:
709 701 if res is None:
710 702 return 'incomplete', find_last_indent(lines)
711 703
712 704 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
713 705 if ends_with_newline:
714 706 return 'complete', None
715 707 return 'incomplete', find_last_indent(lines)
716 708
717 709 # If there's a blank line at the end, assume we're ready to execute
718 710 if not lines[-1].strip():
719 711 return 'complete', None
720 712
721 713 return 'complete', None
722 714
723 715
724 716 def find_last_indent(lines):
725 717 m = _indent_re.match(lines[-1])
726 718 if not m:
727 719 return 0
728 720 return len(m.group(0).replace('\t', ' '*4))
729 721
730 722
731 723 class MaybeAsyncCompile(Compile):
732 724 def __init__(self, extra_flags=0):
733 725 super().__init__()
734 726 self.flags |= extra_flags
735 727
736 728
737 729 if sys.version_info < (3,8):
738 730 def __call__(self, *args, **kwds):
739 731 return compile(*args, **kwds)
740 732
741 733
742 734 class MaybeAsyncCommandCompiler(CommandCompiler):
743 735 def __init__(self, extra_flags=0):
744 736 self.compiler = MaybeAsyncCompile(extra_flags=extra_flags)
745 737
746 738
747 739 if (sys.version_info.major, sys.version_info.minor) >= (3, 8):
748 740 _extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
749 741 else:
750 742 _extra_flags = ast.PyCF_ONLY_AST
751 743
752 744 compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags)
@@ -1,495 +1,499 b''
1 1 import tokenize
2 2 import nose.tools as nt
3 3
4 4 from IPython.testing import tools as tt
5 5 from IPython.utils import py3compat
6 6 u_fmt = py3compat.u_format
7 7
8 8 from IPython.core import inputtransformer as ipt
9 9
10 10 def transform_and_reset(transformer):
11 11 transformer = transformer()
12 12 def transform(inp):
13 13 try:
14 14 return transformer.push(inp)
15 15 finally:
16 16 transformer.reset()
17 17
18 18 return transform
19 19
20 20 # Transformer tests
21 21 def transform_checker(tests, transformer, **kwargs):
22 22 """Utility to loop over test inputs"""
23 23 transformer = transformer(**kwargs)
24 24 try:
25 25 for inp, tr in tests:
26 26 if inp is None:
27 27 out = transformer.reset()
28 28 else:
29 29 out = transformer.push(inp)
30 30 nt.assert_equal(out, tr)
31 31 finally:
32 32 transformer.reset()
33 33
34 34 # Data for all the syntax tests in the form of lists of pairs of
35 35 # raw/transformed input. We store it here as a global dict so that we can use
36 36 # it both within single-function tests and also to validate the behavior of the
37 37 # larger objects
38 38
39 syntax = \
40 dict(assign_system =
41 [(i,py3compat.u_format(o)) for i,o in \
42 [(u'a =! ls', "a = get_ipython().getoutput('ls')"),
43 (u'b = !ls', "b = get_ipython().getoutput('ls')"),
44 (u'c= !ls', "c = get_ipython().getoutput('ls')"),
45 (u'd == !ls', u'd == !ls'), # Invalid syntax, but we leave == alone.
46 ('x=1', 'x=1'), # normal input is unmodified
47 (' ',' '), # blank lines are kept intact
48 # Tuple unpacking
49 (u"a, b = !echo 'a\\nb'", u"a, b = get_ipython().getoutput(\"echo 'a\\\\nb'\")"),
50 (u"a,= !echo 'a'", u"a, = get_ipython().getoutput(\"echo 'a'\")"),
51 (u"a, *bc = !echo 'a\\nb\\nc'", u"a, *bc = get_ipython().getoutput(\"echo 'a\\\\nb\\\\nc'\")"),
52 # Tuple unpacking with regular Python expressions, not our syntax.
53 (u"a, b = range(2)", u"a, b = range(2)"),
54 (u"a, = range(1)", u"a, = range(1)"),
55 (u"a, *bc = range(3)", u"a, *bc = range(3)"),
56 ]],
57
58 assign_magic =
59 [(i,py3compat.u_format(o)) for i,o in \
60 [(u'a =% who', "a = get_ipython().run_line_magic('who', '')"),
61 (u'b = %who', "b = get_ipython().run_line_magic('who', '')"),
62 (u'c= %ls', "c = get_ipython().run_line_magic('ls', '')"),
63 (u'd == %ls', u'd == %ls'), # Invalid syntax, but we leave == alone.
64 ('x=1', 'x=1'), # normal input is unmodified
65 (' ',' '), # blank lines are kept intact
66 (u"a, b = %foo", u"a, b = get_ipython().run_line_magic('foo', '')"),
67 ]],
68
69 classic_prompt =
70 [('>>> x=1', 'x=1'),
71 ('x=1', 'x=1'), # normal input is unmodified
72 (' ', ' '), # blank lines are kept intact
73 ],
74
75 ipy_prompt =
76 [('In [1]: x=1', 'x=1'),
77 ('x=1', 'x=1'), # normal input is unmodified
78 (' ',' '), # blank lines are kept intact
79 ],
80
81 # Tests for the escape transformer to leave normal code alone
82 escaped_noesc =
83 [ (' ', ' '),
84 ('x=1', 'x=1'),
85 ],
86
87 # System calls
88 escaped_shell =
89 [(i,py3compat.u_format(o)) for i,o in \
90 [ (u'!ls', "get_ipython().system('ls')"),
91 # Double-escape shell, this means to capture the output of the
92 # subprocess and return it
93 (u'!!ls', "get_ipython().getoutput('ls')"),
94 ]],
95
96 # Help/object info
97 escaped_help =
98 [(i,py3compat.u_format(o)) for i,o in \
99 [ (u'?', 'get_ipython().show_usage()'),
100 (u'?x1', "get_ipython().run_line_magic('pinfo', 'x1')"),
101 (u'??x2', "get_ipython().run_line_magic('pinfo2', 'x2')"),
102 (u'?a.*s', "get_ipython().run_line_magic('psearch', 'a.*s')"),
103 (u'?%hist1', "get_ipython().run_line_magic('pinfo', '%hist1')"),
104 (u'?%%hist2', "get_ipython().run_line_magic('pinfo', '%%hist2')"),
105 (u'?abc = qwe', "get_ipython().run_line_magic('pinfo', 'abc')"),
106 ]],
107
108 end_help =
109 [(i,py3compat.u_format(o)) for i,o in \
110 [ (u'x3?', "get_ipython().run_line_magic('pinfo', 'x3')"),
111 (u'x4??', "get_ipython().run_line_magic('pinfo2', 'x4')"),
112 (u'%hist1?', "get_ipython().run_line_magic('pinfo', '%hist1')"),
113 (u'%hist2??', "get_ipython().run_line_magic('pinfo2', '%hist2')"),
114 (u'%%hist3?', "get_ipython().run_line_magic('pinfo', '%%hist3')"),
115 (u'%%hist4??', "get_ipython().run_line_magic('pinfo2', '%%hist4')"),
116 (u'Ο€.foo?', "get_ipython().run_line_magic('pinfo', 'Ο€.foo')"),
117 (u'f*?', "get_ipython().run_line_magic('psearch', 'f*')"),
118 (u'ax.*aspe*?', "get_ipython().run_line_magic('psearch', 'ax.*aspe*')"),
119 (u'a = abc?', "get_ipython().set_next_input('a = abc');"
120 "get_ipython().run_line_magic('pinfo', 'abc')"),
121 (u'a = abc.qe??', "get_ipython().set_next_input('a = abc.qe');"
122 "get_ipython().run_line_magic('pinfo2', 'abc.qe')"),
123 (u'a = *.items?', "get_ipython().set_next_input('a = *.items');"
124 "get_ipython().run_line_magic('psearch', '*.items')"),
125 (u'plot(a?', "get_ipython().set_next_input('plot(a');"
126 "get_ipython().run_line_magic('pinfo', 'a')"),
127 (u'a*2 #comment?', 'a*2 #comment?'),
128 ]],
129
130 # Explicit magic calls
131 escaped_magic =
132 [(i,py3compat.u_format(o)) for i,o in \
133 [ (u'%cd', "get_ipython().run_line_magic('cd', '')"),
134 (u'%cd /home', "get_ipython().run_line_magic('cd', '/home')"),
135 # Backslashes need to be escaped.
136 (u'%cd C:\\User', "get_ipython().run_line_magic('cd', 'C:\\\\User')"),
137 (u' %magic', " get_ipython().run_line_magic('magic', '')"),
138 ]],
139
140 # Quoting with separate arguments
141 escaped_quote =
142 [ (',f', 'f("")'),
143 (',f x', 'f("x")'),
144 (' ,f y', ' f("y")'),
145 (',f a b', 'f("a", "b")'),
146 ],
147
148 # Quoting with single argument
149 escaped_quote2 =
150 [ (';f', 'f("")'),
151 (';f x', 'f("x")'),
152 (' ;f y', ' f("y")'),
153 (';f a b', 'f("a b")'),
154 ],
155
156 # Simply apply parens
157 escaped_paren =
158 [ ('/f', 'f()'),
159 ('/f x', 'f(x)'),
160 (' /f y', ' f(y)'),
161 ('/f a b', 'f(a, b)'),
162 ],
163
164 # Check that we transform prompts before other transforms
165 mixed =
166 [(i,py3compat.u_format(o)) for i,o in \
167 [ (u'In [1]: %lsmagic', "get_ipython().run_line_magic('lsmagic', '')"),
168 (u'>>> %lsmagic', "get_ipython().run_line_magic('lsmagic', '')"),
169 (u'In [2]: !ls', "get_ipython().system('ls')"),
170 (u'In [3]: abs?', "get_ipython().run_line_magic('pinfo', 'abs')"),
171 (u'In [4]: b = %who', "b = get_ipython().run_line_magic('who', '')"),
172 ]],
173 )
39 syntax = dict(
40 assign_system=[
41 (i, py3compat.u_format(o))
42 for i, o in [
43 (u"a =! ls", "a = get_ipython().getoutput('ls')"),
44 (u"b = !ls", "b = get_ipython().getoutput('ls')"),
45 (u"c= !ls", "c = get_ipython().getoutput('ls')"),
46 (u"d == !ls", u"d == !ls"), # Invalid syntax, but we leave == alone.
47 ("x=1", "x=1"), # normal input is unmodified
48 (" ", " "), # blank lines are kept intact
49 # Tuple unpacking
50 (
51 u"a, b = !echo 'a\\nb'",
52 u"a, b = get_ipython().getoutput(\"echo 'a\\\\nb'\")",
53 ),
54 (u"a,= !echo 'a'", u"a, = get_ipython().getoutput(\"echo 'a'\")"),
55 (
56 u"a, *bc = !echo 'a\\nb\\nc'",
57 u"a, *bc = get_ipython().getoutput(\"echo 'a\\\\nb\\\\nc'\")",
58 ),
59 # Tuple unpacking with regular Python expressions, not our syntax.
60 (u"a, b = range(2)", u"a, b = range(2)"),
61 (u"a, = range(1)", u"a, = range(1)"),
62 (u"a, *bc = range(3)", u"a, *bc = range(3)"),
63 ]
64 ],
65 assign_magic=[
66 (i, py3compat.u_format(o))
67 for i, o in [
68 (u"a =% who", "a = get_ipython().run_line_magic('who', '')"),
69 (u"b = %who", "b = get_ipython().run_line_magic('who', '')"),
70 (u"c= %ls", "c = get_ipython().run_line_magic('ls', '')"),
71 (u"d == %ls", u"d == %ls"), # Invalid syntax, but we leave == alone.
72 ("x=1", "x=1"), # normal input is unmodified
73 (" ", " "), # blank lines are kept intact
74 (u"a, b = %foo", u"a, b = get_ipython().run_line_magic('foo', '')"),
75 ]
76 ],
77 classic_prompt=[
78 (">>> x=1", "x=1"),
79 ("x=1", "x=1"), # normal input is unmodified
80 (" ", " "), # blank lines are kept intact
81 ],
82 ipy_prompt=[
83 ("In [1]: x=1", "x=1"),
84 ("x=1", "x=1"), # normal input is unmodified
85 (" ", " "), # blank lines are kept intact
86 ],
87 # Tests for the escape transformer to leave normal code alone
88 escaped_noesc=[
89 (" ", " "),
90 ("x=1", "x=1"),
91 ],
92 # System calls
93 escaped_shell=[
94 (i, py3compat.u_format(o))
95 for i, o in [
96 (u"!ls", "get_ipython().system('ls')"),
97 # Double-escape shell, this means to capture the output of the
98 # subprocess and return it
99 (u"!!ls", "get_ipython().getoutput('ls')"),
100 ]
101 ],
102 # Help/object info
103 escaped_help=[
104 (i, py3compat.u_format(o))
105 for i, o in [
106 (u"?", "get_ipython().show_usage()"),
107 (u"?x1", "get_ipython().run_line_magic('pinfo', 'x1')"),
108 (u"??x2", "get_ipython().run_line_magic('pinfo2', 'x2')"),
109 (u"?a.*s", "get_ipython().run_line_magic('psearch', 'a.*s')"),
110 (u"?%hist1", "get_ipython().run_line_magic('pinfo', '%hist1')"),
111 (u"?%%hist2", "get_ipython().run_line_magic('pinfo', '%%hist2')"),
112 (u"?abc = qwe", "get_ipython().run_line_magic('pinfo', 'abc')"),
113 ]
114 ],
115 end_help=[
116 (i, py3compat.u_format(o))
117 for i, o in [
118 (u"x3?", "get_ipython().run_line_magic('pinfo', 'x3')"),
119 (u"x4??", "get_ipython().run_line_magic('pinfo2', 'x4')"),
120 (u"%hist1?", "get_ipython().run_line_magic('pinfo', '%hist1')"),
121 (u"%hist2??", "get_ipython().run_line_magic('pinfo2', '%hist2')"),
122 (u"%%hist3?", "get_ipython().run_line_magic('pinfo', '%%hist3')"),
123 (u"%%hist4??", "get_ipython().run_line_magic('pinfo2', '%%hist4')"),
124 (u"Ο€.foo?", "get_ipython().run_line_magic('pinfo', 'Ο€.foo')"),
125 (u"f*?", "get_ipython().run_line_magic('psearch', 'f*')"),
126 (u"ax.*aspe*?", "get_ipython().run_line_magic('psearch', 'ax.*aspe*')"),
127 (u"a = abc?", "get_ipython().run_line_magic('pinfo', 'abc')"),
128 (u"a = abc.qe??", "get_ipython().run_line_magic('pinfo2', 'abc.qe')"),
129 (u"a = *.items?", "get_ipython().run_line_magic('psearch', '*.items')"),
130 (u"plot(a?", "get_ipython().run_line_magic('pinfo', 'a')"),
131 (u"a*2 #comment?", "a*2 #comment?"),
132 ]
133 ],
134 # Explicit magic calls
135 escaped_magic=[
136 (i, py3compat.u_format(o))
137 for i, o in [
138 (u"%cd", "get_ipython().run_line_magic('cd', '')"),
139 (u"%cd /home", "get_ipython().run_line_magic('cd', '/home')"),
140 # Backslashes need to be escaped.
141 (u"%cd C:\\User", "get_ipython().run_line_magic('cd', 'C:\\\\User')"),
142 (u" %magic", " get_ipython().run_line_magic('magic', '')"),
143 ]
144 ],
145 # Quoting with separate arguments
146 escaped_quote=[
147 (",f", 'f("")'),
148 (",f x", 'f("x")'),
149 (" ,f y", ' f("y")'),
150 (",f a b", 'f("a", "b")'),
151 ],
152 # Quoting with single argument
153 escaped_quote2=[
154 (";f", 'f("")'),
155 (";f x", 'f("x")'),
156 (" ;f y", ' f("y")'),
157 (";f a b", 'f("a b")'),
158 ],
159 # Simply apply parens
160 escaped_paren=[
161 ("/f", "f()"),
162 ("/f x", "f(x)"),
163 (" /f y", " f(y)"),
164 ("/f a b", "f(a, b)"),
165 ],
166 # Check that we transform prompts before other transforms
167 mixed=[
168 (i, py3compat.u_format(o))
169 for i, o in [
170 (u"In [1]: %lsmagic", "get_ipython().run_line_magic('lsmagic', '')"),
171 (u">>> %lsmagic", "get_ipython().run_line_magic('lsmagic', '')"),
172 (u"In [2]: !ls", "get_ipython().system('ls')"),
173 (u"In [3]: abs?", "get_ipython().run_line_magic('pinfo', 'abs')"),
174 (u"In [4]: b = %who", "b = get_ipython().run_line_magic('who', '')"),
175 ]
176 ],
177 )
174 178
175 179 # multiline syntax examples. Each of these should be a list of lists, with
176 180 # each entry itself having pairs of raw/transformed input. The union (with
177 181 # '\n'.join() of the transformed inputs is what the splitter should produce
178 182 # when fed the raw lines one at a time via push.
179 183 syntax_ml = \
180 184 dict(classic_prompt =
181 185 [ [('>>> for i in range(10):','for i in range(10):'),
182 186 ('... print i',' print i'),
183 187 ('... ', ''),
184 188 ],
185 189 [('>>> a="""','a="""'),
186 190 ('... 123"""','123"""'),
187 191 ],
188 192 [('a="""','a="""'),
189 193 ('... 123','123'),
190 194 ('... 456"""','456"""'),
191 195 ],
192 196 [('a="""','a="""'),
193 197 ('>>> 123','123'),
194 198 ('... 456"""','456"""'),
195 199 ],
196 200 [('a="""','a="""'),
197 201 ('123','123'),
198 202 ('... 456"""','... 456"""'),
199 203 ],
200 204 [('....__class__','....__class__'),
201 205 ],
202 206 [('a=5', 'a=5'),
203 207 ('...', ''),
204 208 ],
205 209 [('>>> def f(x):', 'def f(x):'),
206 210 ('...', ''),
207 211 ('... return x', ' return x'),
208 212 ],
209 213 [('board = """....', 'board = """....'),
210 214 ('....', '....'),
211 215 ('...."""', '...."""'),
212 216 ],
213 217 ],
214 218
215 219 ipy_prompt =
216 220 [ [('In [24]: for i in range(10):','for i in range(10):'),
217 221 (' ....: print i',' print i'),
218 222 (' ....: ', ''),
219 223 ],
220 224 [('In [24]: for i in range(10):','for i in range(10):'),
221 225 # Qt console prompts expand with spaces, not dots
222 226 (' ...: print i',' print i'),
223 227 (' ...: ', ''),
224 228 ],
225 229 [('In [24]: for i in range(10):','for i in range(10):'),
226 230 # Sometimes whitespace preceding '...' has been removed
227 231 ('...: print i',' print i'),
228 232 ('...: ', ''),
229 233 ],
230 234 [('In [24]: for i in range(10):','for i in range(10):'),
231 235 # Space after last continuation prompt has been removed (issue #6674)
232 236 ('...: print i',' print i'),
233 237 ('...:', ''),
234 238 ],
235 239 [('In [2]: a="""','a="""'),
236 240 (' ...: 123"""','123"""'),
237 241 ],
238 242 [('a="""','a="""'),
239 243 (' ...: 123','123'),
240 244 (' ...: 456"""','456"""'),
241 245 ],
242 246 [('a="""','a="""'),
243 247 ('In [1]: 123','123'),
244 248 (' ...: 456"""','456"""'),
245 249 ],
246 250 [('a="""','a="""'),
247 251 ('123','123'),
248 252 (' ...: 456"""',' ...: 456"""'),
249 253 ],
250 254 ],
251 255
252 256 multiline_datastructure_prompt =
253 257 [ [('>>> a = [1,','a = [1,'),
254 258 ('... 2]','2]'),
255 259 ],
256 260 ],
257 261
258 262 multiline_datastructure =
259 263 [ [('b = ("%s"', None),
260 264 ('# comment', None),
261 265 ('%foo )', 'b = ("%s"\n# comment\n%foo )'),
262 266 ],
263 267 ],
264 268
265 269 multiline_string =
266 270 [ [("'''foo?", None),
267 271 ("bar'''", "'''foo?\nbar'''"),
268 272 ],
269 273 ],
270 274
271 275 leading_indent =
272 276 [ [(' print "hi"','print "hi"'),
273 277 ],
274 278 [(' for a in range(5):','for a in range(5):'),
275 279 (' a*2',' a*2'),
276 280 ],
277 281 [(' a="""','a="""'),
278 282 (' 123"""','123"""'),
279 283 ],
280 284 [('a="""','a="""'),
281 285 (' 123"""',' 123"""'),
282 286 ],
283 287 ],
284 288
285 289 cellmagic =
286 290 [ [(u'%%foo a', None),
287 291 (None, u_fmt("get_ipython().run_cell_magic('foo', 'a', '')")),
288 292 ],
289 293 [(u'%%bar 123', None),
290 294 (u'hello', None),
291 295 (None , u_fmt("get_ipython().run_cell_magic('bar', '123', 'hello')")),
292 296 ],
293 297 [(u'a=5', 'a=5'),
294 298 (u'%%cellmagic', '%%cellmagic'),
295 299 ],
296 300 ],
297 301
298 302 escaped =
299 303 [ [('%abc def \\', None),
300 304 ('ghi', u_fmt("get_ipython().run_line_magic('abc', 'def ghi')")),
301 305 ],
302 306 [('%abc def \\', None),
303 307 ('ghi\\', None),
304 308 (None, u_fmt("get_ipython().run_line_magic('abc', 'def ghi')")),
305 309 ],
306 310 ],
307 311
308 312 assign_magic =
309 313 [ [(u'a = %bc de \\', None),
310 314 (u'fg', u_fmt("a = get_ipython().run_line_magic('bc', 'de fg')")),
311 315 ],
312 316 [(u'a = %bc de \\', None),
313 317 (u'fg\\', None),
314 318 (None, u_fmt("a = get_ipython().run_line_magic('bc', 'de fg')")),
315 319 ],
316 320 ],
317 321
318 322 assign_system =
319 323 [ [(u'a = !bc de \\', None),
320 324 (u'fg', u_fmt("a = get_ipython().getoutput('bc de fg')")),
321 325 ],
322 326 [(u'a = !bc de \\', None),
323 327 (u'fg\\', None),
324 328 (None, u_fmt("a = get_ipython().getoutput('bc de fg')")),
325 329 ],
326 330 ],
327 331 )
328 332
329 333
330 334 def test_assign_system():
331 335 tt.check_pairs(transform_and_reset(ipt.assign_from_system), syntax['assign_system'])
332 336
333 337 def test_assign_magic():
334 338 tt.check_pairs(transform_and_reset(ipt.assign_from_magic), syntax['assign_magic'])
335 339
336 340 def test_classic_prompt():
337 341 tt.check_pairs(transform_and_reset(ipt.classic_prompt), syntax['classic_prompt'])
338 342 for example in syntax_ml['classic_prompt']:
339 343 transform_checker(example, ipt.classic_prompt)
340 344 for example in syntax_ml['multiline_datastructure_prompt']:
341 345 transform_checker(example, ipt.classic_prompt)
342 346
343 347 # Check that we don't transform the second line if the first is obviously
344 348 # IPython syntax
345 349 transform_checker([
346 350 (u'%foo', '%foo'),
347 351 (u'>>> bar', '>>> bar'),
348 352 ], ipt.classic_prompt)
349 353
350 354
351 355 def test_ipy_prompt():
352 356 tt.check_pairs(transform_and_reset(ipt.ipy_prompt), syntax['ipy_prompt'])
353 357 for example in syntax_ml['ipy_prompt']:
354 358 transform_checker(example, ipt.ipy_prompt)
355 359
356 360 # Check that we don't transform the second line if we're inside a cell magic
357 361 transform_checker([
358 362 (u'%%foo', '%%foo'),
359 363 (u'In [1]: bar', 'In [1]: bar'),
360 364 ], ipt.ipy_prompt)
361 365
362 366 def test_assemble_logical_lines():
363 367 tests = \
364 368 [ [(u"a = \\", None),
365 369 (u"123", u"a = 123"),
366 370 ],
367 371 [(u"a = \\", None), # Test resetting when within a multi-line string
368 372 (u"12 *\\", None),
369 373 (None, u"a = 12 *"),
370 374 ],
371 375 [(u"# foo\\", u"# foo\\"), # Comments can't be continued like this
372 376 ],
373 377 ]
374 378 for example in tests:
375 379 transform_checker(example, ipt.assemble_logical_lines)
376 380
377 381 def test_assemble_python_lines():
378 382 tests = \
379 383 [ [(u"a = '''", None),
380 384 (u"abc'''", u"a = '''\nabc'''"),
381 385 ],
382 386 [(u"a = '''", None), # Test resetting when within a multi-line string
383 387 (u"def", None),
384 388 (None, u"a = '''\ndef"),
385 389 ],
386 390 [(u"a = [1,", None),
387 391 (u"2]", u"a = [1,\n2]"),
388 392 ],
389 393 [(u"a = [1,", None), # Test resetting when within a multi-line string
390 394 (u"2,", None),
391 395 (None, u"a = [1,\n2,"),
392 396 ],
393 397 [(u"a = '''", None), # Test line continuation within a multi-line string
394 398 (u"abc\\", None),
395 399 (u"def", None),
396 400 (u"'''", u"a = '''\nabc\\\ndef\n'''"),
397 401 ],
398 402 ] + syntax_ml['multiline_datastructure']
399 403 for example in tests:
400 404 transform_checker(example, ipt.assemble_python_lines)
401 405
402 406
403 407 def test_help_end():
404 408 tt.check_pairs(transform_and_reset(ipt.help_end), syntax['end_help'])
405 409
406 410 def test_escaped_noesc():
407 411 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_noesc'])
408 412
409 413
410 414 def test_escaped_shell():
411 415 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_shell'])
412 416
413 417
414 418 def test_escaped_help():
415 419 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_help'])
416 420
417 421
418 422 def test_escaped_magic():
419 423 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_magic'])
420 424
421 425
422 426 def test_escaped_quote():
423 427 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_quote'])
424 428
425 429
426 430 def test_escaped_quote2():
427 431 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_quote2'])
428 432
429 433
430 434 def test_escaped_paren():
431 435 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_paren'])
432 436
433 437
434 438 def test_cellmagic():
435 439 for example in syntax_ml['cellmagic']:
436 440 transform_checker(example, ipt.cellmagic)
437 441
438 442 line_example = [(u'%%bar 123', None),
439 443 (u'hello', None),
440 444 (u'' , u_fmt("get_ipython().run_cell_magic('bar', '123', 'hello')")),
441 445 ]
442 446 transform_checker(line_example, ipt.cellmagic, end_on_blank_line=True)
443 447
444 448 def test_has_comment():
445 449 tests = [('text', False),
446 450 ('text #comment', True),
447 451 ('text #comment\n', True),
448 452 ('#comment', True),
449 453 ('#comment\n', True),
450 454 ('a = "#string"', False),
451 455 ('a = "#string" # comment', True),
452 456 ('a #comment not "string"', True),
453 457 ]
454 458 tt.check_pairs(ipt.has_comment, tests)
455 459
456 460 @ipt.TokenInputTransformer.wrap
457 461 def decistmt(tokens):
458 462 """Substitute Decimals for floats in a string of statements.
459 463
460 464 Based on an example from the tokenize module docs.
461 465 """
462 466 result = []
463 467 for toknum, tokval, _, _, _ in tokens:
464 468 if toknum == tokenize.NUMBER and '.' in tokval: # replace NUMBER tokens
465 469 for newtok in [
466 470 (tokenize.NAME, 'Decimal'),
467 471 (tokenize.OP, '('),
468 472 (tokenize.STRING, repr(tokval)),
469 473 (tokenize.OP, ')')
470 474 ]:
471 475 yield newtok
472 476 else:
473 477 yield (toknum, tokval)
474 478
475 479
476 480
477 481 def test_token_input_transformer():
478 482 tests = [(u'1.2', u_fmt(u"Decimal ('1.2')")),
479 483 (u'"1.2"', u'"1.2"'),
480 484 ]
481 485 tt.check_pairs(transform_and_reset(decistmt), tests)
482 486 ml_tests = \
483 487 [ [(u"a = 1.2; b = '''x", None),
484 488 (u"y'''", u_fmt(u"a =Decimal ('1.2');b ='''x\ny'''")),
485 489 ],
486 490 [(u"a = [1.2,", None),
487 491 (u"3]", u_fmt(u"a =[Decimal ('1.2'),\n3 ]")),
488 492 ],
489 493 [(u"a = '''foo", None), # Test resetting when within a multi-line string
490 494 (u"bar", None),
491 495 (None, u"a = '''foo\nbar"),
492 496 ],
493 497 ]
494 498 for example in ml_tests:
495 499 transform_checker(example, decistmt)
@@ -1,358 +1,395 b''
1 1 """Tests for the token-based transformers in IPython.core.inputtransformer2
2 2
3 3 Line-based transformers are the simpler ones; token-based transformers are
4 4 more complex. See test_inputtransformer2_line for tests for line-based
5 5 transformations.
6 6 """
7 7 import nose.tools as nt
8 8 import string
9 9 import sys
10 10 from textwrap import dedent
11 11
12 12 import pytest
13 13
14 14 from IPython.core import inputtransformer2 as ipt2
15 15 from IPython.core.inputtransformer2 import _find_assign_op, make_tokens_by_line
16 16 from IPython.testing.decorators import skip
17 17
18 MULTILINE_MAGIC = ("""\
18 MULTILINE_MAGIC = (
19 """\
19 20 a = f()
20 21 %foo \\
21 22 bar
22 23 g()
23 """.splitlines(keepends=True), (2, 0), """\
24 """.splitlines(
25 keepends=True
26 ),
27 (2, 0),
28 """\
24 29 a = f()
25 30 get_ipython().run_line_magic('foo', ' bar')
26 31 g()
27 """.splitlines(keepends=True))
32 """.splitlines(
33 keepends=True
34 ),
35 )
28 36
29 INDENTED_MAGIC = ("""\
37 INDENTED_MAGIC = (
38 """\
30 39 for a in range(5):
31 40 %ls
32 """.splitlines(keepends=True), (2, 4), """\
41 """.splitlines(
42 keepends=True
43 ),
44 (2, 4),
45 """\
33 46 for a in range(5):
34 47 get_ipython().run_line_magic('ls', '')
35 """.splitlines(keepends=True))
48 """.splitlines(
49 keepends=True
50 ),
51 )
36 52
37 CRLF_MAGIC = ([
38 "a = f()\n",
39 "%ls\r\n",
40 "g()\n"
41 ], (2, 0), [
42 "a = f()\n",
43 "get_ipython().run_line_magic('ls', '')\n",
44 "g()\n"
45 ])
46
47 MULTILINE_MAGIC_ASSIGN = ("""\
53 CRLF_MAGIC = (
54 ["a = f()\n", "%ls\r\n", "g()\n"],
55 (2, 0),
56 ["a = f()\n", "get_ipython().run_line_magic('ls', '')\n", "g()\n"],
57 )
58
59 MULTILINE_MAGIC_ASSIGN = (
60 """\
48 61 a = f()
49 62 b = %foo \\
50 63 bar
51 64 g()
52 """.splitlines(keepends=True), (2, 4), """\
65 """.splitlines(
66 keepends=True
67 ),
68 (2, 4),
69 """\
53 70 a = f()
54 71 b = get_ipython().run_line_magic('foo', ' bar')
55 72 g()
56 """.splitlines(keepends=True))
73 """.splitlines(
74 keepends=True
75 ),
76 )
57 77
58 78 MULTILINE_SYSTEM_ASSIGN = ("""\
59 79 a = f()
60 80 b = !foo \\
61 81 bar
62 82 g()
63 83 """.splitlines(keepends=True), (2, 4), """\
64 84 a = f()
65 85 b = get_ipython().getoutput('foo bar')
66 86 g()
67 87 """.splitlines(keepends=True))
68 88
69 89 #####
70 90
71 91 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = ("""\
72 92 def test():
73 93 for i in range(1):
74 94 print(i)
75 95 res =! ls
76 """.splitlines(keepends=True), (4, 7), '''\
96 """.splitlines(
97 keepends=True
98 ),
99 (4, 7),
100 """\
77 101 def test():
78 102 for i in range(1):
79 103 print(i)
80 104 res =get_ipython().getoutput(\' ls\')
81 '''.splitlines(keepends=True))
105 """.splitlines(
106 keepends=True
107 ),
108 )
82 109
83 110 ######
84 111
85 AUTOCALL_QUOTE = (
86 [",f 1 2 3\n"], (1, 0),
87 ['f("1", "2", "3")\n']
88 )
112 AUTOCALL_QUOTE = ([",f 1 2 3\n"], (1, 0), ['f("1", "2", "3")\n'])
89 113
90 AUTOCALL_QUOTE2 = (
91 [";f 1 2 3\n"], (1, 0),
92 ['f("1 2 3")\n']
93 )
114 AUTOCALL_QUOTE2 = ([";f 1 2 3\n"], (1, 0), ['f("1 2 3")\n'])
94 115
95 AUTOCALL_PAREN = (
96 ["/f 1 2 3\n"], (1, 0),
97 ['f(1, 2, 3)\n']
98 )
116 AUTOCALL_PAREN = (["/f 1 2 3\n"], (1, 0), ["f(1, 2, 3)\n"])
99 117
100 SIMPLE_HELP = (
101 ["foo?\n"], (1, 0),
102 ["get_ipython().run_line_magic('pinfo', 'foo')\n"]
103 )
118 SIMPLE_HELP = (["foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', 'foo')\n"])
104 119
105 120 DETAILED_HELP = (
106 ["foo??\n"], (1, 0),
107 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"]
121 ["foo??\n"],
122 (1, 0),
123 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"],
108 124 )
109 125
110 MAGIC_HELP = (
111 ["%foo?\n"], (1, 0),
112 ["get_ipython().run_line_magic('pinfo', '%foo')\n"]
113 )
126 MAGIC_HELP = (["%foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', '%foo')\n"])
114 127
115 128 HELP_IN_EXPR = (
116 ["a = b + c?\n"], (1, 0),
117 ["get_ipython().set_next_input('a = b + c');"
118 "get_ipython().run_line_magic('pinfo', 'c')\n"]
129 ["a = b + c?\n"],
130 (1, 0),
131 ["get_ipython().run_line_magic('pinfo', 'c')\n"],
119 132 )
120 133
121 HELP_CONTINUED_LINE = ("""\
134 HELP_CONTINUED_LINE = (
135 """\
122 136 a = \\
123 137 zip?
124 """.splitlines(keepends=True), (1, 0),
125 [r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
138 """.splitlines(
139 keepends=True
140 ),
141 (1, 0),
142 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
126 143 )
127 144
128 HELP_MULTILINE = ("""\
145 HELP_MULTILINE = (
146 """\
129 147 (a,
130 148 b) = zip?
131 """.splitlines(keepends=True), (1, 0),
132 [r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
149 """.splitlines(
150 keepends=True
151 ),
152 (1, 0),
153 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
133 154 )
134 155
135 156 HELP_UNICODE = (
136 ["Ο€.foo?\n"], (1, 0),
137 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"]
157 ["Ο€.foo?\n"],
158 (1, 0),
159 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"],
138 160 )
139 161
140 162
141 163 def null_cleanup_transformer(lines):
142 164 """
143 165 A cleanup transform that returns an empty list.
144 166 """
145 167 return []
146 168
147 169 def check_make_token_by_line_never_ends_empty():
148 170 """
149 171 Check that not sequence of single or double characters ends up leading to en empty list of tokens
150 172 """
151 173 from string import printable
174
152 175 for c in printable:
153 176 nt.assert_not_equal(make_tokens_by_line(c)[-1], [])
154 177 for k in printable:
155 178 nt.assert_not_equal(make_tokens_by_line(c+k)[-1], [])
156 179
157 180 def check_find(transformer, case, match=True):
158 sample, expected_start, _ = case
181 sample, expected_start, _ = case
159 182 tbl = make_tokens_by_line(sample)
160 183 res = transformer.find(tbl)
161 184 if match:
162 185 # start_line is stored 0-indexed, expected values are 1-indexed
163 186 nt.assert_equal((res.start_line+1, res.start_col), expected_start)
164 187 return res
165 188 else:
166 189 nt.assert_is(res, None)
167 190
191
168 192 def check_transform(transformer_cls, case):
169 193 lines, start, expected = case
170 194 transformer = transformer_cls(start)
171 195 nt.assert_equal(transformer.transform(lines), expected)
172 196
197
173 198 def test_continued_line():
174 199 lines = MULTILINE_MAGIC_ASSIGN[0]
175 200 nt.assert_equal(ipt2.find_end_of_continued_line(lines, 1), 2)
176 201
177 202 nt.assert_equal(ipt2.assemble_continued_line(lines, (1, 5), 2), "foo bar")
178 203
204
179 205 def test_find_assign_magic():
180 206 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
181 207 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
182 208 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
183 209
210
184 211 def test_transform_assign_magic():
185 212 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
186 213
214
187 215 def test_find_assign_system():
188 216 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
189 217 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
190 218 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
191 219 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
192 220 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
193 221
222
194 223 def test_transform_assign_system():
195 224 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
196 225 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
197 226
227
198 228 def test_find_magic_escape():
199 229 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
200 230 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
201 231 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
202 232
233
203 234 def test_transform_magic_escape():
204 235 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
205 236 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
206 237 check_transform(ipt2.EscapedCommand, CRLF_MAGIC)
207 238
239
208 240 def test_find_autocalls():
209 241 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
210 242 print("Testing %r" % case[0])
211 243 check_find(ipt2.EscapedCommand, case)
212 244
245
213 246 def test_transform_autocall():
214 247 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
215 248 print("Testing %r" % case[0])
216 249 check_transform(ipt2.EscapedCommand, case)
217 250
251
218 252 def test_find_help():
219 253 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
220 254 check_find(ipt2.HelpEnd, case)
221 255
222 256 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
223 257 nt.assert_equal(tf.q_line, 1)
224 258 nt.assert_equal(tf.q_col, 3)
225 259
226 260 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
227 261 nt.assert_equal(tf.q_line, 1)
228 262 nt.assert_equal(tf.q_col, 8)
229 263
230 264 # ? in a comment does not trigger help
231 265 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
232 266 # Nor in a string
233 267 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
234 268
269
235 270 def test_transform_help():
236 271 tf = ipt2.HelpEnd((1, 0), (1, 9))
237 272 nt.assert_equal(tf.transform(HELP_IN_EXPR[0]), HELP_IN_EXPR[2])
238 273
239 274 tf = ipt2.HelpEnd((1, 0), (2, 3))
240 275 nt.assert_equal(tf.transform(HELP_CONTINUED_LINE[0]), HELP_CONTINUED_LINE[2])
241 276
242 277 tf = ipt2.HelpEnd((1, 0), (2, 8))
243 278 nt.assert_equal(tf.transform(HELP_MULTILINE[0]), HELP_MULTILINE[2])
244 279
245 280 tf = ipt2.HelpEnd((1, 0), (1, 0))
246 281 nt.assert_equal(tf.transform(HELP_UNICODE[0]), HELP_UNICODE[2])
247 282
283
248 284 def test_find_assign_op_dedent():
249 285 """
250 286 be careful that empty token like dedent are not counted as parens
251 287 """
288
252 289 class Tk:
253 290 def __init__(self, s):
254 291 self.string = s
255 292
256 293 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','a','=','b')]), 2)
257 294 nt.assert_equal(_find_assign_op([Tk(s) for s in ('','(', 'a','=','b', ')', '=' ,'5')]), 6)
258 295
259 296 examples = [
260 297 pytest.param("a = 1", "complete", None),
261 298 pytest.param("for a in range(5):", "incomplete", 4),
262 299 pytest.param("for a in range(5):\n if a > 0:", "incomplete", 8),
263 300 pytest.param("raise = 2", "invalid", None),
264 301 pytest.param("a = [1,\n2,", "incomplete", 0),
265 302 pytest.param("(\n))", "incomplete", 0),
266 303 pytest.param("\\\r\n", "incomplete", 0),
267 304 pytest.param("a = '''\n hi", "incomplete", 3),
268 305 pytest.param("def a():\n x=1\n global x", "invalid", None),
269 306 pytest.param(
270 307 "a \\ ",
271 308 "invalid",
272 309 None,
273 310 marks=pytest.mark.xfail(
274 311 reason="Bug in python 3.9.8 – bpo 45738",
275 312 condition=sys.version_info[:3] == (3, 9, 8),
276 313 raises=SystemError,
277 314 strict=True,
278 315 ),
279 316 ), # Nothing allowed after backslash,
280 317 pytest.param("1\\\n+2", "complete", None),
281 318 ]
282 319
283 320
284 321 @skip('Tested on master, skip only on iptest not available on 7.x')
285 322 @pytest.mark.xfail(
286 323 reason="Bug in python 3.9.8 – bpo 45738",
287 324 condition=sys.version_info[:3] == (3, 9, 8),
288 325 )
289 326 def test_check_complete():
290 327 cc = ipt2.TransformerManager().check_complete
291 328
292 example = dedent("""
329 example = dedent(
330 """
293 331 if True:
294 a=1""" )
332 a=1"""
333 )
295 334
296 335 nt.assert_equal(cc(example), ('incomplete', 4))
297 336 nt.assert_equal(cc(example+'\n'), ('complete', None))
298 337 nt.assert_equal(cc(example+'\n '), ('complete', None))
299 338
300 339 # no need to loop on all the letters/numbers.
301 short = '12abAB'+string.printable[62:]
340 short = "12abAB" + string.printable[62:]
302 341 for c in short:
303 342 # test does not raise:
304 343 cc(c)
305 344 for k in short:
306 cc(c+k)
345 cc(c + k)
307 346
308 347 nt.assert_equal(cc("def f():\n x=0\n \\\n "), ('incomplete', 2))
309 348
310 349 def test_check_complete_II():
311 350 """
312 351 Test that multiple line strings are properly handled.
313 352
314 353 Separate test function for convenience
315 354
316 355 """
317 356 cc = ipt2.TransformerManager().check_complete
318 357 nt.assert_equal(cc('''def foo():\n """'''), ('incomplete', 4))
319 358
320 359
321 360 def test_null_cleanup_transformer():
322 361 manager = ipt2.TransformerManager()
323 362 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
324 363 assert manager.transform_cell("") == ""
325 364
326 365
327
328
329 366 def test_side_effects_I():
330 367 count = 0
368
331 369 def counter(lines):
332 370 nonlocal count
333 371 count += 1
334 372 return lines
335 373
336 374 counter.has_side_effects = True
337 375
338 376 manager = ipt2.TransformerManager()
339 377 manager.cleanup_transforms.insert(0, counter)
340 assert manager.check_complete("a=1\n") == ('complete', None)
378 assert manager.check_complete("a=1\n") == ("complete", None)
341 379 assert count == 0
342 380
343 381
344
345
346 382 def test_side_effects_II():
347 383 count = 0
384
348 385 def counter(lines):
349 386 nonlocal count
350 387 count += 1
351 388 return lines
352 389
353 390 counter.has_side_effects = True
354 391
355 392 manager = ipt2.TransformerManager()
356 393 manager.line_transforms.insert(0, counter)
357 assert manager.check_complete("b=1\n") == ('complete', None)
394 assert manager.check_complete("b=1\n") == ("complete", None)
358 395 assert count == 0
General Comments 0
You need to be logged in to leave comments. Login now