##// END OF EJS Templates
fix for #10327 : get_ipython().magic() replaced with get_ipython().run_line_magic()
adityausathe -
Show More
@@ -1,531 +1,534 b''
1 1 """Input transformer classes 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 import abc
7 7 import functools
8 8 import re
9 9 from io import StringIO
10 10
11 11 from IPython.core.splitinput import LineInfo
12 12 from IPython.utils import tokenize2
13 13 from IPython.utils.tokenize2 import generate_tokens, untokenize, TokenError
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Globals
17 17 #-----------------------------------------------------------------------------
18 18
19 19 # The escape sequences that define the syntax transformations IPython will
20 20 # apply to user input. These can NOT be just changed here: many regular
21 21 # expressions and other parts of the code may use their hardcoded values, and
22 22 # for all intents and purposes they constitute the 'IPython syntax', so they
23 23 # should be considered fixed.
24 24
25 25 ESC_SHELL = '!' # Send line to underlying system shell
26 26 ESC_SH_CAP = '!!' # Send line to system shell and capture output
27 27 ESC_HELP = '?' # Find information about object
28 28 ESC_HELP2 = '??' # Find extra-detailed information about object
29 29 ESC_MAGIC = '%' # Call magic function
30 30 ESC_MAGIC2 = '%%' # Call cell-magic function
31 31 ESC_QUOTE = ',' # Split args on whitespace, quote each as string and call
32 32 ESC_QUOTE2 = ';' # Quote all args as a single string, call
33 33 ESC_PAREN = '/' # Call first argument with rest of line as arguments
34 34
35 35 ESC_SEQUENCES = [ESC_SHELL, ESC_SH_CAP, ESC_HELP ,\
36 36 ESC_HELP2, ESC_MAGIC, ESC_MAGIC2,\
37 37 ESC_QUOTE, ESC_QUOTE2, ESC_PAREN ]
38 38
39 39
40 40 class InputTransformer(metaclass=abc.ABCMeta):
41 41 """Abstract base class for line-based input transformers."""
42 42
43 43 @abc.abstractmethod
44 44 def push(self, line):
45 45 """Send a line of input to the transformer, returning the transformed
46 46 input or None if the transformer is waiting for more input.
47 47
48 48 Must be overridden by subclasses.
49 49
50 50 Implementations may raise ``SyntaxError`` if the input is invalid. No
51 51 other exceptions may be raised.
52 52 """
53 53 pass
54 54
55 55 @abc.abstractmethod
56 56 def reset(self):
57 57 """Return, transformed any lines that the transformer has accumulated,
58 58 and reset its internal state.
59 59
60 60 Must be overridden by subclasses.
61 61 """
62 62 pass
63 63
64 64 @classmethod
65 65 def wrap(cls, func):
66 66 """Can be used by subclasses as a decorator, to return a factory that
67 67 will allow instantiation with the decorated object.
68 68 """
69 69 @functools.wraps(func)
70 70 def transformer_factory(**kwargs):
71 71 return cls(func, **kwargs)
72 72
73 73 return transformer_factory
74 74
75 75 class StatelessInputTransformer(InputTransformer):
76 76 """Wrapper for a stateless input transformer implemented as a function."""
77 77 def __init__(self, func):
78 78 self.func = func
79 79
80 80 def __repr__(self):
81 81 return "StatelessInputTransformer(func={0!r})".format(self.func)
82 82
83 83 def push(self, line):
84 84 """Send a line of input to the transformer, returning the
85 85 transformed input."""
86 86 return self.func(line)
87 87
88 88 def reset(self):
89 89 """No-op - exists for compatibility."""
90 90 pass
91 91
92 92 class CoroutineInputTransformer(InputTransformer):
93 93 """Wrapper for an input transformer implemented as a coroutine."""
94 94 def __init__(self, coro, **kwargs):
95 95 # Prime it
96 96 self.coro = coro(**kwargs)
97 97 next(self.coro)
98 98
99 99 def __repr__(self):
100 100 return "CoroutineInputTransformer(coro={0!r})".format(self.coro)
101 101
102 102 def push(self, line):
103 103 """Send a line of input to the transformer, returning the
104 104 transformed input or None if the transformer is waiting for more
105 105 input.
106 106 """
107 107 return self.coro.send(line)
108 108
109 109 def reset(self):
110 110 """Return, transformed any lines that the transformer has
111 111 accumulated, and reset its internal state.
112 112 """
113 113 return self.coro.send(None)
114 114
115 115 class TokenInputTransformer(InputTransformer):
116 116 """Wrapper for a token-based input transformer.
117 117
118 118 func should accept a list of tokens (5-tuples, see tokenize docs), and
119 119 return an iterable which can be passed to tokenize.untokenize().
120 120 """
121 121 def __init__(self, func):
122 122 self.func = func
123 123 self.buf = []
124 124 self.reset_tokenizer()
125 125
126 126 def reset_tokenizer(self):
127 127 it = iter(self.buf)
128 128 self.tokenizer = generate_tokens(it.__next__)
129 129
130 130 def push(self, line):
131 131 self.buf.append(line + '\n')
132 132 if all(l.isspace() for l in self.buf):
133 133 return self.reset()
134 134
135 135 tokens = []
136 136 stop_at_NL = False
137 137 try:
138 138 for intok in self.tokenizer:
139 139 tokens.append(intok)
140 140 t = intok[0]
141 141 if t == tokenize2.NEWLINE or (stop_at_NL and t == tokenize2.NL):
142 142 # Stop before we try to pull a line we don't have yet
143 143 break
144 144 elif t == tokenize2.ERRORTOKEN:
145 145 stop_at_NL = True
146 146 except TokenError:
147 147 # Multi-line statement - stop and try again with the next line
148 148 self.reset_tokenizer()
149 149 return None
150 150
151 151 return self.output(tokens)
152 152
153 153 def output(self, tokens):
154 154 self.buf.clear()
155 155 self.reset_tokenizer()
156 156 return untokenize(self.func(tokens)).rstrip('\n')
157 157
158 158 def reset(self):
159 159 l = ''.join(self.buf)
160 160 self.buf.clear()
161 161 self.reset_tokenizer()
162 162 if l:
163 163 return l.rstrip('\n')
164 164
165 165 class assemble_python_lines(TokenInputTransformer):
166 166 def __init__(self):
167 167 super(assemble_python_lines, self).__init__(None)
168 168
169 169 def output(self, tokens):
170 170 return self.reset()
171 171
172 172 @CoroutineInputTransformer.wrap
173 173 def assemble_logical_lines():
174 174 """Join lines following explicit line continuations (\)"""
175 175 line = ''
176 176 while True:
177 177 line = (yield line)
178 178 if not line or line.isspace():
179 179 continue
180 180
181 181 parts = []
182 182 while line is not None:
183 183 if line.endswith('\\') and (not has_comment(line)):
184 184 parts.append(line[:-1])
185 185 line = (yield None) # Get another line
186 186 else:
187 187 parts.append(line)
188 188 break
189 189
190 190 # Output
191 191 line = ''.join(parts)
192 192
193 193 # Utilities
194 194 def _make_help_call(target, esc, lspace, next_input=None):
195 195 """Prepares a pinfo(2)/psearch call from a target name and the escape
196 196 (i.e. ? or ??)"""
197 197 method = 'pinfo2' if esc == '??' \
198 198 else 'psearch' if '*' in target \
199 199 else 'pinfo'
200 200 arg = " ".join([method, target])
201 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
202 t_magic_name, _, t_magic_arg_s = arg.partition(' ')
203 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
201 204 if next_input is None:
202 return '%sget_ipython().magic(%r)' % (lspace, arg)
205 return '%sget_ipython().run_line_magic(%r, %r)' % (lspace, t_magic_name, t_magic_arg_s)
203 206 else:
204 return '%sget_ipython().set_next_input(%r);get_ipython().magic(%r)' % \
205 (lspace, next_input, arg)
207 return '%sget_ipython().set_next_input(%r);get_ipython().run_line_magic(%r, %r)' % \
208 (lspace, next_input, t_magic_name, t_magic_arg_s)
206 209
207 210 # These define the transformations for the different escape characters.
208 211 def _tr_system(line_info):
209 212 "Translate lines escaped with: !"
210 213 cmd = line_info.line.lstrip().lstrip(ESC_SHELL)
211 214 return '%sget_ipython().system(%r)' % (line_info.pre, cmd)
212 215
213 216 def _tr_system2(line_info):
214 217 "Translate lines escaped with: !!"
215 218 cmd = line_info.line.lstrip()[2:]
216 219 return '%sget_ipython().getoutput(%r)' % (line_info.pre, cmd)
217 220
218 221 def _tr_help(line_info):
219 222 "Translate lines escaped with: ?/??"
220 223 # A naked help line should just fire the intro help screen
221 224 if not line_info.line[1:]:
222 225 return 'get_ipython().show_usage()'
223 226
224 227 return _make_help_call(line_info.ifun, line_info.esc, line_info.pre)
225 228
226 229 def _tr_magic(line_info):
227 230 "Translate lines escaped with: %"
228 231 tpl = '%sget_ipython().run_line_magic(%r, %r)'
229 232 if line_info.line.startswith(ESC_MAGIC2):
230 233 return line_info.line
231 234 cmd = ' '.join([line_info.ifun, line_info.the_rest]).strip()
232 235 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
233 236 t_magic_name, _, t_magic_arg_s = cmd.partition(' ')
234 237 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
235 238 return tpl % (line_info.pre, t_magic_name, t_magic_arg_s)
236 239
237 240 def _tr_quote(line_info):
238 241 "Translate lines escaped with: ,"
239 242 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
240 243 '", "'.join(line_info.the_rest.split()) )
241 244
242 245 def _tr_quote2(line_info):
243 246 "Translate lines escaped with: ;"
244 247 return '%s%s("%s")' % (line_info.pre, line_info.ifun,
245 248 line_info.the_rest)
246 249
247 250 def _tr_paren(line_info):
248 251 "Translate lines escaped with: /"
249 252 return '%s%s(%s)' % (line_info.pre, line_info.ifun,
250 253 ", ".join(line_info.the_rest.split()))
251 254
252 255 tr = { ESC_SHELL : _tr_system,
253 256 ESC_SH_CAP : _tr_system2,
254 257 ESC_HELP : _tr_help,
255 258 ESC_HELP2 : _tr_help,
256 259 ESC_MAGIC : _tr_magic,
257 260 ESC_QUOTE : _tr_quote,
258 261 ESC_QUOTE2 : _tr_quote2,
259 262 ESC_PAREN : _tr_paren }
260 263
261 264 @StatelessInputTransformer.wrap
262 265 def escaped_commands(line):
263 266 """Transform escaped commands - %magic, !system, ?help + various autocalls.
264 267 """
265 268 if not line or line.isspace():
266 269 return line
267 270 lineinf = LineInfo(line)
268 271 if lineinf.esc not in tr:
269 272 return line
270 273
271 274 return tr[lineinf.esc](lineinf)
272 275
273 276 _initial_space_re = re.compile(r'\s*')
274 277
275 278 _help_end_re = re.compile(r"""(%{0,2}
276 279 [a-zA-Z_*][\w*]* # Variable name
277 280 (\.[a-zA-Z_*][\w*]*)* # .etc.etc
278 281 )
279 282 (\?\??)$ # ? or ??
280 283 """,
281 284 re.VERBOSE)
282 285
283 286 # Extra pseudotokens for multiline strings and data structures
284 287 _MULTILINE_STRING = object()
285 288 _MULTILINE_STRUCTURE = object()
286 289
287 290 def _line_tokens(line):
288 291 """Helper for has_comment and ends_in_comment_or_string."""
289 292 readline = StringIO(line).readline
290 293 toktypes = set()
291 294 try:
292 295 for t in generate_tokens(readline):
293 296 toktypes.add(t[0])
294 297 except TokenError as e:
295 298 # There are only two cases where a TokenError is raised.
296 299 if 'multi-line string' in e.args[0]:
297 300 toktypes.add(_MULTILINE_STRING)
298 301 else:
299 302 toktypes.add(_MULTILINE_STRUCTURE)
300 303 return toktypes
301 304
302 305 def has_comment(src):
303 306 """Indicate whether an input line has (i.e. ends in, or is) a comment.
304 307
305 308 This uses tokenize, so it can distinguish comments from # inside strings.
306 309
307 310 Parameters
308 311 ----------
309 312 src : string
310 313 A single line input string.
311 314
312 315 Returns
313 316 -------
314 317 comment : bool
315 318 True if source has a comment.
316 319 """
317 320 return (tokenize2.COMMENT in _line_tokens(src))
318 321
319 322 def ends_in_comment_or_string(src):
320 323 """Indicates whether or not an input line ends in a comment or within
321 324 a multiline string.
322 325
323 326 Parameters
324 327 ----------
325 328 src : string
326 329 A single line input string.
327 330
328 331 Returns
329 332 -------
330 333 comment : bool
331 334 True if source ends in a comment or multiline string.
332 335 """
333 336 toktypes = _line_tokens(src)
334 337 return (tokenize2.COMMENT in toktypes) or (_MULTILINE_STRING in toktypes)
335 338
336 339
337 340 @StatelessInputTransformer.wrap
338 341 def help_end(line):
339 342 """Translate lines with ?/?? at the end"""
340 343 m = _help_end_re.search(line)
341 344 if m is None or ends_in_comment_or_string(line):
342 345 return line
343 346 target = m.group(1)
344 347 esc = m.group(3)
345 348 lspace = _initial_space_re.match(line).group(0)
346 349
347 350 # If we're mid-command, put it back on the next prompt for the user.
348 351 next_input = line.rstrip('?') if line.strip() != m.group(0) else None
349 352
350 353 return _make_help_call(target, esc, lspace, next_input)
351 354
352 355
353 356 @CoroutineInputTransformer.wrap
354 357 def cellmagic(end_on_blank_line=False):
355 358 """Captures & transforms cell magics.
356 359
357 360 After a cell magic is started, this stores up any lines it gets until it is
358 361 reset (sent None).
359 362 """
360 363 tpl = 'get_ipython().run_cell_magic(%r, %r, %r)'
361 364 cellmagic_help_re = re.compile('%%\w+\?')
362 365 line = ''
363 366 while True:
364 367 line = (yield line)
365 368 # consume leading empty lines
366 369 while not line:
367 370 line = (yield line)
368 371
369 372 if not line.startswith(ESC_MAGIC2):
370 373 # This isn't a cell magic, idle waiting for reset then start over
371 374 while line is not None:
372 375 line = (yield line)
373 376 continue
374 377
375 378 if cellmagic_help_re.match(line):
376 379 # This case will be handled by help_end
377 380 continue
378 381
379 382 first = line
380 383 body = []
381 384 line = (yield None)
382 385 while (line is not None) and \
383 386 ((line.strip() != '') or not end_on_blank_line):
384 387 body.append(line)
385 388 line = (yield None)
386 389
387 390 # Output
388 391 magic_name, _, first = first.partition(' ')
389 392 magic_name = magic_name.lstrip(ESC_MAGIC2)
390 393 line = tpl % (magic_name, first, u'\n'.join(body))
391 394
392 395
393 396 def _strip_prompts(prompt_re, initial_re=None, turnoff_re=None):
394 397 """Remove matching input prompts from a block of input.
395 398
396 399 Parameters
397 400 ----------
398 401 prompt_re : regular expression
399 402 A regular expression matching any input prompt (including continuation)
400 403 initial_re : regular expression, optional
401 404 A regular expression matching only the initial prompt, but not continuation.
402 405 If no initial expression is given, prompt_re will be used everywhere.
403 406 Used mainly for plain Python prompts, where the continuation prompt
404 407 ``...`` is a valid Python expression in Python 3, so shouldn't be stripped.
405 408
406 409 If initial_re and prompt_re differ,
407 410 only initial_re will be tested against the first line.
408 411 If any prompt is found on the first two lines,
409 412 prompts will be stripped from the rest of the block.
410 413 """
411 414 if initial_re is None:
412 415 initial_re = prompt_re
413 416 line = ''
414 417 while True:
415 418 line = (yield line)
416 419
417 420 # First line of cell
418 421 if line is None:
419 422 continue
420 423 out, n1 = initial_re.subn('', line, count=1)
421 424 if turnoff_re and not n1:
422 425 if turnoff_re.match(line):
423 426 # We're in e.g. a cell magic; disable this transformer for
424 427 # the rest of the cell.
425 428 while line is not None:
426 429 line = (yield line)
427 430 continue
428 431
429 432 line = (yield out)
430 433
431 434 if line is None:
432 435 continue
433 436 # check for any prompt on the second line of the cell,
434 437 # because people often copy from just after the first prompt,
435 438 # so we might not see it in the first line.
436 439 out, n2 = prompt_re.subn('', line, count=1)
437 440 line = (yield out)
438 441
439 442 if n1 or n2:
440 443 # Found a prompt in the first two lines - check for it in
441 444 # the rest of the cell as well.
442 445 while line is not None:
443 446 line = (yield prompt_re.sub('', line, count=1))
444 447
445 448 else:
446 449 # Prompts not in input - wait for reset
447 450 while line is not None:
448 451 line = (yield line)
449 452
450 453 @CoroutineInputTransformer.wrap
451 454 def classic_prompt():
452 455 """Strip the >>>/... prompts of the Python interactive shell."""
453 456 # FIXME: non-capturing version (?:...) usable?
454 457 prompt_re = re.compile(r'^(>>>|\.\.\.)( |$)')
455 458 initial_re = re.compile(r'^>>>( |$)')
456 459 # Any %magic/!system is IPython syntax, so we needn't look for >>> prompts
457 460 turnoff_re = re.compile(r'^[%!]')
458 461 return _strip_prompts(prompt_re, initial_re, turnoff_re)
459 462
460 463 @CoroutineInputTransformer.wrap
461 464 def ipy_prompt():
462 465 """Strip IPython's In [1]:/...: prompts."""
463 466 # FIXME: non-capturing version (?:...) usable?
464 467 prompt_re = re.compile(r'^(In \[\d+\]: |\s*\.{3,}: ?)')
465 468 # Disable prompt stripping inside cell magics
466 469 turnoff_re = re.compile(r'^%%')
467 470 return _strip_prompts(prompt_re, turnoff_re=turnoff_re)
468 471
469 472
470 473 @CoroutineInputTransformer.wrap
471 474 def leading_indent():
472 475 """Remove leading indentation.
473 476
474 477 If the first line starts with a spaces or tabs, the same whitespace will be
475 478 removed from each following line until it is reset.
476 479 """
477 480 space_re = re.compile(r'^[ \t]+')
478 481 line = ''
479 482 while True:
480 483 line = (yield line)
481 484
482 485 if line is None:
483 486 continue
484 487
485 488 m = space_re.match(line)
486 489 if m:
487 490 space = m.group(0)
488 491 while line is not None:
489 492 if line.startswith(space):
490 493 line = line[len(space):]
491 494 line = (yield line)
492 495 else:
493 496 # No leading spaces - wait for reset
494 497 while line is not None:
495 498 line = (yield line)
496 499
497 500
498 501 _assign_pat = \
499 502 r'''(?P<lhs>(\s*)
500 503 ([\w\.]+) # Initial identifier
501 504 (\s*,\s*
502 505 \*?[\w\.]+)* # Further identifiers for unpacking
503 506 \s*?,? # Trailing comma
504 507 )
505 508 \s*=\s*
506 509 '''
507 510
508 511 assign_system_re = re.compile(r'{}!\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
509 512 assign_system_template = '%s = get_ipython().getoutput(%r)'
510 513 @StatelessInputTransformer.wrap
511 514 def assign_from_system(line):
512 515 """Transform assignment from system commands (e.g. files = !ls)"""
513 516 m = assign_system_re.match(line)
514 517 if m is None:
515 518 return line
516 519
517 520 return assign_system_template % m.group('lhs', 'cmd')
518 521
519 522 assign_magic_re = re.compile(r'{}%\s*(?P<cmd>.*)'.format(_assign_pat), re.VERBOSE)
520 523 assign_magic_template = '%s = get_ipython().run_line_magic(%r, %r)'
521 524 @StatelessInputTransformer.wrap
522 525 def assign_from_magic(line):
523 526 """Transform assignment from magic commands (e.g. a = %who_ls)"""
524 527 m = assign_magic_re.match(line)
525 528 if m is None:
526 529 return line
527 530 #Prepare arguments for get_ipython().run_line_magic(magic_name, magic_args)
528 531 m_lhs, m_cmd = m.group('lhs', 'cmd')
529 t_magic_name, _, t_magic_arg_s = cmd.partition(' ')
532 t_magic_name, _, t_magic_arg_s = m_cmd.partition(' ')
530 533 t_magic_name = t_magic_name.lstrip(ESC_MAGIC)
531 534 return assign_magic_template % (m_lhs, t_magic_name, t_magic_arg_s)
General Comments 0
You need to be logged in to leave comments. Login now