##// END OF EJS Templates
Remove set-next input when triggering help....
Matthias Bussonnier -
Show More
@@ -1,538 +1,536 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 Notes
412 410 -----
413 411 If `initial_re` and `prompt_re differ`,
414 412 only `initial_re` will be tested against the first line.
415 413 If any prompt is found on the first two lines,
416 414 prompts will be stripped from the rest of the block.
417 415 """
418 416 if initial_re is None:
419 417 initial_re = prompt_re
420 418 line = ''
421 419 while True:
422 420 line = (yield line)
423 421
424 422 # First line of cell
425 423 if line is None:
426 424 continue
427 425 out, n1 = initial_re.subn('', line, count=1)
428 426 if turnoff_re and not n1:
429 427 if turnoff_re.match(line):
430 428 # We're in e.g. a cell magic; disable this transformer for
431 429 # the rest of the cell.
432 430 while line is not None:
433 431 line = (yield line)
434 432 continue
435 433
436 434 line = (yield out)
437 435
438 436 if line is None:
439 437 continue
440 438 # check for any prompt on the second line of the cell,
441 439 # because people often copy from just after the first prompt,
442 440 # so we might not see it in the first line.
443 441 out, n2 = prompt_re.subn('', line, count=1)
444 442 line = (yield out)
445 443
446 444 if n1 or n2:
447 445 # Found a prompt in the first two lines - check for it in
448 446 # the rest of the cell as well.
449 447 while line is not None:
450 448 line = (yield prompt_re.sub('', line, count=1))
451 449
452 450 else:
453 451 # Prompts not in input - wait for reset
454 452 while line is not None:
455 453 line = (yield line)
456 454
457 455 @CoroutineInputTransformer.wrap
458 456 def classic_prompt():
459 457 """Strip the >>>/... prompts of the Python interactive shell."""
460 458 # FIXME: non-capturing version (?:...) usable?
461 459 prompt_re = re.compile(r'^(>>>|\.\.\.)( |$)')
462 460 initial_re = re.compile(r'^>>>( |$)')
463 461 # Any %magic/!system is IPython syntax, so we needn't look for >>> prompts
464 462 turnoff_re = re.compile(r'^[%!]')
465 463 return _strip_prompts(prompt_re, initial_re, turnoff_re)
466 464
467 465 @CoroutineInputTransformer.wrap
468 466 def ipy_prompt():
469 467 """Strip IPython's In [1]:/...: prompts."""
470 468 # FIXME: non-capturing version (?:...) usable?
471 469 prompt_re = re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)')
472 470 # Disable prompt stripping inside cell magics
473 471 turnoff_re = re.compile(r'^%%')
474 472 return _strip_prompts(prompt_re, turnoff_re=turnoff_re)
475 473
476 474
477 475 @CoroutineInputTransformer.wrap
478 476 def leading_indent():
479 477 """Remove leading indentation.
480 478
481 479 If the first line starts with a spaces or tabs, the same whitespace will be
482 480 removed from each following line until it is reset.
483 481 """
484 482 space_re = re.compile(r'^[ \t]+')
485 483 line = ''
486 484 while True:
487 485 line = (yield line)
488 486
489 487 if line is None:
490 488 continue
491 489
492 490 m = space_re.match(line)
493 491 if m:
494 492 space = m.group(0)
495 493 while line is not None:
496 494 if line.startswith(space):
497 495 line = line[len(space):]
498 496 line = (yield line)
499 497 else:
500 498 # No leading spaces - wait for reset
501 499 while line is not None:
502 500 line = (yield line)
503 501
504 502
505 503 _assign_pat = \
506 504 r'''(?P<lhs>(\s*)
507 505 ([\w\.]+) # Initial identifier
508 506 (\s*,\s*
509 507 \*?[\w\.]+)* # Further identifiers for unpacking
510 508 \s*?,? # Trailing comma
511 509 )
512 510 \s*=\s*
513 511 '''
514 512
515 513 assign_system_re = re.compile(r'{}!\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
516 514 assign_system_template = '%s = get_ipython().getoutput(%r)'
517 515 @StatelessInputTransformer.wrap
518 516 def assign_from_system(line):
519 517 """Transform assignment from system commands (e.g. files = !ls)"""
520 518 m = assign_system_re.match(line)
521 519 if m is None:
522 520 return line
523 521
524 522 return assign_system_template % m.group('lhs', 'cmd')
525 523
526 524 assign_magic_re = re.compile(r'{}%\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
527 525 assign_magic_template = '%s = get_ipython().run_line_magic(%r, %r)'
528 526 @StatelessInputTransformer.wrap
529 527 def assign_from_magic(line):
530 528 """Transform assignment from magic commands (e.g. a = %who_ls)"""
531 529 m = assign_magic_re.match(line)
532 530 if m is None:
533 531 return line
534 532 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
535 533 m_lhs, m_cmd = m.group('lhs', 'cmd')
536 534 t_magic_name, _, t_magic_arg_s = m_cmd.partition(' ')
537 535 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
538 536 return assign_magic_template % (m_lhs, t_magic_name, t_magic_arg_s)
@@ -1,796 +1,788 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, Optional, Any
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(
95 95 re.compile(
96 96 r"""
97 97 ^( # Match from the beginning of a line, either:
98 98
99 99 # 1. First-line prompt:
100 100 ((\[nav\]|\[ins\])?\ )? # Vi editing mode prompt, if it's there
101 101 In\ # The 'In' of the prompt, with a space
102 102 \[\d+\]: # Command index, as displayed in the prompt
103 103 \ # With a mandatory trailing space
104 104
105 105 | # ... or ...
106 106
107 107 # 2. The three dots of the multiline prompt
108 108 \s* # All leading whitespace characters
109 109 \.{3,}: # The three (or more) dots
110 110 \ ? # With an optional trailing space
111 111
112 112 )
113 113 """,
114 114 re.VERBOSE,
115 115 )
116 116 )
117 117
118 118
119 119 def cell_magic(lines):
120 120 if not lines or not lines[0].startswith('%%'):
121 121 return lines
122 122 if re.match(r'%%\w+\?', lines[0]):
123 123 # This case will be handled by help_end
124 124 return lines
125 125 magic_name, _, first_line = lines[0][2:].rstrip().partition(' ')
126 126 body = ''.join(lines[1:])
127 127 return ['get_ipython().run_cell_magic(%r, %r, %r)\n'
128 128 % (magic_name, first_line, body)]
129 129
130 130
131 131 def _find_assign_op(token_line) -> Optional[int]:
132 132 """Get the index of the first assignment in the line ('=' not inside brackets)
133 133
134 134 Note: We don't try to support multiple special assignment (a = b = %foo)
135 135 """
136 136 paren_level = 0
137 137 for i, ti in enumerate(token_line):
138 138 s = ti.string
139 139 if s == '=' and paren_level == 0:
140 140 return i
141 141 if s in {'(','[','{'}:
142 142 paren_level += 1
143 143 elif s in {')', ']', '}'}:
144 144 if paren_level > 0:
145 145 paren_level -= 1
146 146 return None
147 147
148 148 def find_end_of_continued_line(lines, start_line: int):
149 149 """Find the last line of a line explicitly extended using backslashes.
150 150
151 151 Uses 0-indexed line numbers.
152 152 """
153 153 end_line = start_line
154 154 while lines[end_line].endswith('\\\n'):
155 155 end_line += 1
156 156 if end_line >= len(lines):
157 157 break
158 158 return end_line
159 159
160 160 def assemble_continued_line(lines, start: Tuple[int, int], end_line: int):
161 161 r"""Assemble a single line from multiple continued line pieces
162 162
163 163 Continued lines are lines ending in ``\``, and the line following the last
164 164 ``\`` in the block.
165 165
166 166 For example, this code continues over multiple lines::
167 167
168 168 if (assign_ix is not None) \
169 169 and (len(line) >= assign_ix + 2) \
170 170 and (line[assign_ix+1].string == '%') \
171 171 and (line[assign_ix+2].type == tokenize.NAME):
172 172
173 173 This statement contains four continued line pieces.
174 174 Assembling these pieces into a single line would give::
175 175
176 176 if (assign_ix is not None) and (len(line) >= assign_ix + 2) and (line[...
177 177
178 178 This uses 0-indexed line numbers. *start* is (lineno, colno).
179 179
180 180 Used to allow ``%magic`` and ``!system`` commands to be continued over
181 181 multiple lines.
182 182 """
183 183 parts = [lines[start[0]][start[1]:]] + lines[start[0]+1:end_line+1]
184 184 return ' '.join([p.rstrip()[:-1] for p in parts[:-1]] # Strip backslash+newline
185 185 + [parts[-1].rstrip()]) # Strip newline from last line
186 186
187 187 class TokenTransformBase:
188 188 """Base class for transformations which examine tokens.
189 189
190 190 Special syntax should not be transformed when it occurs inside strings or
191 191 comments. This is hard to reliably avoid with regexes. The solution is to
192 192 tokenise the code as Python, and recognise the special syntax in the tokens.
193 193
194 194 IPython's special syntax is not valid Python syntax, so tokenising may go
195 195 wrong after the special syntax starts. These classes therefore find and
196 196 transform *one* instance of special syntax at a time into regular Python
197 197 syntax. After each transformation, tokens are regenerated to find the next
198 198 piece of special syntax.
199 199
200 200 Subclasses need to implement one class method (find)
201 201 and one regular method (transform).
202 202
203 203 The priority attribute can select which transformation to apply if multiple
204 204 transformers match in the same place. Lower numbers have higher priority.
205 205 This allows "%magic?" to be turned into a help call rather than a magic call.
206 206 """
207 207 # Lower numbers -> higher priority (for matches in the same location)
208 208 priority = 10
209 209
210 210 def sortby(self):
211 211 return self.start_line, self.start_col, self.priority
212 212
213 213 def __init__(self, start):
214 214 self.start_line = start[0] - 1 # Shift from 1-index to 0-index
215 215 self.start_col = start[1]
216 216
217 217 @classmethod
218 218 def find(cls, tokens_by_line):
219 219 """Find one instance of special syntax in the provided tokens.
220 220
221 221 Tokens are grouped into logical lines for convenience,
222 222 so it is easy to e.g. look at the first token of each line.
223 223 *tokens_by_line* is a list of lists of tokenize.TokenInfo objects.
224 224
225 225 This should return an instance of its class, pointing to the start
226 226 position it has found, or None if it found no match.
227 227 """
228 228 raise NotImplementedError
229 229
230 230 def transform(self, lines: List[str]):
231 231 """Transform one instance of special syntax found by ``find()``
232 232
233 233 Takes a list of strings representing physical lines,
234 234 returns a similar list of transformed lines.
235 235 """
236 236 raise NotImplementedError
237 237
238 238 class MagicAssign(TokenTransformBase):
239 239 """Transformer for assignments from magics (a = %foo)"""
240 240 @classmethod
241 241 def find(cls, tokens_by_line):
242 242 """Find the first magic assignment (a = %foo) in the cell.
243 243 """
244 244 for line in tokens_by_line:
245 245 assign_ix = _find_assign_op(line)
246 246 if (assign_ix is not None) \
247 247 and (len(line) >= assign_ix + 2) \
248 248 and (line[assign_ix+1].string == '%') \
249 249 and (line[assign_ix+2].type == tokenize.NAME):
250 250 return cls(line[assign_ix+1].start)
251 251
252 252 def transform(self, lines: List[str]):
253 253 """Transform a magic assignment found by the ``find()`` classmethod.
254 254 """
255 255 start_line, start_col = self.start_line, self.start_col
256 256 lhs = lines[start_line][:start_col]
257 257 end_line = find_end_of_continued_line(lines, start_line)
258 258 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
259 259 assert rhs.startswith('%'), rhs
260 260 magic_name, _, args = rhs[1:].partition(' ')
261 261
262 262 lines_before = lines[:start_line]
263 263 call = "get_ipython().run_line_magic({!r}, {!r})".format(magic_name, args)
264 264 new_line = lhs + call + '\n'
265 265 lines_after = lines[end_line+1:]
266 266
267 267 return lines_before + [new_line] + lines_after
268 268
269 269
270 270 class SystemAssign(TokenTransformBase):
271 271 """Transformer for assignments from system commands (a = !foo)"""
272 272 @classmethod
273 273 def find(cls, tokens_by_line):
274 274 """Find the first system assignment (a = !foo) in the cell.
275 275 """
276 276 for line in tokens_by_line:
277 277 assign_ix = _find_assign_op(line)
278 278 if (assign_ix is not None) \
279 279 and not line[assign_ix].line.strip().startswith('=') \
280 280 and (len(line) >= assign_ix + 2) \
281 281 and (line[assign_ix + 1].type == tokenize.ERRORTOKEN):
282 282 ix = assign_ix + 1
283 283
284 284 while ix < len(line) and line[ix].type == tokenize.ERRORTOKEN:
285 285 if line[ix].string == '!':
286 286 return cls(line[ix].start)
287 287 elif not line[ix].string.isspace():
288 288 break
289 289 ix += 1
290 290
291 291 def transform(self, lines: List[str]):
292 292 """Transform a system assignment found by the ``find()`` classmethod.
293 293 """
294 294 start_line, start_col = self.start_line, self.start_col
295 295
296 296 lhs = lines[start_line][:start_col]
297 297 end_line = find_end_of_continued_line(lines, start_line)
298 298 rhs = assemble_continued_line(lines, (start_line, start_col), end_line)
299 299 assert rhs.startswith('!'), rhs
300 300 cmd = rhs[1:]
301 301
302 302 lines_before = lines[:start_line]
303 303 call = "get_ipython().getoutput({!r})".format(cmd)
304 304 new_line = lhs + call + '\n'
305 305 lines_after = lines[end_line + 1:]
306 306
307 307 return lines_before + [new_line] + lines_after
308 308
309 309 # The escape sequences that define the syntax transformations IPython will
310 310 # apply to user input. These can NOT be just changed here: many regular
311 311 # expressions and other parts of the code may use their hardcoded values, and
312 312 # for all intents and purposes they constitute the 'IPython syntax', so they
313 313 # should be considered fixed.
314 314
315 315 ESC_SHELL = '!' # Send line to underlying system shell
316 316 ESC_SH_CAP = '!!' # Send line to system shell and capture output
317 317 ESC_HELP = '?' # Find information about object
318 318 ESC_HELP2 = '??' # Find extra-detailed information about object
319 319 ESC_MAGIC = '%' # Call magic function
320 320 ESC_MAGIC2 = '%%' # Call cell-magic function
321 321 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
322 322 ESC_QUOTE2 = ';' # Quote all args as a single string, call
323 323 ESC_PAREN = '/' # Call first argument with rest of line as arguments
324 324
325 325 ESCAPE_SINGLES = {'!', '?', '%', ',', ';', '/'}
326 326 ESCAPE_DOUBLES = {'!!', '??'} # %% (cell magic) is handled separately
327 327
328 def _make_help_call(target, esc, next_input=None):
328 def _make_help_call(target, esc):
329 329 """Prepares a pinfo(2)/psearch call from a target name and the escape
330 330 (i.e. ? or ??)"""
331 331 method = 'pinfo2' if esc == '??' \
332 332 else 'psearch' if '*' in target \
333 333 else 'pinfo'
334 334 arg = " ".join([method, target])
335 335 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
336 336 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
337 337 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
338 if next_input is None:
339 return 'get_ipython().run_line_magic(%r, %r)' % (t_magic_name, t_magic_arg_s)
340 else:
341 return 'get_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
342 (next_input, t_magic_name, t_magic_arg_s)
338 return "get_ipython().run_line_magic(%r, %r)" % (t_magic_name, t_magic_arg_s)
339
343 340
344 341 def _tr_help(content):
345 342 """Translate lines escaped with: ?
346 343
347 344 A naked help line should fire the intro help screen (shell.show_usage())
348 345 """
349 346 if not content:
350 347 return 'get_ipython().show_usage()'
351 348
352 349 return _make_help_call(content, '?')
353 350
354 351 def _tr_help2(content):
355 352 """Translate lines escaped with: ??
356 353
357 354 A naked help line should fire the intro help screen (shell.show_usage())
358 355 """
359 356 if not content:
360 357 return 'get_ipython().show_usage()'
361 358
362 359 return _make_help_call(content, '??')
363 360
364 361 def _tr_magic(content):
365 362 "Translate lines escaped with a percent sign: %"
366 363 name, _, args = content.partition(' ')
367 364 return 'get_ipython().run_line_magic(%r, %r)' % (name, args)
368 365
369 366 def _tr_quote(content):
370 367 "Translate lines escaped with a comma: ,"
371 368 name, _, args = content.partition(' ')
372 369 return '%s("%s")' % (name, '", "'.join(args.split()) )
373 370
374 371 def _tr_quote2(content):
375 372 "Translate lines escaped with a semicolon: ;"
376 373 name, _, args = content.partition(' ')
377 374 return '%s("%s")' % (name, args)
378 375
379 376 def _tr_paren(content):
380 377 "Translate lines escaped with a slash: /"
381 378 name, _, args = content.partition(' ')
382 379 return '%s(%s)' % (name, ", ".join(args.split()))
383 380
384 381 tr = { ESC_SHELL : 'get_ipython().system({!r})'.format,
385 382 ESC_SH_CAP : 'get_ipython().getoutput({!r})'.format,
386 383 ESC_HELP : _tr_help,
387 384 ESC_HELP2 : _tr_help2,
388 385 ESC_MAGIC : _tr_magic,
389 386 ESC_QUOTE : _tr_quote,
390 387 ESC_QUOTE2 : _tr_quote2,
391 388 ESC_PAREN : _tr_paren }
392 389
393 390 class EscapedCommand(TokenTransformBase):
394 391 """Transformer for escaped commands like %foo, !foo, or /foo"""
395 392 @classmethod
396 393 def find(cls, tokens_by_line):
397 394 """Find the first escaped command (%foo, !foo, etc.) in the cell.
398 395 """
399 396 for line in tokens_by_line:
400 397 if not line:
401 398 continue
402 399 ix = 0
403 400 ll = len(line)
404 401 while ll > ix and line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
405 402 ix += 1
406 403 if ix >= ll:
407 404 continue
408 405 if line[ix].string in ESCAPE_SINGLES:
409 406 return cls(line[ix].start)
410 407
411 408 def transform(self, lines):
412 409 """Transform an escaped line found by the ``find()`` classmethod.
413 410 """
414 411 start_line, start_col = self.start_line, self.start_col
415 412
416 413 indent = lines[start_line][:start_col]
417 414 end_line = find_end_of_continued_line(lines, start_line)
418 415 line = assemble_continued_line(lines, (start_line, start_col), end_line)
419 416
420 417 if len(line) > 1 and line[:2] in ESCAPE_DOUBLES:
421 418 escape, content = line[:2], line[2:]
422 419 else:
423 420 escape, content = line[:1], line[1:]
424 421
425 422 if escape in tr:
426 423 call = tr[escape](content)
427 424 else:
428 425 call = ''
429 426
430 427 lines_before = lines[:start_line]
431 428 new_line = indent + call + '\n'
432 429 lines_after = lines[end_line + 1:]
433 430
434 431 return lines_before + [new_line] + lines_after
435 432
436 433 _help_end_re = re.compile(r"""(%{0,2}
437 434 (?!\d)[\w*]+ # Variable name
438 435 (\.(?!\d)[\w*]+)* # .etc.etc
439 436 )
440 437 (\?\??)$ # ? or ??
441 438 """,
442 439 re.VERBOSE)
443 440
444 441 class HelpEnd(TokenTransformBase):
445 442 """Transformer for help syntax: obj? and obj??"""
446 443 # This needs to be higher priority (lower number) than EscapedCommand so
447 444 # that inspecting magics (%foo?) works.
448 445 priority = 5
449 446
450 447 def __init__(self, start, q_locn):
451 448 super().__init__(start)
452 449 self.q_line = q_locn[0] - 1 # Shift from 1-indexed to 0-indexed
453 450 self.q_col = q_locn[1]
454 451
455 452 @classmethod
456 453 def find(cls, tokens_by_line):
457 454 """Find the first help command (foo?) in the cell.
458 455 """
459 456 for line in tokens_by_line:
460 457 # Last token is NEWLINE; look at last but one
461 458 if len(line) > 2 and line[-2].string == '?':
462 459 # Find the first token that's not INDENT/DEDENT
463 460 ix = 0
464 461 while line[ix].type in {tokenize.INDENT, tokenize.DEDENT}:
465 462 ix += 1
466 463 return cls(line[ix].start, line[-2].start)
467 464
468 465 def transform(self, lines):
469 466 """Transform a help command found by the ``find()`` classmethod.
470 467 """
471 468 piece = ''.join(lines[self.start_line:self.q_line+1])
472 469 indent, content = piece[:self.start_col], piece[self.start_col:]
473 470 lines_before = lines[:self.start_line]
474 471 lines_after = lines[self.q_line + 1:]
475 472
476 473 m = _help_end_re.search(content)
477 474 if not m:
478 475 raise SyntaxError(content)
479 476 assert m is not None, content
480 477 target = m.group(1)
481 478 esc = m.group(3)
482 479
483 # If we're mid-command, put it back on the next prompt for the user.
484 next_input = None
485 if (not lines_before) and (not lines_after) \
486 and content.strip() != m.group(0):
487 next_input = content.rstrip('?\n')
488 480
489 call = _make_help_call(target, esc, next_input=next_input)
481 call = _make_help_call(target, esc)
490 482 new_line = indent + call + '\n'
491 483
492 484 return lines_before + [new_line] + lines_after
493 485
494 486 def make_tokens_by_line(lines:List[str]):
495 487 """Tokenize a series of lines and group tokens by line.
496 488
497 489 The tokens for a multiline Python string or expression are grouped as one
498 490 line. All lines except the last lines should keep their line ending ('\\n',
499 491 '\\r\\n') for this to properly work. Use `.splitlines(keeplineending=True)`
500 492 for example when passing block of text to this function.
501 493
502 494 """
503 495 # NL tokens are used inside multiline expressions, but also after blank
504 496 # lines or comments. This is intentional - see https://bugs.python.org/issue17061
505 497 # We want to group the former case together but split the latter, so we
506 498 # track parentheses level, similar to the internals of tokenize.
507 499
508 500 # reexported from token on 3.7+
509 501 NEWLINE, NL = tokenize.NEWLINE, tokenize.NL # type: ignore
510 502 tokens_by_line: List[List[Any]] = [[]]
511 503 if len(lines) > 1 and not lines[0].endswith(("\n", "\r", "\r\n", "\x0b", "\x0c")):
512 504 warnings.warn(
513 505 "`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",
514 506 stacklevel=2,
515 507 )
516 508 parenlev = 0
517 509 try:
518 510 for token in tokenize.generate_tokens(iter(lines).__next__):
519 511 tokens_by_line[-1].append(token)
520 512 if (token.type == NEWLINE) \
521 513 or ((token.type == NL) and (parenlev <= 0)):
522 514 tokens_by_line.append([])
523 515 elif token.string in {'(', '[', '{'}:
524 516 parenlev += 1
525 517 elif token.string in {')', ']', '}'}:
526 518 if parenlev > 0:
527 519 parenlev -= 1
528 520 except tokenize.TokenError:
529 521 # Input ended in a multiline string or expression. That's OK for us.
530 522 pass
531 523
532 524
533 525 if not tokens_by_line[-1]:
534 526 tokens_by_line.pop()
535 527
536 528
537 529 return tokens_by_line
538 530
539 531
540 532 def has_sunken_brackets(tokens: List[tokenize.TokenInfo]):
541 533 """Check if the depth of brackets in the list of tokens drops below 0"""
542 534 parenlev = 0
543 535 for token in tokens:
544 536 if token.string in {"(", "[", "{"}:
545 537 parenlev += 1
546 538 elif token.string in {")", "]", "}"}:
547 539 parenlev -= 1
548 540 if parenlev < 0:
549 541 return True
550 542 return False
551 543
552 544
553 545 def show_linewise_tokens(s: str):
554 546 """For investigation and debugging"""
555 547 if not s.endswith('\n'):
556 548 s += '\n'
557 549 lines = s.splitlines(keepends=True)
558 550 for line in make_tokens_by_line(lines):
559 551 print("Line -------")
560 552 for tokinfo in line:
561 553 print(" ", tokinfo)
562 554
563 555 # Arbitrary limit to prevent getting stuck in infinite loops
564 556 TRANSFORM_LOOP_LIMIT = 500
565 557
566 558 class TransformerManager:
567 559 """Applies various transformations to a cell or code block.
568 560
569 561 The key methods for external use are ``transform_cell()``
570 562 and ``check_complete()``.
571 563 """
572 564 def __init__(self):
573 565 self.cleanup_transforms = [
574 566 leading_empty_lines,
575 567 leading_indent,
576 568 classic_prompt,
577 569 ipython_prompt,
578 570 ]
579 571 self.line_transforms = [
580 572 cell_magic,
581 573 ]
582 574 self.token_transformers = [
583 575 MagicAssign,
584 576 SystemAssign,
585 577 EscapedCommand,
586 578 HelpEnd,
587 579 ]
588 580
589 581 def do_one_token_transform(self, lines):
590 582 """Find and run the transform earliest in the code.
591 583
592 584 Returns (changed, lines).
593 585
594 586 This method is called repeatedly until changed is False, indicating
595 587 that all available transformations are complete.
596 588
597 589 The tokens following IPython special syntax might not be valid, so
598 590 the transformed code is retokenised every time to identify the next
599 591 piece of special syntax. Hopefully long code cells are mostly valid
600 592 Python, not using lots of IPython special syntax, so this shouldn't be
601 593 a performance issue.
602 594 """
603 595 tokens_by_line = make_tokens_by_line(lines)
604 596 candidates = []
605 597 for transformer_cls in self.token_transformers:
606 598 transformer = transformer_cls.find(tokens_by_line)
607 599 if transformer:
608 600 candidates.append(transformer)
609 601
610 602 if not candidates:
611 603 # Nothing to transform
612 604 return False, lines
613 605 ordered_transformers = sorted(candidates, key=TokenTransformBase.sortby)
614 606 for transformer in ordered_transformers:
615 607 try:
616 608 return True, transformer.transform(lines)
617 609 except SyntaxError:
618 610 pass
619 611 return False, lines
620 612
621 613 def do_token_transforms(self, lines):
622 614 for _ in range(TRANSFORM_LOOP_LIMIT):
623 615 changed, lines = self.do_one_token_transform(lines)
624 616 if not changed:
625 617 return lines
626 618
627 619 raise RuntimeError("Input transformation still changing after "
628 620 "%d iterations. Aborting." % TRANSFORM_LOOP_LIMIT)
629 621
630 622 def transform_cell(self, cell: str) -> str:
631 623 """Transforms a cell of input code"""
632 624 if not cell.endswith('\n'):
633 625 cell += '\n' # Ensure the cell has a trailing newline
634 626 lines = cell.splitlines(keepends=True)
635 627 for transform in self.cleanup_transforms + self.line_transforms:
636 628 lines = transform(lines)
637 629
638 630 lines = self.do_token_transforms(lines)
639 631 return ''.join(lines)
640 632
641 633 def check_complete(self, cell: str):
642 634 """Return whether a block of code is ready to execute, or should be continued
643 635
644 636 Parameters
645 637 ----------
646 638 cell : string
647 639 Python input code, which can be multiline.
648 640
649 641 Returns
650 642 -------
651 643 status : str
652 644 One of 'complete', 'incomplete', or 'invalid' if source is not a
653 645 prefix of valid code.
654 646 indent_spaces : int or None
655 647 The number of spaces by which to indent the next line of code. If
656 648 status is not 'incomplete', this is None.
657 649 """
658 650 # Remember if the lines ends in a new line.
659 651 ends_with_newline = False
660 652 for character in reversed(cell):
661 653 if character == '\n':
662 654 ends_with_newline = True
663 655 break
664 656 elif character.strip():
665 657 break
666 658 else:
667 659 continue
668 660
669 661 if not ends_with_newline:
670 662 # Append an newline for consistent tokenization
671 663 # See https://bugs.python.org/issue33899
672 664 cell += '\n'
673 665
674 666 lines = cell.splitlines(keepends=True)
675 667
676 668 if not lines:
677 669 return 'complete', None
678 670
679 671 if lines[-1].endswith('\\'):
680 672 # Explicit backslash continuation
681 673 return 'incomplete', find_last_indent(lines)
682 674
683 675 try:
684 676 for transform in self.cleanup_transforms:
685 677 if not getattr(transform, 'has_side_effects', False):
686 678 lines = transform(lines)
687 679 except SyntaxError:
688 680 return 'invalid', None
689 681
690 682 if lines[0].startswith('%%'):
691 683 # Special case for cell magics - completion marked by blank line
692 684 if lines[-1].strip():
693 685 return 'incomplete', find_last_indent(lines)
694 686 else:
695 687 return 'complete', None
696 688
697 689 try:
698 690 for transform in self.line_transforms:
699 691 if not getattr(transform, 'has_side_effects', False):
700 692 lines = transform(lines)
701 693 lines = self.do_token_transforms(lines)
702 694 except SyntaxError:
703 695 return 'invalid', None
704 696
705 697 tokens_by_line = make_tokens_by_line(lines)
706 698
707 699 # Bail if we got one line and there are more closing parentheses than
708 700 # the opening ones
709 701 if (
710 702 len(lines) == 1
711 703 and tokens_by_line
712 704 and has_sunken_brackets(tokens_by_line[0])
713 705 ):
714 706 return "invalid", None
715 707
716 708 if not tokens_by_line:
717 709 return 'incomplete', find_last_indent(lines)
718 710
719 711 if tokens_by_line[-1][-1].type != tokenize.ENDMARKER:
720 712 # We're in a multiline string or expression
721 713 return 'incomplete', find_last_indent(lines)
722 714
723 715 newline_types = {tokenize.NEWLINE, tokenize.COMMENT, tokenize.ENDMARKER} # type: ignore
724 716
725 717 # Pop the last line which only contains DEDENTs and ENDMARKER
726 718 last_token_line = None
727 719 if {t.type for t in tokens_by_line[-1]} in [
728 720 {tokenize.DEDENT, tokenize.ENDMARKER},
729 721 {tokenize.ENDMARKER}
730 722 ] and len(tokens_by_line) > 1:
731 723 last_token_line = tokens_by_line.pop()
732 724
733 725 while tokens_by_line[-1] and tokens_by_line[-1][-1].type in newline_types:
734 726 tokens_by_line[-1].pop()
735 727
736 728 if not tokens_by_line[-1]:
737 729 return 'incomplete', find_last_indent(lines)
738 730
739 731 if tokens_by_line[-1][-1].string == ':':
740 732 # The last line starts a block (e.g. 'if foo:')
741 733 ix = 0
742 734 while tokens_by_line[-1][ix].type in {tokenize.INDENT, tokenize.DEDENT}:
743 735 ix += 1
744 736
745 737 indent = tokens_by_line[-1][ix].start[1]
746 738 return 'incomplete', indent + 4
747 739
748 740 if tokens_by_line[-1][0].line.endswith('\\'):
749 741 return 'incomplete', None
750 742
751 743 # At this point, our checks think the code is complete (or invalid).
752 744 # We'll use codeop.compile_command to check this with the real parser
753 745 try:
754 746 with warnings.catch_warnings():
755 747 warnings.simplefilter('error', SyntaxWarning)
756 748 res = compile_command(''.join(lines), symbol='exec')
757 749 except (SyntaxError, OverflowError, ValueError, TypeError,
758 750 MemoryError, SyntaxWarning):
759 751 return 'invalid', None
760 752 else:
761 753 if res is None:
762 754 return 'incomplete', find_last_indent(lines)
763 755
764 756 if last_token_line and last_token_line[0].type == tokenize.DEDENT:
765 757 if ends_with_newline:
766 758 return 'complete', None
767 759 return 'incomplete', find_last_indent(lines)
768 760
769 761 # If there's a blank line at the end, assume we're ready to execute
770 762 if not lines[-1].strip():
771 763 return 'complete', None
772 764
773 765 return 'complete', None
774 766
775 767
776 768 def find_last_indent(lines):
777 769 m = _indent_re.match(lines[-1])
778 770 if not m:
779 771 return 0
780 772 return len(m.group(0).replace('\t', ' '*4))
781 773
782 774
783 775 class MaybeAsyncCompile(Compile):
784 776 def __init__(self, extra_flags=0):
785 777 super().__init__()
786 778 self.flags |= extra_flags
787 779
788 780
789 781 class MaybeAsyncCommandCompiler(CommandCompiler):
790 782 def __init__(self, extra_flags=0):
791 783 self.compiler = MaybeAsyncCompile(extra_flags=extra_flags)
792 784
793 785
794 786 _extra_flags = ast.PyCF_ALLOW_TOP_LEVEL_AWAIT
795 787
796 788 compile_command = MaybeAsyncCommandCompiler(extra_flags=_extra_flags)
@@ -1,484 +1,469 b''
1 1 import tokenize
2 2
3 3 from IPython.testing import tools as tt
4 4
5 5 from IPython.core import inputtransformer as ipt
6 6
7 7 def transform_and_reset(transformer):
8 8 transformer = transformer()
9 9 def transform(inp):
10 10 try:
11 11 return transformer.push(inp)
12 12 finally:
13 13 transformer.reset()
14 14
15 15 return transform
16 16
17 17 # Transformer tests
18 18 def transform_checker(tests, transformer, **kwargs):
19 19 """Utility to loop over test inputs"""
20 20 transformer = transformer(**kwargs)
21 21 try:
22 22 for inp, tr in tests:
23 23 if inp is None:
24 24 out = transformer.reset()
25 25 else:
26 26 out = transformer.push(inp)
27 27 assert out == tr
28 28 finally:
29 29 transformer.reset()
30 30
31 31 # Data for all the syntax tests in the form of lists of pairs of
32 32 # raw/transformed input. We store it here as a global dict so that we can use
33 33 # it both within single-function tests and also to validate the behavior of the
34 34 # larger objects
35 35
36 36 syntax = \
37 37 dict(assign_system =
38 38 [('a =! ls', "a = get_ipython().getoutput('ls')"),
39 39 ('b = !ls', "b = get_ipython().getoutput('ls')"),
40 40 ('c= !ls', "c = get_ipython().getoutput('ls')"),
41 41 ('d == !ls', 'd == !ls'), # Invalid syntax, but we leave == alone.
42 42 ('x=1', 'x=1'), # normal input is unmodified
43 43 (' ',' '), # blank lines are kept intact
44 44 # Tuple unpacking
45 45 ("a, b = !echo 'a\\nb'", "a, b = get_ipython().getoutput(\"echo 'a\\\\nb'\")"),
46 46 ("a,= !echo 'a'", "a, = get_ipython().getoutput(\"echo 'a'\")"),
47 47 ("a, *bc = !echo 'a\\nb\\nc'", "a, *bc = get_ipython().getoutput(\"echo 'a\\\\nb\\\\nc'\")"),
48 48 # Tuple unpacking with regular Python expressions, not our syntax.
49 49 ("a, b = range(2)", "a, b = range(2)"),
50 50 ("a, = range(1)", "a, = range(1)"),
51 51 ("a, *bc = range(3)", "a, *bc = range(3)"),
52 52 ],
53 53
54 54 assign_magic =
55 55 [('a =% who', "a = get_ipython().run_line_magic('who', '')"),
56 56 ('b = %who', "b = get_ipython().run_line_magic('who', '')"),
57 57 ('c= %ls', "c = get_ipython().run_line_magic('ls', '')"),
58 58 ('d == %ls', 'd == %ls'), # Invalid syntax, but we leave == alone.
59 59 ('x=1', 'x=1'), # normal input is unmodified
60 60 (' ',' '), # blank lines are kept intact
61 61 ("a, b = %foo", "a, b = get_ipython().run_line_magic('foo', '')"),
62 ],
63
64 classic_prompt =
65 [('>>> x=1', 'x=1'),
66 ('x=1', 'x=1'), # normal input is unmodified
67 (' ', ' '), # blank lines are kept intact
68 ],
69
70 ipy_prompt =
71 [('In [1]: x=1', 'x=1'),
72 ('x=1', 'x=1'), # normal input is unmodified
73 (' ',' '), # blank lines are kept intact
74 ],
75
76 # Tests for the escape transformer to leave normal code alone
77 escaped_noesc =
78 [ (' ', ' '),
79 ('x=1', 'x=1'),
80 ],
81
82 # System calls
83 escaped_shell =
84 [ ('!ls', "get_ipython().system('ls')"),
85 # Double-escape shell, this means to capture the output of the
86 # subprocess and return it
87 ('!!ls', "get_ipython().getoutput('ls')"),
88 ],
89
90 # Help/object info
91 escaped_help =
92 [ ('?', 'get_ipython().show_usage()'),
93 ('?x1', "get_ipython().run_line_magic('pinfo', 'x1')"),
94 ('??x2', "get_ipython().run_line_magic('pinfo2', 'x2')"),
95 ('?a.*s', "get_ipython().run_line_magic('psearch', 'a.*s')"),
96 ('?%hist1', "get_ipython().run_line_magic('pinfo', '%hist1')"),
97 ('?%%hist2', "get_ipython().run_line_magic('pinfo', '%%hist2')"),
98 ('?abc = qwe', "get_ipython().run_line_magic('pinfo', 'abc')"),
99 ],
100
101 end_help =
102 [ ('x3?', "get_ipython().run_line_magic('pinfo', 'x3')"),
103 ('x4??', "get_ipython().run_line_magic('pinfo2', 'x4')"),
104 ('%hist1?', "get_ipython().run_line_magic('pinfo', '%hist1')"),
105 ('%hist2??', "get_ipython().run_line_magic('pinfo2', '%hist2')"),
106 ('%%hist3?', "get_ipython().run_line_magic('pinfo', '%%hist3')"),
107 ('%%hist4??', "get_ipython().run_line_magic('pinfo2', '%%hist4')"),
108 ('Ο€.foo?', "get_ipython().run_line_magic('pinfo', 'Ο€.foo')"),
109 ('f*?', "get_ipython().run_line_magic('psearch', 'f*')"),
110 ('ax.*aspe*?', "get_ipython().run_line_magic('psearch', 'ax.*aspe*')"),
111 ('a = abc?', "get_ipython().set_next_input('a = abc');"
112 "get_ipython().run_line_magic('pinfo', 'abc')"),
113 ('a = abc.qe??', "get_ipython().set_next_input('a = abc.qe');"
114 "get_ipython().run_line_magic('pinfo2', 'abc.qe')"),
115 ('a = *.items?', "get_ipython().set_next_input('a = *.items');"
116 "get_ipython().run_line_magic('psearch', '*.items')"),
117 ('plot(a?', "get_ipython().set_next_input('plot(a');"
118 "get_ipython().run_line_magic('pinfo', 'a')"),
119 ('a*2 #comment?', 'a*2 #comment?'),
120 ],
121
122 # Explicit magic calls
123 escaped_magic =
124 [ ('%cd', "get_ipython().run_line_magic('cd', '')"),
125 ('%cd /home', "get_ipython().run_line_magic('cd', '/home')"),
126 # Backslashes need to be escaped.
127 ('%cd C:\\User', "get_ipython().run_line_magic('cd', 'C:\\\\User')"),
128 (' %magic', " get_ipython().run_line_magic('magic', '')"),
129 ],
130
131 # Quoting with separate arguments
132 escaped_quote =
133 [ (',f', 'f("")'),
134 (',f x', 'f("x")'),
135 (' ,f y', ' f("y")'),
136 (',f a b', 'f("a", "b")'),
137 ],
138
139 # Quoting with single argument
140 escaped_quote2 =
141 [ (';f', 'f("")'),
142 (';f x', 'f("x")'),
143 (' ;f y', ' f("y")'),
144 (';f a b', 'f("a b")'),
145 ],
146
147 # Simply apply parens
148 escaped_paren =
149 [ ('/f', 'f()'),
150 ('/f x', 'f(x)'),
151 (' /f y', ' f(y)'),
152 ('/f a b', 'f(a, b)'),
153 ],
154
155 # Check that we transform prompts before other transforms
156 mixed =
157 [ ('In [1]: %lsmagic', "get_ipython().run_line_magic('lsmagic', '')"),
158 ('>>> %lsmagic', "get_ipython().run_line_magic('lsmagic', '')"),
159 ('In [2]: !ls', "get_ipython().system('ls')"),
160 ('In [3]: abs?', "get_ipython().run_line_magic('pinfo', 'abs')"),
161 ('In [4]: b = %who', "b = get_ipython().run_line_magic('who', '')"),
162 ],
163 )
62 ],
63 classic_prompt=[
64 (">>> x=1", "x=1"),
65 ("x=1", "x=1"), # normal input is unmodified
66 (" ", " "), # blank lines are kept intact
67 ],
68 ipy_prompt=[
69 ("In [1]: x=1", "x=1"),
70 ("x=1", "x=1"), # normal input is unmodified
71 (" ", " "), # blank lines are kept intact
72 ],
73 # Tests for the escape transformer to leave normal code alone
74 escaped_noesc=[
75 (" ", " "),
76 ("x=1", "x=1"),
77 ],
78 # System calls
79 escaped_shell=[
80 ("!ls", "get_ipython().system('ls')"),
81 # Double-escape shell, this means to capture the output of the
82 # subprocess and return it
83 ("!!ls", "get_ipython().getoutput('ls')"),
84 ],
85 # Help/object info
86 escaped_help=[
87 ("?", "get_ipython().show_usage()"),
88 ("?x1", "get_ipython().run_line_magic('pinfo', 'x1')"),
89 ("??x2", "get_ipython().run_line_magic('pinfo2', 'x2')"),
90 ("?a.*s", "get_ipython().run_line_magic('psearch', 'a.*s')"),
91 ("?%hist1", "get_ipython().run_line_magic('pinfo', '%hist1')"),
92 ("?%%hist2", "get_ipython().run_line_magic('pinfo', '%%hist2')"),
93 ("?abc = qwe", "get_ipython().run_line_magic('pinfo', 'abc')"),
94 ],
95 end_help=[
96 ("x3?", "get_ipython().run_line_magic('pinfo', 'x3')"),
97 ("x4??", "get_ipython().run_line_magic('pinfo2', 'x4')"),
98 ("%hist1?", "get_ipython().run_line_magic('pinfo', '%hist1')"),
99 ("%hist2??", "get_ipython().run_line_magic('pinfo2', '%hist2')"),
100 ("%%hist3?", "get_ipython().run_line_magic('pinfo', '%%hist3')"),
101 ("%%hist4??", "get_ipython().run_line_magic('pinfo2', '%%hist4')"),
102 ("Ο€.foo?", "get_ipython().run_line_magic('pinfo', 'Ο€.foo')"),
103 ("f*?", "get_ipython().run_line_magic('psearch', 'f*')"),
104 ("ax.*aspe*?", "get_ipython().run_line_magic('psearch', 'ax.*aspe*')"),
105 ("a = abc?", "get_ipython().run_line_magic('pinfo', 'abc')"),
106 ("a = abc.qe??", "get_ipython().run_line_magic('pinfo2', 'abc.qe')"),
107 ("a = *.items?", "get_ipython().run_line_magic('psearch', '*.items')"),
108 ("plot(a?", "get_ipython().run_line_magic('pinfo', 'a')"),
109 ("a*2 #comment?", "a*2 #comment?"),
110 ],
111 # Explicit magic calls
112 escaped_magic=[
113 ("%cd", "get_ipython().run_line_magic('cd', '')"),
114 ("%cd /home", "get_ipython().run_line_magic('cd', '/home')"),
115 # Backslashes need to be escaped.
116 ("%cd C:\\User", "get_ipython().run_line_magic('cd', 'C:\\\\User')"),
117 (" %magic", " get_ipython().run_line_magic('magic', '')"),
118 ],
119 # Quoting with separate arguments
120 escaped_quote=[
121 (",f", 'f("")'),
122 (",f x", 'f("x")'),
123 (" ,f y", ' f("y")'),
124 (",f a b", 'f("a", "b")'),
125 ],
126 # Quoting with single argument
127 escaped_quote2=[
128 (";f", 'f("")'),
129 (";f x", 'f("x")'),
130 (" ;f y", ' f("y")'),
131 (";f a b", 'f("a b")'),
132 ],
133 # Simply apply parens
134 escaped_paren=[
135 ("/f", "f()"),
136 ("/f x", "f(x)"),
137 (" /f y", " f(y)"),
138 ("/f a b", "f(a, b)"),
139 ],
140 # Check that we transform prompts before other transforms
141 mixed=[
142 ("In [1]: %lsmagic", "get_ipython().run_line_magic('lsmagic', '')"),
143 (">>> %lsmagic", "get_ipython().run_line_magic('lsmagic', '')"),
144 ("In [2]: !ls", "get_ipython().system('ls')"),
145 ("In [3]: abs?", "get_ipython().run_line_magic('pinfo', 'abs')"),
146 ("In [4]: b = %who", "b = get_ipython().run_line_magic('who', '')"),
147 ],
148 )
164 149
165 150 # multiline syntax examples. Each of these should be a list of lists, with
166 151 # each entry itself having pairs of raw/transformed input. The union (with
167 152 # '\n'.join() of the transformed inputs is what the splitter should produce
168 153 # when fed the raw lines one at a time via push.
169 154 syntax_ml = \
170 155 dict(classic_prompt =
171 156 [ [('>>> for i in range(10):','for i in range(10):'),
172 157 ('... print i',' print i'),
173 158 ('... ', ''),
174 159 ],
175 160 [('>>> a="""','a="""'),
176 161 ('... 123"""','123"""'),
177 162 ],
178 163 [('a="""','a="""'),
179 164 ('... 123','123'),
180 165 ('... 456"""','456"""'),
181 166 ],
182 167 [('a="""','a="""'),
183 168 ('>>> 123','123'),
184 169 ('... 456"""','456"""'),
185 170 ],
186 171 [('a="""','a="""'),
187 172 ('123','123'),
188 173 ('... 456"""','... 456"""'),
189 174 ],
190 175 [('....__class__','....__class__'),
191 176 ],
192 177 [('a=5', 'a=5'),
193 178 ('...', ''),
194 179 ],
195 180 [('>>> def f(x):', 'def f(x):'),
196 181 ('...', ''),
197 182 ('... return x', ' return x'),
198 183 ],
199 184 [('board = """....', 'board = """....'),
200 185 ('....', '....'),
201 186 ('...."""', '...."""'),
202 187 ],
203 188 ],
204 189
205 190 ipy_prompt =
206 191 [ [('In [24]: for i in range(10):','for i in range(10):'),
207 192 (' ....: print i',' print i'),
208 193 (' ....: ', ''),
209 194 ],
210 195 [('In [24]: for i in range(10):','for i in range(10):'),
211 196 # Qt console prompts expand with spaces, not dots
212 197 (' ...: print i',' print i'),
213 198 (' ...: ', ''),
214 199 ],
215 200 [('In [24]: for i in range(10):','for i in range(10):'),
216 201 # Sometimes whitespace preceding '...' has been removed
217 202 ('...: print i',' print i'),
218 203 ('...: ', ''),
219 204 ],
220 205 [('In [24]: for i in range(10):','for i in range(10):'),
221 206 # Space after last continuation prompt has been removed (issue #6674)
222 207 ('...: print i',' print i'),
223 208 ('...:', ''),
224 209 ],
225 210 [('In [2]: a="""','a="""'),
226 211 (' ...: 123"""','123"""'),
227 212 ],
228 213 [('a="""','a="""'),
229 214 (' ...: 123','123'),
230 215 (' ...: 456"""','456"""'),
231 216 ],
232 217 [('a="""','a="""'),
233 218 ('In [1]: 123','123'),
234 219 (' ...: 456"""','456"""'),
235 220 ],
236 221 [('a="""','a="""'),
237 222 ('123','123'),
238 223 (' ...: 456"""',' ...: 456"""'),
239 224 ],
240 225 ],
241 226
242 227 multiline_datastructure_prompt =
243 228 [ [('>>> a = [1,','a = [1,'),
244 229 ('... 2]','2]'),
245 230 ],
246 231 ],
247 232
248 233 multiline_datastructure =
249 234 [ [('b = ("%s"', None),
250 235 ('# comment', None),
251 236 ('%foo )', 'b = ("%s"\n# comment\n%foo )'),
252 237 ],
253 238 ],
254 239
255 240 multiline_string =
256 241 [ [("'''foo?", None),
257 242 ("bar'''", "'''foo?\nbar'''"),
258 243 ],
259 244 ],
260 245
261 246 leading_indent =
262 247 [ [(' print "hi"','print "hi"'),
263 248 ],
264 249 [(' for a in range(5):','for a in range(5):'),
265 250 (' a*2',' a*2'),
266 251 ],
267 252 [(' a="""','a="""'),
268 253 (' 123"""','123"""'),
269 254 ],
270 255 [('a="""','a="""'),
271 256 (' 123"""',' 123"""'),
272 257 ],
273 258 ],
274 259
275 260 cellmagic =
276 261 [ [('%%foo a', None),
277 262 (None, "get_ipython().run_cell_magic('foo', 'a', '')"),
278 263 ],
279 264 [('%%bar 123', None),
280 265 ('hello', None),
281 266 (None , "get_ipython().run_cell_magic('bar', '123', 'hello')"),
282 267 ],
283 268 [('a=5', 'a=5'),
284 269 ('%%cellmagic', '%%cellmagic'),
285 270 ],
286 271 ],
287 272
288 273 escaped =
289 274 [ [('%abc def \\', None),
290 275 ('ghi', "get_ipython().run_line_magic('abc', 'def ghi')"),
291 276 ],
292 277 [('%abc def \\', None),
293 278 ('ghi\\', None),
294 279 (None, "get_ipython().run_line_magic('abc', 'def ghi')"),
295 280 ],
296 281 ],
297 282
298 283 assign_magic =
299 284 [ [('a = %bc de \\', None),
300 285 ('fg', "a = get_ipython().run_line_magic('bc', 'de fg')"),
301 286 ],
302 287 [('a = %bc de \\', None),
303 288 ('fg\\', None),
304 289 (None, "a = get_ipython().run_line_magic('bc', 'de fg')"),
305 290 ],
306 291 ],
307 292
308 293 assign_system =
309 294 [ [('a = !bc de \\', None),
310 295 ('fg', "a = get_ipython().getoutput('bc de fg')"),
311 296 ],
312 297 [('a = !bc de \\', None),
313 298 ('fg\\', None),
314 299 (None, "a = get_ipython().getoutput('bc de fg')"),
315 300 ],
316 301 ],
317 302 )
318 303
319 304
320 305 def test_assign_system():
321 306 tt.check_pairs(transform_and_reset(ipt.assign_from_system), syntax['assign_system'])
322 307
323 308 def test_assign_magic():
324 309 tt.check_pairs(transform_and_reset(ipt.assign_from_magic), syntax['assign_magic'])
325 310
326 311 def test_classic_prompt():
327 312 tt.check_pairs(transform_and_reset(ipt.classic_prompt), syntax['classic_prompt'])
328 313 for example in syntax_ml['classic_prompt']:
329 314 transform_checker(example, ipt.classic_prompt)
330 315 for example in syntax_ml['multiline_datastructure_prompt']:
331 316 transform_checker(example, ipt.classic_prompt)
332 317
333 318 # Check that we don't transform the second line if the first is obviously
334 319 # IPython syntax
335 320 transform_checker([
336 321 ('%foo', '%foo'),
337 322 ('>>> bar', '>>> bar'),
338 323 ], ipt.classic_prompt)
339 324
340 325
341 326 def test_ipy_prompt():
342 327 tt.check_pairs(transform_and_reset(ipt.ipy_prompt), syntax['ipy_prompt'])
343 328 for example in syntax_ml['ipy_prompt']:
344 329 transform_checker(example, ipt.ipy_prompt)
345 330
346 331 # Check that we don't transform the second line if we're inside a cell magic
347 332 transform_checker([
348 333 ('%%foo', '%%foo'),
349 334 ('In [1]: bar', 'In [1]: bar'),
350 335 ], ipt.ipy_prompt)
351 336
352 337 def test_assemble_logical_lines():
353 338 tests = \
354 339 [ [("a = \\", None),
355 340 ("123", "a = 123"),
356 341 ],
357 342 [("a = \\", None), # Test resetting when within a multi-line string
358 343 ("12 *\\", None),
359 344 (None, "a = 12 *"),
360 345 ],
361 346 [("# foo\\", "# foo\\"), # Comments can't be continued like this
362 347 ],
363 348 ]
364 349 for example in tests:
365 350 transform_checker(example, ipt.assemble_logical_lines)
366 351
367 352 def test_assemble_python_lines():
368 353 tests = \
369 354 [ [("a = '''", None),
370 355 ("abc'''", "a = '''\nabc'''"),
371 356 ],
372 357 [("a = '''", None), # Test resetting when within a multi-line string
373 358 ("def", None),
374 359 (None, "a = '''\ndef"),
375 360 ],
376 361 [("a = [1,", None),
377 362 ("2]", "a = [1,\n2]"),
378 363 ],
379 364 [("a = [1,", None), # Test resetting when within a multi-line string
380 365 ("2,", None),
381 366 (None, "a = [1,\n2,"),
382 367 ],
383 368 [("a = '''", None), # Test line continuation within a multi-line string
384 369 ("abc\\", None),
385 370 ("def", None),
386 371 ("'''", "a = '''\nabc\\\ndef\n'''"),
387 372 ],
388 373 ] + syntax_ml['multiline_datastructure']
389 374 for example in tests:
390 375 transform_checker(example, ipt.assemble_python_lines)
391 376
392 377
393 378 def test_help_end():
394 379 tt.check_pairs(transform_and_reset(ipt.help_end), syntax['end_help'])
395 380
396 381 def test_escaped_noesc():
397 382 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_noesc'])
398 383
399 384
400 385 def test_escaped_shell():
401 386 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_shell'])
402 387
403 388
404 389 def test_escaped_help():
405 390 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_help'])
406 391
407 392
408 393 def test_escaped_magic():
409 394 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_magic'])
410 395
411 396
412 397 def test_escaped_quote():
413 398 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_quote'])
414 399
415 400
416 401 def test_escaped_quote2():
417 402 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_quote2'])
418 403
419 404
420 405 def test_escaped_paren():
421 406 tt.check_pairs(transform_and_reset(ipt.escaped_commands), syntax['escaped_paren'])
422 407
423 408
424 409 def test_cellmagic():
425 410 for example in syntax_ml['cellmagic']:
426 411 transform_checker(example, ipt.cellmagic)
427 412
428 413 line_example = [('%%bar 123', None),
429 414 ('hello', None),
430 415 ('' , "get_ipython().run_cell_magic('bar', '123', 'hello')"),
431 416 ]
432 417 transform_checker(line_example, ipt.cellmagic, end_on_blank_line=True)
433 418
434 419 def test_has_comment():
435 420 tests = [('text', False),
436 421 ('text #comment', True),
437 422 ('text #comment\n', True),
438 423 ('#comment', True),
439 424 ('#comment\n', True),
440 425 ('a = "#string"', False),
441 426 ('a = "#string" # comment', True),
442 427 ('a #comment not "string"', True),
443 428 ]
444 429 tt.check_pairs(ipt.has_comment, tests)
445 430
446 431 @ipt.TokenInputTransformer.wrap
447 432 def decistmt(tokens):
448 433 """Substitute Decimals for floats in a string of statements.
449 434
450 435 Based on an example from the tokenize module docs.
451 436 """
452 437 result = []
453 438 for toknum, tokval, _, _, _ in tokens:
454 439 if toknum == tokenize.NUMBER and '.' in tokval: # replace NUMBER tokens
455 440 yield from [
456 441 (tokenize.NAME, 'Decimal'),
457 442 (tokenize.OP, '('),
458 443 (tokenize.STRING, repr(tokval)),
459 444 (tokenize.OP, ')')
460 445 ]
461 446 else:
462 447 yield (toknum, tokval)
463 448
464 449
465 450
466 451 def test_token_input_transformer():
467 452 tests = [('1.2', "Decimal ('1.2')"),
468 453 ('"1.2"', '"1.2"'),
469 454 ]
470 455 tt.check_pairs(transform_and_reset(decistmt), tests)
471 456 ml_tests = \
472 457 [ [("a = 1.2; b = '''x", None),
473 458 ("y'''", "a =Decimal ('1.2');b ='''x\ny'''"),
474 459 ],
475 460 [("a = [1.2,", None),
476 461 ("3]", "a =[Decimal ('1.2'),\n3 ]"),
477 462 ],
478 463 [("a = '''foo", None), # Test resetting when within a multi-line string
479 464 ("bar", None),
480 465 (None, "a = '''foo\nbar"),
481 466 ],
482 467 ]
483 468 for example in ml_tests:
484 469 transform_checker(example, decistmt)
@@ -1,405 +1,442 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 platform
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
17 MULTILINE_MAGIC = ("""\
17 MULTILINE_MAGIC = (
18 """\
18 19 a = f()
19 20 %foo \\
20 21 bar
21 22 g()
22 """.splitlines(keepends=True), (2, 0), """\
23 """.splitlines(
24 keepends=True
25 ),
26 (2, 0),
27 """\
23 28 a = f()
24 29 get_ipython().run_line_magic('foo', ' bar')
25 30 g()
26 """.splitlines(keepends=True))
31 """.splitlines(
32 keepends=True
33 ),
34 )
27 35
28 INDENTED_MAGIC = ("""\
36 INDENTED_MAGIC = (
37 """\
29 38 for a in range(5):
30 39 %ls
31 """.splitlines(keepends=True), (2, 4), """\
40 """.splitlines(
41 keepends=True
42 ),
43 (2, 4),
44 """\
32 45 for a in range(5):
33 46 get_ipython().run_line_magic('ls', '')
34 """.splitlines(keepends=True))
47 """.splitlines(
48 keepends=True
49 ),
50 )
35 51
36 CRLF_MAGIC = ([
37 "a = f()\n",
38 "%ls\r\n",
39 "g()\n"
40 ], (2, 0), [
41 "a = f()\n",
42 "get_ipython().run_line_magic('ls', '')\n",
43 "g()\n"
44 ])
45
46 MULTILINE_MAGIC_ASSIGN = ("""\
52 CRLF_MAGIC = (
53 ["a = f()\n", "%ls\r\n", "g()\n"],
54 (2, 0),
55 ["a = f()\n", "get_ipython().run_line_magic('ls', '')\n", "g()\n"],
56 )
57
58 MULTILINE_MAGIC_ASSIGN = (
59 """\
47 60 a = f()
48 61 b = %foo \\
49 62 bar
50 63 g()
51 """.splitlines(keepends=True), (2, 4), """\
64 """.splitlines(
65 keepends=True
66 ),
67 (2, 4),
68 """\
52 69 a = f()
53 70 b = get_ipython().run_line_magic('foo', ' bar')
54 71 g()
55 """.splitlines(keepends=True))
72 """.splitlines(
73 keepends=True
74 ),
75 )
56 76
57 77 MULTILINE_SYSTEM_ASSIGN = ("""\
58 78 a = f()
59 79 b = !foo \\
60 80 bar
61 81 g()
62 82 """.splitlines(keepends=True), (2, 4), """\
63 83 a = f()
64 84 b = get_ipython().getoutput('foo bar')
65 85 g()
66 86 """.splitlines(keepends=True))
67 87
68 88 #####
69 89
70 90 MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT = ("""\
71 91 def test():
72 92 for i in range(1):
73 93 print(i)
74 94 res =! ls
75 """.splitlines(keepends=True), (4, 7), '''\
95 """.splitlines(
96 keepends=True
97 ),
98 (4, 7),
99 """\
76 100 def test():
77 101 for i in range(1):
78 102 print(i)
79 103 res =get_ipython().getoutput(\' ls\')
80 '''.splitlines(keepends=True))
104 """.splitlines(
105 keepends=True
106 ),
107 )
81 108
82 109 ######
83 110
84 AUTOCALL_QUOTE = (
85 [",f 1 2 3\n"], (1, 0),
86 ['f("1", "2", "3")\n']
87 )
111 AUTOCALL_QUOTE = ([",f 1 2 3\n"], (1, 0), ['f("1", "2", "3")\n'])
88 112
89 AUTOCALL_QUOTE2 = (
90 [";f 1 2 3\n"], (1, 0),
91 ['f("1 2 3")\n']
92 )
113 AUTOCALL_QUOTE2 = ([";f 1 2 3\n"], (1, 0), ['f("1 2 3")\n'])
93 114
94 AUTOCALL_PAREN = (
95 ["/f 1 2 3\n"], (1, 0),
96 ['f(1, 2, 3)\n']
97 )
115 AUTOCALL_PAREN = (["/f 1 2 3\n"], (1, 0), ["f(1, 2, 3)\n"])
98 116
99 SIMPLE_HELP = (
100 ["foo?\n"], (1, 0),
101 ["get_ipython().run_line_magic('pinfo', 'foo')\n"]
102 )
117 SIMPLE_HELP = (["foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', 'foo')\n"])
103 118
104 119 DETAILED_HELP = (
105 ["foo??\n"], (1, 0),
106 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"]
120 ["foo??\n"],
121 (1, 0),
122 ["get_ipython().run_line_magic('pinfo2', 'foo')\n"],
107 123 )
108 124
109 MAGIC_HELP = (
110 ["%foo?\n"], (1, 0),
111 ["get_ipython().run_line_magic('pinfo', '%foo')\n"]
112 )
125 MAGIC_HELP = (["%foo?\n"], (1, 0), ["get_ipython().run_line_magic('pinfo', '%foo')\n"])
113 126
114 127 HELP_IN_EXPR = (
115 ["a = b + c?\n"], (1, 0),
116 ["get_ipython().set_next_input('a = b + c');"
117 "get_ipython().run_line_magic('pinfo', 'c')\n"]
128 ["a = b + c?\n"],
129 (1, 0),
130 ["get_ipython().run_line_magic('pinfo', 'c')\n"],
118 131 )
119 132
120 HELP_CONTINUED_LINE = ("""\
133 HELP_CONTINUED_LINE = (
134 """\
121 135 a = \\
122 136 zip?
123 """.splitlines(keepends=True), (1, 0),
124 [r"get_ipython().set_next_input('a = \\\nzip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
137 """.splitlines(
138 keepends=True
139 ),
140 (1, 0),
141 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
125 142 )
126 143
127 HELP_MULTILINE = ("""\
144 HELP_MULTILINE = (
145 """\
128 146 (a,
129 147 b) = zip?
130 """.splitlines(keepends=True), (1, 0),
131 [r"get_ipython().set_next_input('(a,\nb) = zip');get_ipython().run_line_magic('pinfo', 'zip')" + "\n"]
148 """.splitlines(
149 keepends=True
150 ),
151 (1, 0),
152 [r"get_ipython().run_line_magic('pinfo', 'zip')" + "\n"],
132 153 )
133 154
134 155 HELP_UNICODE = (
135 ["Ο€.foo?\n"], (1, 0),
136 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"]
156 ["Ο€.foo?\n"],
157 (1, 0),
158 ["get_ipython().run_line_magic('pinfo', 'Ο€.foo')\n"],
137 159 )
138 160
139 161
140 162 def null_cleanup_transformer(lines):
141 163 """
142 164 A cleanup transform that returns an empty list.
143 165 """
144 166 return []
145 167
146 168
147 169 def test_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 assert make_tokens_by_line(c)[-1] != []
154 177 for k in printable:
155 178 assert make_tokens_by_line(c + k)[-1] != []
156 179
157 180
158 181 def check_find(transformer, case, match=True):
159 sample, expected_start, _ = case
182 sample, expected_start, _ = case
160 183 tbl = make_tokens_by_line(sample)
161 184 res = transformer.find(tbl)
162 185 if match:
163 186 # start_line is stored 0-indexed, expected values are 1-indexed
164 187 assert (res.start_line + 1, res.start_col) == expected_start
165 188 return res
166 189 else:
167 190 assert res is None
168 191
192
169 193 def check_transform(transformer_cls, case):
170 194 lines, start, expected = case
171 195 transformer = transformer_cls(start)
172 196 assert transformer.transform(lines) == expected
173 197
198
174 199 def test_continued_line():
175 200 lines = MULTILINE_MAGIC_ASSIGN[0]
176 201 assert ipt2.find_end_of_continued_line(lines, 1) == 2
177 202
178 203 assert ipt2.assemble_continued_line(lines, (1, 5), 2) == "foo bar"
179 204
205
180 206 def test_find_assign_magic():
181 207 check_find(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
182 208 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN, match=False)
183 209 check_find(ipt2.MagicAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT, match=False)
184 210
211
185 212 def test_transform_assign_magic():
186 213 check_transform(ipt2.MagicAssign, MULTILINE_MAGIC_ASSIGN)
187 214
215
188 216 def test_find_assign_system():
189 217 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
190 218 check_find(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
191 219 check_find(ipt2.SystemAssign, (["a = !ls\n"], (1, 5), None))
192 220 check_find(ipt2.SystemAssign, (["a=!ls\n"], (1, 2), None))
193 221 check_find(ipt2.SystemAssign, MULTILINE_MAGIC_ASSIGN, match=False)
194 222
223
195 224 def test_transform_assign_system():
196 225 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN)
197 226 check_transform(ipt2.SystemAssign, MULTILINE_SYSTEM_ASSIGN_AFTER_DEDENT)
198 227
228
199 229 def test_find_magic_escape():
200 230 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC)
201 231 check_find(ipt2.EscapedCommand, INDENTED_MAGIC)
202 232 check_find(ipt2.EscapedCommand, MULTILINE_MAGIC_ASSIGN, match=False)
203 233
234
204 235 def test_transform_magic_escape():
205 236 check_transform(ipt2.EscapedCommand, MULTILINE_MAGIC)
206 237 check_transform(ipt2.EscapedCommand, INDENTED_MAGIC)
207 238 check_transform(ipt2.EscapedCommand, CRLF_MAGIC)
208 239
240
209 241 def test_find_autocalls():
210 242 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
211 243 print("Testing %r" % case[0])
212 244 check_find(ipt2.EscapedCommand, case)
213 245
246
214 247 def test_transform_autocall():
215 248 for case in [AUTOCALL_QUOTE, AUTOCALL_QUOTE2, AUTOCALL_PAREN]:
216 249 print("Testing %r" % case[0])
217 250 check_transform(ipt2.EscapedCommand, case)
218 251
252
219 253 def test_find_help():
220 254 for case in [SIMPLE_HELP, DETAILED_HELP, MAGIC_HELP, HELP_IN_EXPR]:
221 255 check_find(ipt2.HelpEnd, case)
222 256
223 257 tf = check_find(ipt2.HelpEnd, HELP_CONTINUED_LINE)
224 258 assert tf.q_line == 1
225 259 assert tf.q_col == 3
226 260
227 261 tf = check_find(ipt2.HelpEnd, HELP_MULTILINE)
228 262 assert tf.q_line == 1
229 263 assert tf.q_col == 8
230 264
231 265 # ? in a comment does not trigger help
232 266 check_find(ipt2.HelpEnd, (["foo # bar?\n"], None, None), match=False)
233 267 # Nor in a string
234 268 check_find(ipt2.HelpEnd, (["foo = '''bar?\n"], None, None), match=False)
235 269
270
236 271 def test_transform_help():
237 272 tf = ipt2.HelpEnd((1, 0), (1, 9))
238 273 assert tf.transform(HELP_IN_EXPR[0]) == HELP_IN_EXPR[2]
239 274
240 275 tf = ipt2.HelpEnd((1, 0), (2, 3))
241 276 assert tf.transform(HELP_CONTINUED_LINE[0]) == HELP_CONTINUED_LINE[2]
242 277
243 278 tf = ipt2.HelpEnd((1, 0), (2, 8))
244 279 assert tf.transform(HELP_MULTILINE[0]) == HELP_MULTILINE[2]
245 280
246 281 tf = ipt2.HelpEnd((1, 0), (1, 0))
247 282 assert tf.transform(HELP_UNICODE[0]) == HELP_UNICODE[2]
248 283
284
249 285 def test_find_assign_op_dedent():
250 286 """
251 287 be careful that empty token like dedent are not counted as parens
252 288 """
289
253 290 class Tk:
254 291 def __init__(self, s):
255 292 self.string = s
256 293
257 294 assert _find_assign_op([Tk(s) for s in ("", "a", "=", "b")]) == 2
258 295 assert (
259 296 _find_assign_op([Tk(s) for s in ("", "(", "a", "=", "b", ")", "=", "5")]) == 6
260 297 )
261 298
262 299
263 300 examples = [
264 301 pytest.param("a = 1", "complete", None),
265 302 pytest.param("for a in range(5):", "incomplete", 4),
266 303 pytest.param("for a in range(5):\n if a > 0:", "incomplete", 8),
267 304 pytest.param("raise = 2", "invalid", None),
268 305 pytest.param("a = [1,\n2,", "incomplete", 0),
269 306 pytest.param("(\n))", "incomplete", 0),
270 307 pytest.param("\\\r\n", "incomplete", 0),
271 308 pytest.param("a = '''\n hi", "incomplete", 3),
272 309 pytest.param("def a():\n x=1\n global x", "invalid", None),
273 310 pytest.param(
274 311 "a \\ ",
275 312 "invalid",
276 313 None,
277 314 marks=pytest.mark.xfail(
278 315 reason="Bug in python 3.9.8 – bpo 45738",
279 316 condition=sys.version_info
280 317 in [(3, 9, 8, "final", 0), (3, 11, 0, "alpha", 2)],
281 318 raises=SystemError,
282 319 strict=True,
283 320 ),
284 321 ), # Nothing allowed after backslash,
285 322 pytest.param("1\\\n+2", "complete", None),
286 323 ]
287 324
288 325
289 326 @pytest.mark.parametrize("code, expected, number", examples)
290 327 def test_check_complete_param(code, expected, number):
291 328 cc = ipt2.TransformerManager().check_complete
292 329 assert cc(code) == (expected, number)
293 330
294 331
295 332 @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
296 333 @pytest.mark.xfail(
297 334 reason="Bug in python 3.9.8 – bpo 45738",
298 335 condition=sys.version_info in [(3, 9, 8, "final", 0), (3, 11, 0, "alpha", 2)],
299 336 raises=SystemError,
300 337 strict=True,
301 338 )
302 339 def test_check_complete():
303 340 cc = ipt2.TransformerManager().check_complete
304 341
305 example = dedent("""
342 example = dedent(
343 """
306 344 if True:
307 a=1""" )
345 a=1"""
346 )
308 347
309 348 assert cc(example) == ("incomplete", 4)
310 349 assert cc(example + "\n") == ("complete", None)
311 350 assert cc(example + "\n ") == ("complete", None)
312 351
313 352 # no need to loop on all the letters/numbers.
314 short = '12abAB'+string.printable[62:]
353 short = "12abAB" + string.printable[62:]
315 354 for c in short:
316 355 # test does not raise:
317 356 cc(c)
318 357 for k in short:
319 cc(c+k)
358 cc(c + k)
320 359
321 360 assert cc("def f():\n x=0\n \\\n ") == ("incomplete", 2)
322 361
323 362
324 363 @pytest.mark.xfail(platform.python_implementation() == "PyPy", reason="fail on pypy")
325 364 @pytest.mark.parametrize(
326 365 "value, expected",
327 366 [
328 367 ('''def foo():\n """''', ("incomplete", 4)),
329 368 ("""async with example:\n pass""", ("incomplete", 4)),
330 369 ("""async with example:\n pass\n """, ("complete", None)),
331 370 ],
332 371 )
333 372 def test_check_complete_II(value, expected):
334 373 """
335 374 Test that multiple line strings are properly handled.
336 375
337 376 Separate test function for convenience
338 377
339 378 """
340 379 cc = ipt2.TransformerManager().check_complete
341 380 assert cc(value) == expected
342 381
343 382
344 383 @pytest.mark.parametrize(
345 384 "value, expected",
346 385 [
347 386 (")", ("invalid", None)),
348 387 ("]", ("invalid", None)),
349 388 ("}", ("invalid", None)),
350 389 (")(", ("invalid", None)),
351 390 ("][", ("invalid", None)),
352 391 ("}{", ("invalid", None)),
353 392 ("]()(", ("invalid", None)),
354 393 ("())(", ("invalid", None)),
355 394 (")[](", ("invalid", None)),
356 395 ("()](", ("invalid", None)),
357 396 ],
358 397 )
359 398 def test_check_complete_invalidates_sunken_brackets(value, expected):
360 399 """
361 400 Test that a single line with more closing brackets than the opening ones is
362 401 interpreted as invalid
363 402 """
364 403 cc = ipt2.TransformerManager().check_complete
365 404 assert cc(value) == expected
366 405
367 406
368 407 def test_null_cleanup_transformer():
369 408 manager = ipt2.TransformerManager()
370 409 manager.cleanup_transforms.insert(0, null_cleanup_transformer)
371 410 assert manager.transform_cell("") == ""
372 411
373 412
374
375
376 413 def test_side_effects_I():
377 414 count = 0
415
378 416 def counter(lines):
379 417 nonlocal count
380 418 count += 1
381 419 return lines
382 420
383 421 counter.has_side_effects = True
384 422
385 423 manager = ipt2.TransformerManager()
386 424 manager.cleanup_transforms.insert(0, counter)
387 assert manager.check_complete("a=1\n") == ('complete', None)
425 assert manager.check_complete("a=1\n") == ("complete", None)
388 426 assert count == 0
389 427
390 428
391
392
393 429 def test_side_effects_II():
394 430 count = 0
431
395 432 def counter(lines):
396 433 nonlocal count
397 434 count += 1
398 435 return lines
399 436
400 437 counter.has_side_effects = True
401 438
402 439 manager = ipt2.TransformerManager()
403 440 manager.line_transforms.insert(0, counter)
404 assert manager.check_complete("b=1\n") == ('complete', None)
441 assert manager.check_complete("b=1\n") == ("complete", None)
405 442 assert count == 0
@@ -1,1059 +1,1064 b''
1 1 ============
2 2 8.x Series
3 3 ============
4 4
5 5
6 6 .. _version 8.3.0:
7 7
8 8 IPython 8.3.0
9 9 -------------
10 10
11 - :ghpull:`13625`, using ``?``, ``??``, ``*?`` will not call
12 ``set_next_input`` as most frontend allow proper multiline editing and it was
13 causing issues for many users of multi-cell frontends.
14
15
11 16 - :ghpull:`13600`, ``pre_run_*``-hooks will now have a ``cell_id`` attribute on
12 17 the info object when frontend provide it.
13 18
14 19 .. _version 8.2.0:
15 20
16 21 IPython 8.2.0
17 22 -------------
18 23
19 24 IPython 8.2 mostly bring bugfixes to IPython.
20 25
21 26 - Auto-suggestion can now be elected with the ``end`` key. :ghpull:`13566`
22 27 - Some traceback issues with ``assert etb is not None`` have been fixed. :ghpull:`13588`
23 28 - History is now pulled from the sqitel database and not from in-memory.
24 29 In particular when using the ``%paste`` magic, the content of the pasted text will
25 30 be part of the history and not the verbatim text ``%paste`` anymore. :ghpull:`13592`
26 31 - Fix ``Ctrl-\\`` exit cleanup :ghpull:`13603`
27 32 - Fixes to ``ultratb`` ipdb support when used outside of IPython. :ghpull:`13498`
28 33
29 34
30 35 I am still trying to fix and investigate :ghissue:`13598`, which seem to be
31 36 random, and would appreciate help if you find reproducible minimal case. I've
32 37 tried to make various changes to the codebase to mitigate it, but a proper fix
33 38 will be difficult without understanding the cause.
34 39
35 40
36 41 All the issues on pull-requests for this release can be found in the `8.2
37 42 milestone. <https://github.com/ipython/ipython/milestone/100>`__ . And some
38 43 documentation only PR can be found as part of the `7.33 milestone
39 44 <https://github.com/ipython/ipython/milestone/101>`__ (currently not released).
40 45
41 46 Thanks to the `D. E. Shaw group <https://deshaw.com/>`__ for sponsoring
42 47 work on IPython and related libraries.
43 48
44 49 .. _version 8.1.1:
45 50
46 51 IPython 8.1.1
47 52 -------------
48 53
49 54 Fix an issue with virtualenv and Python 3.8 introduced in 8.1
50 55
51 56 Revert :ghpull:`13537` (fix an issue with symlinks in virtualenv) that raises an
52 57 error in Python 3.8, and fixed in a different way in :ghpull:`13559`.
53 58
54 59 .. _version 8.1:
55 60
56 61 IPython 8.1.0
57 62 -------------
58 63
59 64 IPython 8.1 is the first minor release after 8.0 and fixes a number of bugs and
60 65 Update a few behavior that were problematic with the 8.0 as with many new major
61 66 release.
62 67
63 68 Note that beyond the changes listed here, IPython 8.1.0 also contains all the
64 69 features listed in :ref:`version 7.32`.
65 70
66 71 - Misc and multiple fixes around quotation auto-closing. It is now disabled by
67 72 default. Run with ``TerminalInteractiveShell.auto_match=True`` to re-enabled
68 73 - Require pygments>=2.4.0 :ghpull:`13459`, this was implicit in the code, but
69 74 is now explicit in ``setup.cfg``/``setup.py``
70 75 - Docs improvement of ``core.magic_arguments`` examples. :ghpull:`13433`
71 76 - Multi-line edit executes too early with await. :ghpull:`13424`
72 77
73 78 - ``black`` is back as an optional dependency, and autoformatting disabled by
74 79 default until some fixes are implemented (black improperly reformat magics).
75 80 :ghpull:`13471` Additionally the ability to use ``yapf`` as a code
76 81 reformatter has been added :ghpull:`13528` . You can use
77 82 ``TerminalInteractiveShell.autoformatter="black"``,
78 83 ``TerminalInteractiveShell.autoformatter="yapf"`` to re-enable auto formating
79 84 with black, or switch to yapf.
80 85
81 86 - Fix and issue where ``display`` was not defined.
82 87
83 88 - Auto suggestions are now configurable. Currently only
84 89 ``AutoSuggestFromHistory`` (default) and ``None``. new provider contribution
85 90 welcomed. :ghpull:`13475`
86 91
87 92 - multiple packaging/testing improvement to simplify downstream packaging
88 93 (xfail with reasons, try to not access network...).
89 94
90 95 - Update deprecation. ``InteractiveShell.magic`` internal method has been
91 96 deprecated for many years but did not emit a warning until now.
92 97
93 98 - internal ``appended_to_syspath`` context manager has been deprecated.
94 99
95 100 - fix an issue with symlinks in virtualenv :ghpull:`13537` (Reverted in 8.1.1)
96 101
97 102 - Fix an issue with vim mode, where cursor would not be reset on exit :ghpull:`13472`
98 103
99 104 - ipython directive now remove only known pseudo-decorators :ghpull:`13532`
100 105
101 106 - ``IPython/lib/security`` which used to be used for jupyter notebook has been
102 107 removed.
103 108
104 109 - Fix an issue where ``async with`` would execute on new lines. :ghpull:`13436`
105 110
106 111
107 112 We want to remind users that IPython is part of the Jupyter organisations, and
108 113 thus governed by a Code of Conduct. Some of the behavior we have seen on GitHub is not acceptable.
109 114 Abuse and non-respectful comments on discussion will not be tolerated.
110 115
111 116 Many thanks to all the contributors to this release, many of the above fixed issue and
112 117 new features where done by first time contributors, showing there is still
113 118 plenty of easy contribution possible in IPython
114 119 . You can find all individual contributions
115 120 to this milestone `on github <https://github.com/ipython/ipython/milestone/91>`__.
116 121
117 122 Thanks as well to the `D. E. Shaw group <https://deshaw.com/>`__ for sponsoring
118 123 work on IPython and related libraries. In particular the Lazy autoloading of
119 124 magics that you will find described in the 7.32 release notes.
120 125
121 126
122 127 .. _version 8.0.1:
123 128
124 129 IPython 8.0.1 (CVE-2022-21699)
125 130 ------------------------------
126 131
127 132 IPython 8.0.1, 7.31.1 and 5.11 are security releases that change some default
128 133 values in order to prevent potential Execution with Unnecessary Privileges.
129 134
130 135 Almost all version of IPython looks for configuration and profiles in current
131 136 working directory. Since IPython was developed before pip and environments
132 137 existed it was used a convenient way to load code/packages in a project
133 138 dependant way.
134 139
135 140 In 2022, it is not necessary anymore, and can lead to confusing behavior where
136 141 for example cloning a repository and starting IPython or loading a notebook from
137 142 any Jupyter-Compatible interface that has ipython set as a kernel can lead to
138 143 code execution.
139 144
140 145
141 146 I did not find any standard way for packaged to advertise CVEs they fix, I'm
142 147 thus trying to add a ``__patched_cves__`` attribute to the IPython module that
143 148 list the CVEs that should have been fixed. This attribute is informational only
144 149 as if a executable has a flaw, this value can always be changed by an attacker.
145 150
146 151 .. code::
147 152
148 153 In [1]: import IPython
149 154
150 155 In [2]: IPython.__patched_cves__
151 156 Out[2]: {'CVE-2022-21699'}
152 157
153 158 In [3]: 'CVE-2022-21699' in IPython.__patched_cves__
154 159 Out[3]: True
155 160
156 161 Thus starting with this version:
157 162
158 163 - The current working directory is not searched anymore for profiles or
159 164 configurations files.
160 165 - Added a ``__patched_cves__`` attribute (set of strings) to IPython module that contain
161 166 the list of fixed CVE. This is informational only.
162 167
163 168 Further details can be read on the `GitHub Advisory <https://github.com/ipython/ipython/security/advisories/GHSA-pq7m-3gw7-gq5x>`__
164 169
165 170
166 171 .. _version 8.0:
167 172
168 173 IPython 8.0
169 174 -----------
170 175
171 176 IPython 8.0 is bringing a large number of new features and improvements to both the
172 177 user of the terminal and of the kernel via Jupyter. The removal of compatibility
173 178 with older version of Python is also the opportunity to do a couple of
174 179 performance improvements in particular with respect to startup time.
175 180 The 8.x branch started diverging from its predecessor around IPython 7.12
176 181 (January 2020).
177 182
178 183 This release contains 250+ pull requests, in addition to many of the features
179 184 and backports that have made it to the 7.x branch. Please see the
180 185 `8.0 milestone <https://github.com/ipython/ipython/milestone/73?closed=1>`__ for the full list of pull requests.
181 186
182 187 Please feel free to send pull requests to updates those notes after release,
183 188 I have likely forgotten a few things reviewing 250+ PRs.
184 189
185 190 Dependencies changes/downstream packaging
186 191 -----------------------------------------
187 192
188 193 Most of our building steps have been changed to be (mostly) declarative
189 194 and follow PEP 517. We are trying to completely remove ``setup.py`` (:ghpull:`13238`) and are
190 195 looking for help to do so.
191 196
192 197 - minimum supported ``traitlets`` version is now 5+
193 198 - we now require ``stack_data``
194 199 - minimal Python is now 3.8
195 200 - ``nose`` is not a testing requirement anymore
196 201 - ``pytest`` replaces nose.
197 202 - ``iptest``/``iptest3`` cli entrypoints do not exists anymore.
198 203 - minimum officially support ``numpy`` version has been bumped, but this should
199 204 not have much effect on packaging.
200 205
201 206
202 207 Deprecation and removal
203 208 -----------------------
204 209
205 210 We removed almost all features, arguments, functions, and modules that were
206 211 marked as deprecated between IPython 1.0 and 5.0. As a reminder, 5.0 was released
207 212 in 2016, and 1.0 in 2013. Last release of the 5 branch was 5.10.0, in May 2020.
208 213 The few remaining deprecated features we left have better deprecation warnings
209 214 or have been turned into explicit errors for better error messages.
210 215
211 216 I will use this occasion to add the following requests to anyone emitting a
212 217 deprecation warning:
213 218
214 219 - Please add at least ``stacklevel=2`` so that the warning is emitted into the
215 220 caller context, and not the callee one.
216 221 - Please add **since which version** something is deprecated.
217 222
218 223 As a side note, it is much easier to conditionally compare version
219 224 numbers rather than using ``try/except`` when functionality changes with a version.
220 225
221 226 I won't list all the removed features here, but modules like ``IPython.kernel``,
222 227 which was just a shim module around ``ipykernel`` for the past 8 years, have been
223 228 removed, and so many other similar things that pre-date the name **Jupyter**
224 229 itself.
225 230
226 231 We no longer need to add ``IPython.extensions`` to the PYTHONPATH because that is being
227 232 handled by ``load_extension``.
228 233
229 234 We are also removing ``Cythonmagic``, ``sympyprinting`` and ``rmagic`` as they are now in
230 235 other packages and no longer need to be inside IPython.
231 236
232 237
233 238 Documentation
234 239 -------------
235 240
236 241 The majority of our docstrings have now been reformatted and automatically fixed by
237 242 the experimental `VΓ©lin <https://pypi.org/project/velin/>`_ project to conform
238 243 to numpydoc.
239 244
240 245 Type annotations
241 246 ----------------
242 247
243 248 While IPython itself is highly dynamic and can't be completely typed, many of
244 249 the functions now have type annotations, and part of the codebase is now checked
245 250 by mypy.
246 251
247 252
248 253 Featured changes
249 254 ----------------
250 255
251 256 Here is a features list of changes in IPython 8.0. This is of course non-exhaustive.
252 257 Please note as well that many features have been added in the 7.x branch as well
253 258 (and hence why you want to read the 7.x what's new notes), in particular
254 259 features contributed by QuantStack (with respect to debugger protocol and Xeus
255 260 Python), as well as many debugger features that I was pleased to implement as
256 261 part of my work at QuanSight and sponsored by DE Shaw.
257 262
258 263 Traceback improvements
259 264 ~~~~~~~~~~~~~~~~~~~~~~
260 265
261 266 Previously, error tracebacks for errors happening in code cells were showing a
262 267 hash, the one used for compiling the Python AST::
263 268
264 269 In [1]: def foo():
265 270 ...: return 3 / 0
266 271 ...:
267 272
268 273 In [2]: foo()
269 274 ---------------------------------------------------------------------------
270 275 ZeroDivisionError Traceback (most recent call last)
271 276 <ipython-input-2-c19b6d9633cf> in <module>
272 277 ----> 1 foo()
273 278
274 279 <ipython-input-1-1595a74c32d5> in foo()
275 280 1 def foo():
276 281 ----> 2 return 3 / 0
277 282 3
278 283
279 284 ZeroDivisionError: division by zero
280 285
281 286 The error traceback is now correctly formatted, showing the cell number in which the error happened::
282 287
283 288 In [1]: def foo():
284 289 ...: return 3 / 0
285 290 ...:
286 291
287 292 Input In [2]: foo()
288 293 ---------------------------------------------------------------------------
289 294 ZeroDivisionError Traceback (most recent call last)
290 295 input In [2], in <module>
291 296 ----> 1 foo()
292 297
293 298 Input In [1], in foo()
294 299 1 def foo():
295 300 ----> 2 return 3 / 0
296 301
297 302 ZeroDivisionError: division by zero
298 303
299 304 The ``stack_data`` package has been integrated, which provides smarter information in the traceback;
300 305 in particular it will highlight the AST node where an error occurs which can help to quickly narrow down errors.
301 306
302 307 For example in the following snippet::
303 308
304 309 def foo(i):
305 310 x = [[[0]]]
306 311 return x[0][i][0]
307 312
308 313
309 314 def bar():
310 315 return foo(0) + foo(
311 316 1
312 317 ) + foo(2)
313 318
314 319
315 320 calling ``bar()`` would raise an ``IndexError`` on the return line of ``foo``,
316 321 and IPython 8.0 is capable of telling you where the index error occurs::
317 322
318 323
319 324 IndexError
320 325 Input In [2], in <module>
321 326 ----> 1 bar()
322 327 ^^^^^
323 328
324 329 Input In [1], in bar()
325 330 6 def bar():
326 331 ----> 7 return foo(0) + foo(
327 332 ^^^^
328 333 8 1
329 334 ^^^^^^^^
330 335 9 ) + foo(2)
331 336 ^^^^
332 337
333 338 Input In [1], in foo(i)
334 339 1 def foo(i):
335 340 2 x = [[[0]]]
336 341 ----> 3 return x[0][i][0]
337 342 ^^^^^^^
338 343
339 344 The corresponding locations marked here with ``^`` will show up highlighted in
340 345 the terminal and notebooks.
341 346
342 347 Finally, a colon ``::`` and line number is appended after a filename in
343 348 traceback::
344 349
345 350
346 351 ZeroDivisionError Traceback (most recent call last)
347 352 File ~/error.py:4, in <module>
348 353 1 def f():
349 354 2 1/0
350 355 ----> 4 f()
351 356
352 357 File ~/error.py:2, in f()
353 358 1 def f():
354 359 ----> 2 1/0
355 360
356 361 Many terminals and editors have integrations enabling you to directly jump to the
357 362 relevant file/line when this syntax is used, so this small addition may have a high
358 363 impact on productivity.
359 364
360 365
361 366 Autosuggestions
362 367 ~~~~~~~~~~~~~~~
363 368
364 369 Autosuggestion is a very useful feature available in `fish <https://fishshell.com/>`__, `zsh <https://en.wikipedia.org/wiki/Z_shell>`__, and `prompt-toolkit <https://python-prompt-toolkit.readthedocs.io/en/master/pages/asking_for_input.html#auto-suggestion>`__.
365 370
366 371 `Ptpython <https://github.com/prompt-toolkit/ptpython#ptpython>`__ allows users to enable this feature in
367 372 `ptpython/config.py <https://github.com/prompt-toolkit/ptpython/blob/master/examples/ptpython_config/config.py#L90>`__.
368 373
369 374 This feature allows users to accept autosuggestions with ctrl e, ctrl f,
370 375 or right arrow as described below.
371 376
372 377 1. Start ipython
373 378
374 379 .. image:: ../_images/8.0/auto_suggest_1_prompt_no_text.png
375 380
376 381 2. Run ``print("hello")``
377 382
378 383 .. image:: ../_images/8.0/auto_suggest_2_print_hello_suggest.png
379 384
380 385 3. start typing ``print`` again to see the autosuggestion
381 386
382 387 .. image:: ../_images/8.0/auto_suggest_3_print_hello_suggest.png
383 388
384 389 4. Press ``ctrl-f``, or ``ctrl-e``, or ``right-arrow`` to accept the suggestion
385 390
386 391 .. image:: ../_images/8.0/auto_suggest_4_print_hello.png
387 392
388 393 You can also complete word by word:
389 394
390 395 1. Run ``def say_hello(): print("hello")``
391 396
392 397 .. image:: ../_images/8.0/auto_suggest_second_prompt.png
393 398
394 399 2. Start typing the first letter if ``def`` to see the autosuggestion
395 400
396 401 .. image:: ../_images/8.0/auto_suggest_d_phantom.png
397 402
398 403 3. Press ``alt-f`` (or ``escape`` followed by ``f``), to accept the first word of the suggestion
399 404
400 405 .. image:: ../_images/8.0/auto_suggest_def_phantom.png
401 406
402 407 Importantly, this feature does not interfere with tab completion:
403 408
404 409 1. After running ``def say_hello(): print("hello")``, press d
405 410
406 411 .. image:: ../_images/8.0/auto_suggest_d_phantom.png
407 412
408 413 2. Press Tab to start tab completion
409 414
410 415 .. image:: ../_images/8.0/auto_suggest_d_completions.png
411 416
412 417 3A. Press Tab again to select the first option
413 418
414 419 .. image:: ../_images/8.0/auto_suggest_def_completions.png
415 420
416 421 3B. Press ``alt f`` (``escape``, ``f``) to accept to accept the first word of the suggestion
417 422
418 423 .. image:: ../_images/8.0/auto_suggest_def_phantom.png
419 424
420 425 3C. Press ``ctrl-f`` or ``ctrl-e`` to accept the entire suggestion
421 426
422 427 .. image:: ../_images/8.0/auto_suggest_match_parens.png
423 428
424 429
425 430 Currently, autosuggestions are only shown in the emacs or vi insert editing modes:
426 431
427 432 - The ctrl e, ctrl f, and alt f shortcuts work by default in emacs mode.
428 433 - To use these shortcuts in vi insert mode, you will have to create `custom keybindings in your config.py <https://github.com/mskar/setup/commit/2892fcee46f9f80ef7788f0749edc99daccc52f4/>`__.
429 434
430 435
431 436 Show pinfo information in ipdb using "?" and "??"
432 437 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
433 438
434 439 In IPDB, it is now possible to show the information about an object using "?"
435 440 and "??", in much the same way that it can be done when using the IPython prompt::
436 441
437 442 ipdb> partial?
438 443 Init signature: partial(self, /, *args, **kwargs)
439 444 Docstring:
440 445 partial(func, *args, **keywords) - new function with partial application
441 446 of the given arguments and keywords.
442 447 File: ~/.pyenv/versions/3.8.6/lib/python3.8/functools.py
443 448 Type: type
444 449 Subclasses:
445 450
446 451 Previously, ``pinfo`` or ``pinfo2`` command had to be used for this purpose.
447 452
448 453
449 454 Autoreload 3 feature
450 455 ~~~~~~~~~~~~~~~~~~~~
451 456
452 457 Example: When an IPython session is run with the 'autoreload' extension loaded,
453 458 you will now have the option '3' to select, which means the following:
454 459
455 460 1. replicate all functionality from option 2
456 461 2. autoload all new funcs/classes/enums/globals from the module when they are added
457 462 3. autoload all newly imported funcs/classes/enums/globals from external modules
458 463
459 464 Try ``%autoreload 3`` in an IPython session after running ``%load_ext autoreload``.
460 465
461 466 For more information please see the following unit test : ``extensions/tests/test_autoreload.py:test_autoload_newly_added_objects``
462 467
463 468 Auto formatting with black in the CLI
464 469 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
465 470
466 471 This feature was present in 7.x, but disabled by default.
467 472
468 473 In 8.0, input was automatically reformatted with Black when black was installed.
469 474 This feature has been reverted for the time being.
470 475 You can re-enable it by setting ``TerminalInteractiveShell.autoformatter`` to ``"black"``
471 476
472 477 History Range Glob feature
473 478 ~~~~~~~~~~~~~~~~~~~~~~~~~~
474 479
475 480 Previously, when using ``%history``, users could specify either
476 481 a range of sessions and lines, for example:
477 482
478 483 .. code-block:: python
479 484
480 485 ~8/1-~6/5 # see history from the first line of 8 sessions ago,
481 486 # to the fifth line of 6 sessions ago.``
482 487
483 488 Or users could specify a glob pattern:
484 489
485 490 .. code-block:: python
486 491
487 492 -g <pattern> # glob ALL history for the specified pattern.
488 493
489 494 However users could *not* specify both.
490 495
491 496 If a user *did* specify both a range and a glob pattern,
492 497 then the glob pattern would be used (globbing *all* history) *and the range would be ignored*.
493 498
494 499 With this enhancement, if a user specifies both a range and a glob pattern, then the glob pattern will be applied to the specified range of history.
495 500
496 501 Don't start a multi-line cell with sunken parenthesis
497 502 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
498 503
499 504 From now on, IPython will not ask for the next line of input when given a single
500 505 line with more closing than opening brackets. For example, this means that if
501 506 you (mis)type ``]]`` instead of ``[]``, a ``SyntaxError`` will show up, instead of
502 507 the ``...:`` prompt continuation.
503 508
504 509 IPython shell for ipdb interact
505 510 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
506 511
507 512 The ipdb ``interact`` starts an IPython shell instead of Python's built-in ``code.interact()``.
508 513
509 514 Automatic Vi prompt stripping
510 515 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
511 516
512 517 When pasting code into IPython, it will strip the leading prompt characters if
513 518 there are any. For example, you can paste the following code into the console -
514 519 it will still work, even though each line is prefixed with prompts (`In`,
515 520 `Out`)::
516 521
517 522 In [1]: 2 * 2 == 4
518 523 Out[1]: True
519 524
520 525 In [2]: print("This still works as pasted")
521 526
522 527
523 528 Previously, this was not the case for the Vi-mode prompts::
524 529
525 530 In [1]: [ins] In [13]: 2 * 2 == 4
526 531 ...: Out[13]: True
527 532 ...:
528 533 File "<ipython-input-1-727bb88eaf33>", line 1
529 534 [ins] In [13]: 2 * 2 == 4
530 535 ^
531 536 SyntaxError: invalid syntax
532 537
533 538 This is now fixed, and Vi prompt prefixes - ``[ins]`` and ``[nav]`` - are
534 539 skipped just as the normal ``In`` would be.
535 540
536 541 IPython shell can be started in the Vi mode using ``ipython --TerminalInteractiveShell.editing_mode=vi``,
537 542 You should be able to change mode dynamically with ``%config TerminalInteractiveShell.editing_mode='vi'``
538 543
539 544 Empty History Ranges
540 545 ~~~~~~~~~~~~~~~~~~~~
541 546
542 547 A number of magics that take history ranges can now be used with an empty
543 548 range. These magics are:
544 549
545 550 * ``%save``
546 551 * ``%load``
547 552 * ``%pastebin``
548 553 * ``%pycat``
549 554
550 555 Using them this way will make them take the history of the current session up
551 556 to the point of the magic call (such that the magic itself will not be
552 557 included).
553 558
554 559 Therefore it is now possible to save the whole history to a file using
555 560 ``%save <filename>``, load and edit it using ``%load`` (makes for a nice usage
556 561 when followed with :kbd:`F2`), send it to `dpaste.org <http://dpast.org>`_ using
557 562 ``%pastebin``, or view the whole thing syntax-highlighted with a single
558 563 ``%pycat``.
559 564
560 565
561 566 Windows timing implementation: Switch to process_time
562 567 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
563 568 Timing on Windows, for example with ``%%time``, was changed from being based on ``time.perf_counter``
564 569 (which counted time even when the process was sleeping) to being based on ``time.process_time`` instead
565 570 (which only counts CPU time). This brings it closer to the behavior on Linux. See :ghpull:`12984`.
566 571
567 572 Miscellaneous
568 573 ~~~~~~~~~~~~~
569 574 - Non-text formatters are not disabled in the terminal, which should simplify
570 575 writing extensions displaying images or other mimetypes in supporting terminals.
571 576 :ghpull:`12315`
572 577 - It is now possible to automatically insert matching brackets in Terminal IPython using the
573 578 ``TerminalInteractiveShell.auto_match=True`` option. :ghpull:`12586`
574 579 - We are thinking of deprecating the current ``%%javascript`` magic in favor of a better replacement. See :ghpull:`13376`.
575 580 - ``~`` is now expanded when part of a path in most magics :ghpull:`13385`
576 581 - ``%/%%timeit`` magic now adds a comma every thousands to make reading a long number easier :ghpull:`13379`
577 582 - ``"info"`` messages can now be customised to hide some fields :ghpull:`13343`
578 583 - ``collections.UserList`` now pretty-prints :ghpull:`13320`
579 584 - The debugger now has a persistent history, which should make it less
580 585 annoying to retype commands :ghpull:`13246`
581 586 - ``!pip`` ``!conda`` ``!cd`` or ``!ls`` are likely doing the wrong thing. We
582 587 now warn users if they use one of those commands. :ghpull:`12954`
583 588 - Make ``%precision`` work for ``numpy.float64`` type :ghpull:`12902`
584 589
585 590 Re-added support for XDG config directories
586 591 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
587 592
588 593 XDG support through the years comes and goes. There is a tension between having
589 594 an identical location for configuration in all platforms versus having simple instructions.
590 595 After initial failures a couple of years ago, IPython was modified to automatically migrate XDG
591 596 config files back into ``~/.ipython``. That migration code has now been removed.
592 597 IPython now checks the XDG locations, so if you _manually_ move your config
593 598 files to your preferred location, IPython will not move them back.
594 599
595 600
596 601 Preparing for Python 3.10
597 602 -------------------------
598 603
599 604 To prepare for Python 3.10, we have started working on removing reliance and
600 605 any dependency that is not compatible with Python 3.10. This includes migrating our
601 606 test suite to pytest and starting to remove nose. This also means that the
602 607 ``iptest`` command is now gone and all testing is via pytest.
603 608
604 609 This was in large part thanks to the NumFOCUS Small Developer grant, which enabled us to
605 610 allocate \$4000 to hire `Nikita Kniazev (@Kojoley) <https://github.com/Kojoley>`_,
606 611 who did a fantastic job at updating our code base, migrating to pytest, pushing
607 612 our coverage, and fixing a large number of bugs. I highly recommend contacting
608 613 them if you need help with C++ and Python projects.
609 614
610 615 You can find all relevant issues and PRs with the SDG 2021 tag `<https://github.com/ipython/ipython/issues?q=label%3A%22Numfocus+SDG+2021%22+>`__
611 616
612 617 Removing support for older Python versions
613 618 ------------------------------------------
614 619
615 620
616 621 We are removing support for Python up through 3.7, allowing internal code to use the more
617 622 efficient ``pathlib`` and to make better use of type annotations.
618 623
619 624 .. image:: ../_images/8.0/pathlib_pathlib_everywhere.jpg
620 625 :alt: "Meme image of Toy Story with Woody and Buzz, with the text 'pathlib, pathlib everywhere'"
621 626
622 627
623 628 We had about 34 PRs only to update some logic to update some functions from managing strings to
624 629 using Pathlib.
625 630
626 631 The completer has also seen significant updates and now makes use of newer Jedi APIs,
627 632 offering faster and more reliable tab completion.
628 633
629 634 Misc Statistics
630 635 ---------------
631 636
632 637 Here are some numbers::
633 638
634 639 7.x: 296 files, 12561 blank lines, 20282 comments, 35142 line of code.
635 640 8.0: 252 files, 12053 blank lines, 19232 comments, 34505 line of code.
636 641
637 642 $ git diff --stat 7.x...master | tail -1
638 643 340 files changed, 13399 insertions(+), 12421 deletions(-)
639 644
640 645 We have commits from 162 authors, who contributed 1916 commits in 23 month, excluding merges (to not bias toward
641 646 maintainers pushing buttons).::
642 647
643 648 $ git shortlog -s --no-merges 7.x...master | sort -nr
644 649 535 Matthias Bussonnier
645 650 86 Nikita Kniazev
646 651 69 Blazej Michalik
647 652 49 Samuel Gaist
648 653 27 Itamar Turner-Trauring
649 654 18 Spas Kalaydzhisyki
650 655 17 Thomas Kluyver
651 656 17 Quentin Peter
652 657 17 James Morris
653 658 17 Artur Svistunov
654 659 15 Bart Skowron
655 660 14 Alex Hall
656 661 13 rushabh-v
657 662 13 Terry Davis
658 663 13 Benjamin Ragan-Kelley
659 664 8 martinRenou
660 665 8 farisachugthai
661 666 7 dswij
662 667 7 Gal B
663 668 7 Corentin Cadiou
664 669 6 yuji96
665 670 6 Martin Skarzynski
666 671 6 Justin Palmer
667 672 6 Daniel Goldfarb
668 673 6 Ben Greiner
669 674 5 Sammy Al Hashemi
670 675 5 Paul Ivanov
671 676 5 Inception95
672 677 5 Eyenpi
673 678 5 Douglas Blank
674 679 5 Coco Mishra
675 680 5 Bibo Hao
676 681 5 AndrΓ© A. Gomes
677 682 5 Ahmed Fasih
678 683 4 takuya fujiwara
679 684 4 palewire
680 685 4 Thomas A Caswell
681 686 4 Talley Lambert
682 687 4 Scott Sanderson
683 688 4 Ram Rachum
684 689 4 Nick Muoh
685 690 4 Nathan Goldbaum
686 691 4 Mithil Poojary
687 692 4 Michael T
688 693 4 Jakub Klus
689 694 4 Ian Castleden
690 695 4 Eli Rykoff
691 696 4 Ashwin Vishnu
692 697 3 谭九鼎
693 698 3 sleeping
694 699 3 Sylvain Corlay
695 700 3 Peter Corke
696 701 3 Paul Bissex
697 702 3 Matthew Feickert
698 703 3 Fernando Perez
699 704 3 Eric Wieser
700 705 3 Daniel Mietchen
701 706 3 Aditya Sathe
702 707 3 007vedant
703 708 2 rchiodo
704 709 2 nicolaslazo
705 710 2 luttik
706 711 2 gorogoroumaru
707 712 2 foobarbyte
708 713 2 bar-hen
709 714 2 Theo Ouzhinski
710 715 2 Strawkage
711 716 2 Samreen Zarroug
712 717 2 Pete Blois
713 718 2 Meysam Azad
714 719 2 Matthieu Ancellin
715 720 2 Mark Schmitz
716 721 2 Maor Kleinberger
717 722 2 MRCWirtz
718 723 2 Lumir Balhar
719 724 2 Julien Rabinow
720 725 2 Juan Luis Cano RodrΓ­guez
721 726 2 Joyce Er
722 727 2 Jakub
723 728 2 Faris A Chugthai
724 729 2 Ethan Madden
725 730 2 Dimitri Papadopoulos
726 731 2 Diego Fernandez
727 732 2 Daniel Shimon
728 733 2 Coco Bennett
729 734 2 Carlos Cordoba
730 735 2 Boyuan Liu
731 736 2 BaoGiang HoangVu
732 737 2 Augusto
733 738 2 Arthur Svistunov
734 739 2 Arthur Moreira
735 740 2 Ali Nabipour
736 741 2 Adam Hackbarth
737 742 1 richard
738 743 1 linar-jether
739 744 1 lbennett
740 745 1 juacrumar
741 746 1 gpotter2
742 747 1 digitalvirtuoso
743 748 1 dalthviz
744 749 1 Yonatan Goldschmidt
745 750 1 Tomasz KΕ‚oczko
746 751 1 Tobias Bengfort
747 752 1 Timur Kushukov
748 753 1 Thomas
749 754 1 Snir Broshi
750 755 1 Shao Yang Hong
751 756 1 Sanjana-03
752 757 1 Romulo Filho
753 758 1 Rodolfo Carvalho
754 759 1 Richard Shadrach
755 760 1 Reilly Tucker Siemens
756 761 1 Rakessh Roshan
757 762 1 Piers Titus van der Torren
758 763 1 PhanatosZou
759 764 1 Pavel Safronov
760 765 1 Paulo S. Costa
761 766 1 Paul McCarthy
762 767 1 NotWearingPants
763 768 1 Naelson Douglas
764 769 1 Michael Tiemann
765 770 1 Matt Wozniski
766 771 1 Markus Wageringel
767 772 1 Marcus Wirtz
768 773 1 Marcio Mazza
769 774 1 LumΓ­r 'Frenzy' Balhar
770 775 1 Lightyagami1
771 776 1 Leon Anavi
772 777 1 LeafyLi
773 778 1 L0uisJ0shua
774 779 1 Kyle Cutler
775 780 1 Krzysztof Cybulski
776 781 1 Kevin Kirsche
777 782 1 KIU Shueng Chuan
778 783 1 Jonathan Slenders
779 784 1 Jay Qi
780 785 1 Jake VanderPlas
781 786 1 Iwan Briquemont
782 787 1 Hussaina Begum Nandyala
783 788 1 Gordon Ball
784 789 1 Gabriel Simonetto
785 790 1 Frank Tobia
786 791 1 Erik
787 792 1 Elliott Sales de Andrade
788 793 1 Daniel Hahler
789 794 1 Dan Green-Leipciger
790 795 1 Dan Green
791 796 1 Damian Yurzola
792 797 1 Coon, Ethan T
793 798 1 Carol Willing
794 799 1 Brian Lee
795 800 1 Brendan Gerrity
796 801 1 Blake Griffin
797 802 1 Bastian Ebeling
798 803 1 Bartosz Telenczuk
799 804 1 Ankitsingh6299
800 805 1 Andrew Port
801 806 1 Andrew J. Hesford
802 807 1 Albert Zhang
803 808 1 Adam Johnson
804 809
805 810 This does not, of course, represent non-code contributions, for which we are also grateful.
806 811
807 812
808 813 API Changes using Frappuccino
809 814 -----------------------------
810 815
811 816 This is an experimental exhaustive API difference using `Frappuccino <https://pypi.org/project/frappuccino/>`_
812 817
813 818
814 819 The following items are new in IPython 8.0 ::
815 820
816 821 + IPython.core.async_helpers.get_asyncio_loop()
817 822 + IPython.core.completer.Dict
818 823 + IPython.core.completer.Pattern
819 824 + IPython.core.completer.Sequence
820 825 + IPython.core.completer.__skip_doctest__
821 826 + IPython.core.debugger.Pdb.precmd(self, line)
822 827 + IPython.core.debugger.__skip_doctest__
823 828 + IPython.core.display.__getattr__(name)
824 829 + IPython.core.display.warn
825 830 + IPython.core.display_functions
826 831 + IPython.core.display_functions.DisplayHandle
827 832 + IPython.core.display_functions.DisplayHandle.display(self, obj, **kwargs)
828 833 + IPython.core.display_functions.DisplayHandle.update(self, obj, **kwargs)
829 834 + IPython.core.display_functions.__all__
830 835 + IPython.core.display_functions.__builtins__
831 836 + IPython.core.display_functions.__cached__
832 837 + IPython.core.display_functions.__doc__
833 838 + IPython.core.display_functions.__file__
834 839 + IPython.core.display_functions.__loader__
835 840 + IPython.core.display_functions.__name__
836 841 + IPython.core.display_functions.__package__
837 842 + IPython.core.display_functions.__spec__
838 843 + IPython.core.display_functions.b2a_hex
839 844 + IPython.core.display_functions.clear_output(wait=False)
840 845 + IPython.core.display_functions.display(*objs, include='None', exclude='None', metadata='None', transient='None', display_id='None', raw=False, clear=False, **kwargs)
841 846 + IPython.core.display_functions.publish_display_data(data, metadata='None', source='<deprecated>', *, transient='None', **kwargs)
842 847 + IPython.core.display_functions.update_display(obj, *, display_id, **kwargs)
843 848 + IPython.core.extensions.BUILTINS_EXTS
844 849 + IPython.core.inputtransformer2.has_sunken_brackets(tokens)
845 850 + IPython.core.interactiveshell.Callable
846 851 + IPython.core.interactiveshell.__annotations__
847 852 + IPython.core.ultratb.List
848 853 + IPython.core.ultratb.Tuple
849 854 + IPython.lib.pretty.CallExpression
850 855 + IPython.lib.pretty.CallExpression.factory(name)
851 856 + IPython.lib.pretty.RawStringLiteral
852 857 + IPython.lib.pretty.RawText
853 858 + IPython.terminal.debugger.TerminalPdb.do_interact(self, arg)
854 859 + IPython.terminal.embed.Set
855 860
856 861 The following items have been removed (or moved to superclass)::
857 862
858 863 - IPython.core.application.BaseIPythonApplication.initialize_subcommand
859 864 - IPython.core.completer.Sentinel
860 865 - IPython.core.completer.skip_doctest
861 866 - IPython.core.debugger.Tracer
862 867 - IPython.core.display.DisplayHandle
863 868 - IPython.core.display.DisplayHandle.display
864 869 - IPython.core.display.DisplayHandle.update
865 870 - IPython.core.display.b2a_hex
866 871 - IPython.core.display.clear_output
867 872 - IPython.core.display.display
868 873 - IPython.core.display.publish_display_data
869 874 - IPython.core.display.update_display
870 875 - IPython.core.excolors.Deprec
871 876 - IPython.core.excolors.ExceptionColors
872 877 - IPython.core.history.warn
873 878 - IPython.core.hooks.late_startup_hook
874 879 - IPython.core.hooks.pre_run_code_hook
875 880 - IPython.core.hooks.shutdown_hook
876 881 - IPython.core.interactiveshell.InteractiveShell.init_deprecation_warnings
877 882 - IPython.core.interactiveshell.InteractiveShell.init_readline
878 883 - IPython.core.interactiveshell.InteractiveShell.write
879 884 - IPython.core.interactiveshell.InteractiveShell.write_err
880 885 - IPython.core.interactiveshell.get_default_colors
881 886 - IPython.core.interactiveshell.removed_co_newlocals
882 887 - IPython.core.magics.execution.ExecutionMagics.profile_missing_notice
883 888 - IPython.core.magics.script.PIPE
884 889 - IPython.core.prefilter.PrefilterManager.init_transformers
885 890 - IPython.core.release.classifiers
886 891 - IPython.core.release.description
887 892 - IPython.core.release.keywords
888 893 - IPython.core.release.long_description
889 894 - IPython.core.release.name
890 895 - IPython.core.release.platforms
891 896 - IPython.core.release.url
892 897 - IPython.core.ultratb.VerboseTB.format_records
893 898 - IPython.core.ultratb.find_recursion
894 899 - IPython.core.ultratb.findsource
895 900 - IPython.core.ultratb.fix_frame_records_filenames
896 901 - IPython.core.ultratb.inspect_error
897 902 - IPython.core.ultratb.is_recursion_error
898 903 - IPython.core.ultratb.with_patch_inspect
899 904 - IPython.external.__all__
900 905 - IPython.external.__builtins__
901 906 - IPython.external.__cached__
902 907 - IPython.external.__doc__
903 908 - IPython.external.__file__
904 909 - IPython.external.__loader__
905 910 - IPython.external.__name__
906 911 - IPython.external.__package__
907 912 - IPython.external.__path__
908 913 - IPython.external.__spec__
909 914 - IPython.kernel.KernelConnectionInfo
910 915 - IPython.kernel.__builtins__
911 916 - IPython.kernel.__cached__
912 917 - IPython.kernel.__warningregistry__
913 918 - IPython.kernel.pkg
914 919 - IPython.kernel.protocol_version
915 920 - IPython.kernel.protocol_version_info
916 921 - IPython.kernel.src
917 922 - IPython.kernel.version_info
918 923 - IPython.kernel.warn
919 924 - IPython.lib.backgroundjobs
920 925 - IPython.lib.backgroundjobs.BackgroundJobBase
921 926 - IPython.lib.backgroundjobs.BackgroundJobBase.run
922 927 - IPython.lib.backgroundjobs.BackgroundJobBase.traceback
923 928 - IPython.lib.backgroundjobs.BackgroundJobExpr
924 929 - IPython.lib.backgroundjobs.BackgroundJobExpr.call
925 930 - IPython.lib.backgroundjobs.BackgroundJobFunc
926 931 - IPython.lib.backgroundjobs.BackgroundJobFunc.call
927 932 - IPython.lib.backgroundjobs.BackgroundJobManager
928 933 - IPython.lib.backgroundjobs.BackgroundJobManager.flush
929 934 - IPython.lib.backgroundjobs.BackgroundJobManager.new
930 935 - IPython.lib.backgroundjobs.BackgroundJobManager.remove
931 936 - IPython.lib.backgroundjobs.BackgroundJobManager.result
932 937 - IPython.lib.backgroundjobs.BackgroundJobManager.status
933 938 - IPython.lib.backgroundjobs.BackgroundJobManager.traceback
934 939 - IPython.lib.backgroundjobs.__builtins__
935 940 - IPython.lib.backgroundjobs.__cached__
936 941 - IPython.lib.backgroundjobs.__doc__
937 942 - IPython.lib.backgroundjobs.__file__
938 943 - IPython.lib.backgroundjobs.__loader__
939 944 - IPython.lib.backgroundjobs.__name__
940 945 - IPython.lib.backgroundjobs.__package__
941 946 - IPython.lib.backgroundjobs.__spec__
942 947 - IPython.lib.kernel.__builtins__
943 948 - IPython.lib.kernel.__cached__
944 949 - IPython.lib.kernel.__doc__
945 950 - IPython.lib.kernel.__file__
946 951 - IPython.lib.kernel.__loader__
947 952 - IPython.lib.kernel.__name__
948 953 - IPython.lib.kernel.__package__
949 954 - IPython.lib.kernel.__spec__
950 955 - IPython.lib.kernel.__warningregistry__
951 956 - IPython.paths.fs_encoding
952 957 - IPython.terminal.debugger.DEFAULT_BUFFER
953 958 - IPython.terminal.debugger.cursor_in_leading_ws
954 959 - IPython.terminal.debugger.emacs_insert_mode
955 960 - IPython.terminal.debugger.has_selection
956 961 - IPython.terminal.debugger.vi_insert_mode
957 962 - IPython.terminal.interactiveshell.DISPLAY_BANNER_DEPRECATED
958 963 - IPython.terminal.ipapp.TerminalIPythonApp.parse_command_line
959 964 - IPython.testing.test
960 965 - IPython.utils.contexts.NoOpContext
961 966 - IPython.utils.io.IOStream
962 967 - IPython.utils.io.IOStream.close
963 968 - IPython.utils.io.IOStream.write
964 969 - IPython.utils.io.IOStream.writelines
965 970 - IPython.utils.io.__warningregistry__
966 971 - IPython.utils.io.atomic_writing
967 972 - IPython.utils.io.stderr
968 973 - IPython.utils.io.stdin
969 974 - IPython.utils.io.stdout
970 975 - IPython.utils.io.unicode_std_stream
971 976 - IPython.utils.path.get_ipython_cache_dir
972 977 - IPython.utils.path.get_ipython_dir
973 978 - IPython.utils.path.get_ipython_module_path
974 979 - IPython.utils.path.get_ipython_package_dir
975 980 - IPython.utils.path.locate_profile
976 981 - IPython.utils.path.unquote_filename
977 982 - IPython.utils.py3compat.PY2
978 983 - IPython.utils.py3compat.PY3
979 984 - IPython.utils.py3compat.buffer_to_bytes
980 985 - IPython.utils.py3compat.builtin_mod_name
981 986 - IPython.utils.py3compat.cast_bytes
982 987 - IPython.utils.py3compat.getcwd
983 988 - IPython.utils.py3compat.isidentifier
984 989 - IPython.utils.py3compat.u_format
985 990
986 991 The following signatures differ between 7.x and 8.0::
987 992
988 993 - IPython.core.completer.IPCompleter.unicode_name_matches(self, text)
989 994 + IPython.core.completer.IPCompleter.unicode_name_matches(text)
990 995
991 996 - IPython.core.completer.match_dict_keys(keys, prefix, delims)
992 997 + IPython.core.completer.match_dict_keys(keys, prefix, delims, extra_prefix='None')
993 998
994 999 - IPython.core.interactiveshell.InteractiveShell.object_inspect_mime(self, oname, detail_level=0)
995 1000 + IPython.core.interactiveshell.InteractiveShell.object_inspect_mime(self, oname, detail_level=0, omit_sections='()')
996 1001
997 1002 - IPython.core.interactiveshell.InteractiveShell.set_hook(self, name, hook, priority=50, str_key='None', re_key='None', _warn_deprecated=True)
998 1003 + IPython.core.interactiveshell.InteractiveShell.set_hook(self, name, hook, priority=50, str_key='None', re_key='None')
999 1004
1000 1005 - IPython.core.oinspect.Inspector.info(self, obj, oname='', formatter='None', info='None', detail_level=0)
1001 1006 + IPython.core.oinspect.Inspector.info(self, obj, oname='', info='None', detail_level=0)
1002 1007
1003 1008 - IPython.core.oinspect.Inspector.pinfo(self, obj, oname='', formatter='None', info='None', detail_level=0, enable_html_pager=True)
1004 1009 + IPython.core.oinspect.Inspector.pinfo(self, obj, oname='', formatter='None', info='None', detail_level=0, enable_html_pager=True, omit_sections='()')
1005 1010
1006 1011 - IPython.core.profiledir.ProfileDir.copy_config_file(self, config_file, path='None', overwrite=False)
1007 1012 + IPython.core.profiledir.ProfileDir.copy_config_file(self, config_file, path, overwrite=False)
1008 1013
1009 1014 - IPython.core.ultratb.VerboseTB.format_record(self, frame, file, lnum, func, lines, index)
1010 1015 + IPython.core.ultratb.VerboseTB.format_record(self, frame_info)
1011 1016
1012 1017 - IPython.terminal.embed.InteractiveShellEmbed.mainloop(self, local_ns='None', module='None', stack_depth=0, display_banner='None', global_ns='None', compile_flags='None')
1013 1018 + IPython.terminal.embed.InteractiveShellEmbed.mainloop(self, local_ns='None', module='None', stack_depth=0, compile_flags='None')
1014 1019
1015 1020 - IPython.terminal.embed.embed(**kwargs)
1016 1021 + IPython.terminal.embed.embed(*, header='', compile_flags='None', **kwargs)
1017 1022
1018 1023 - IPython.terminal.interactiveshell.TerminalInteractiveShell.interact(self, display_banner='<object object at 0xffffff>')
1019 1024 + IPython.terminal.interactiveshell.TerminalInteractiveShell.interact(self)
1020 1025
1021 1026 - IPython.terminal.interactiveshell.TerminalInteractiveShell.mainloop(self, display_banner='<object object at 0xffffff>')
1022 1027 + IPython.terminal.interactiveshell.TerminalInteractiveShell.mainloop(self)
1023 1028
1024 1029 - IPython.utils.path.get_py_filename(name, force_win32='None')
1025 1030 + IPython.utils.path.get_py_filename(name)
1026 1031
1027 1032 The following are new attributes (that might be inherited)::
1028 1033
1029 1034 + IPython.core.completer.IPCompleter.unicode_names
1030 1035 + IPython.core.debugger.InterruptiblePdb.precmd
1031 1036 + IPython.core.debugger.Pdb.precmd
1032 1037 + IPython.core.ultratb.AutoFormattedTB.has_colors
1033 1038 + IPython.core.ultratb.ColorTB.has_colors
1034 1039 + IPython.core.ultratb.FormattedTB.has_colors
1035 1040 + IPython.core.ultratb.ListTB.has_colors
1036 1041 + IPython.core.ultratb.SyntaxTB.has_colors
1037 1042 + IPython.core.ultratb.TBTools.has_colors
1038 1043 + IPython.core.ultratb.VerboseTB.has_colors
1039 1044 + IPython.terminal.debugger.TerminalPdb.do_interact
1040 1045 + IPython.terminal.debugger.TerminalPdb.precmd
1041 1046
1042 1047 The following attribute/methods have been removed::
1043 1048
1044 1049 - IPython.core.application.BaseIPythonApplication.deprecated_subcommands
1045 1050 - IPython.core.ultratb.AutoFormattedTB.format_records
1046 1051 - IPython.core.ultratb.ColorTB.format_records
1047 1052 - IPython.core.ultratb.FormattedTB.format_records
1048 1053 - IPython.terminal.embed.InteractiveShellEmbed.init_deprecation_warnings
1049 1054 - IPython.terminal.embed.InteractiveShellEmbed.init_readline
1050 1055 - IPython.terminal.embed.InteractiveShellEmbed.write
1051 1056 - IPython.terminal.embed.InteractiveShellEmbed.write_err
1052 1057 - IPython.terminal.interactiveshell.TerminalInteractiveShell.init_deprecation_warnings
1053 1058 - IPython.terminal.interactiveshell.TerminalInteractiveShell.init_readline
1054 1059 - IPython.terminal.interactiveshell.TerminalInteractiveShell.write
1055 1060 - IPython.terminal.interactiveshell.TerminalInteractiveShell.write_err
1056 1061 - IPython.terminal.ipapp.LocateIPythonApp.deprecated_subcommands
1057 1062 - IPython.terminal.ipapp.LocateIPythonApp.initialize_subcommand
1058 1063 - IPython.terminal.ipapp.TerminalIPythonApp.deprecated_subcommands
1059 1064 - IPython.terminal.ipapp.TerminalIPythonApp.initialize_subcommand
General Comments 0
You need to be logged in to leave comments. Login now