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