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