##// END OF EJS Templates
Merge pull request #4678 from ivanov/console-display-text...
Min RK -
r13867:edb38312 merge
parent child Browse files
Show More
@@ -1,497 +1,501 b''
1 1 # -*- coding: utf-8 -*-
2 2 """terminal client to the IPython kernel
3 3
4 4 """
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (C) 2013 The IPython Development Team
7 7 #
8 8 # Distributed under the terms of the BSD License. The full license is in
9 9 # the file COPYING, distributed as part of this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15 from __future__ import print_function
16 16
17 17 import bdb
18 18 import signal
19 19 import os
20 20 import sys
21 21 import time
22 22 import subprocess
23 23 from io import BytesIO
24 24 import base64
25 25
26 26 try:
27 27 from queue import Empty # Py 3
28 28 except ImportError:
29 29 from Queue import Empty # Py 2
30 30
31 31 from IPython.core import page
32 32 from IPython.utils.warn import warn, error
33 33 from IPython.utils import io
34 34 from IPython.utils.py3compat import string_types, input
35 35 from IPython.utils.traitlets import List, Enum, Any, Instance, Unicode, Float
36 36 from IPython.utils.tempdir import NamedFileInTemporaryDirectory
37 37
38 38 from IPython.terminal.interactiveshell import TerminalInteractiveShell
39 39 from IPython.terminal.console.completer import ZMQCompleter
40 40
41 41
42 42 class ZMQTerminalInteractiveShell(TerminalInteractiveShell):
43 43 """A subclass of TerminalInteractiveShell that uses the 0MQ kernel"""
44 44 _executing = False
45 45 _execution_state = Unicode('')
46 46 kernel_timeout = Float(60, config=True,
47 47 help="""Timeout for giving up on a kernel (in seconds).
48 48
49 49 On first connect and restart, the console tests whether the
50 50 kernel is running and responsive by sending kernel_info_requests.
51 51 This sets the timeout in seconds for how long the kernel can take
52 52 before being presumed dead.
53 53 """
54 54 )
55 55
56 56 image_handler = Enum(('PIL', 'stream', 'tempfile', 'callable'),
57 57 config=True, help=
58 58 """
59 59 Handler for image type output. This is useful, for example,
60 60 when connecting to the kernel in which pylab inline backend is
61 61 activated. There are four handlers defined. 'PIL': Use
62 62 Python Imaging Library to popup image; 'stream': Use an
63 63 external program to show the image. Image will be fed into
64 64 the STDIN of the program. You will need to configure
65 65 `stream_image_handler`; 'tempfile': Use an external program to
66 66 show the image. Image will be saved in a temporally file and
67 67 the program is called with the temporally file. You will need
68 68 to configure `tempfile_image_handler`; 'callable': You can set
69 69 any Python callable which is called with the image data. You
70 70 will need to configure `callable_image_handler`.
71 71 """
72 72 )
73 73
74 74 stream_image_handler = List(config=True, help=
75 75 """
76 76 Command to invoke an image viewer program when you are using
77 77 'stream' image handler. This option is a list of string where
78 78 the first element is the command itself and reminders are the
79 79 options for the command. Raw image data is given as STDIN to
80 80 the program.
81 81 """
82 82 )
83 83
84 84 tempfile_image_handler = List(config=True, help=
85 85 """
86 86 Command to invoke an image viewer program when you are using
87 87 'tempfile' image handler. This option is a list of string
88 88 where the first element is the command itself and reminders
89 89 are the options for the command. You can use {file} and
90 90 {format} in the string to represent the location of the
91 91 generated image file and image format.
92 92 """
93 93 )
94 94
95 95 callable_image_handler = Any(config=True, help=
96 96 """
97 97 Callable object called via 'callable' image handler with one
98 98 argument, `data`, which is `msg["content"]["data"]` where
99 99 `msg` is the message from iopub channel. For exmaple, you can
100 100 find base64 encoded PNG data as `data['image/png']`.
101 101 """
102 102 )
103 103
104 104 mime_preference = List(
105 105 default_value=['image/png', 'image/jpeg', 'image/svg+xml'],
106 106 config=True, allow_none=False, help=
107 107 """
108 108 Preferred object representation MIME type in order. First
109 109 matched MIME type will be used.
110 110 """
111 111 )
112 112
113 113 manager = Instance('IPython.kernel.KernelManager')
114 114 client = Instance('IPython.kernel.KernelClient')
115 115 def _client_changed(self, name, old, new):
116 116 self.session_id = new.session.session
117 117 session_id = Unicode()
118 118
119 119 def init_completer(self):
120 120 """Initialize the completion machinery.
121 121
122 122 This creates completion machinery that can be used by client code,
123 123 either interactively in-process (typically triggered by the readline
124 124 library), programmatically (such as in test suites) or out-of-process
125 125 (typically over the network by remote frontends).
126 126 """
127 127 from IPython.core.completerlib import (module_completer,
128 128 magic_run_completer, cd_completer)
129 129
130 130 self.Completer = ZMQCompleter(self, self.client, config=self.config)
131 131
132 132
133 133 self.set_hook('complete_command', module_completer, str_key = 'import')
134 134 self.set_hook('complete_command', module_completer, str_key = 'from')
135 135 self.set_hook('complete_command', magic_run_completer, str_key = '%run')
136 136 self.set_hook('complete_command', cd_completer, str_key = '%cd')
137 137
138 138 # Only configure readline if we truly are using readline. IPython can
139 139 # do tab-completion over the network, in GUIs, etc, where readline
140 140 # itself may be absent
141 141 if self.has_readline:
142 142 self.set_readline_completer()
143 143
144 144 def ask_exit(self):
145 145 super(ZMQTerminalInteractiveShell, self).ask_exit()
146 146 if self.exit_now and self.manager:
147 147 self.client.shutdown()
148 148
149 149 def run_cell(self, cell, store_history=True):
150 150 """Run a complete IPython cell.
151 151
152 152 Parameters
153 153 ----------
154 154 cell : str
155 155 The code (including IPython code such as %magic functions) to run.
156 156 store_history : bool
157 157 If True, the raw and translated cell will be stored in IPython's
158 158 history. For user code calling back into IPython's machinery, this
159 159 should be set to False.
160 160 """
161 161 if (not cell) or cell.isspace():
162 162 return
163 163
164 164 if cell.strip() == 'exit':
165 165 # explicitly handle 'exit' command
166 166 return self.ask_exit()
167 167
168 168 # flush stale replies, which could have been ignored, due to missed heartbeats
169 169 while self.client.shell_channel.msg_ready():
170 170 self.client.shell_channel.get_msg()
171 171 # shell_channel.execute takes 'hidden', which is the inverse of store_hist
172 172 msg_id = self.client.shell_channel.execute(cell, not store_history)
173 173
174 174 # first thing is wait for any side effects (output, stdin, etc.)
175 175 self._executing = True
176 176 self._execution_state = "busy"
177 177 while self._execution_state != 'idle' and self.client.is_alive():
178 178 try:
179 179 self.handle_stdin_request(msg_id, timeout=0.05)
180 180 except Empty:
181 181 # display intermediate print statements, etc.
182 182 self.handle_iopub(msg_id)
183 183 pass
184 184
185 185 # after all of that is done, wait for the execute reply
186 186 while self.client.is_alive():
187 187 try:
188 188 self.handle_execute_reply(msg_id, timeout=0.05)
189 189 except Empty:
190 190 pass
191 191 else:
192 192 break
193 193 self._executing = False
194 194
195 195 #-----------------
196 196 # message handlers
197 197 #-----------------
198 198
199 199 def handle_execute_reply(self, msg_id, timeout=None):
200 200 msg = self.client.shell_channel.get_msg(block=False, timeout=timeout)
201 201 if msg["parent_header"].get("msg_id", None) == msg_id:
202 202
203 203 self.handle_iopub(msg_id)
204 204
205 205 content = msg["content"]
206 206 status = content['status']
207 207
208 208 if status == 'aborted':
209 209 self.write('Aborted\n')
210 210 return
211 211 elif status == 'ok':
212 212 # print execution payloads as well:
213 213 for item in content["payload"]:
214 214 text = item.get('text', None)
215 215 if text:
216 216 page.page(text)
217 217
218 218 elif status == 'error':
219 219 for frame in content["traceback"]:
220 220 print(frame, file=io.stderr)
221 221
222 222 self.execution_count = int(content["execution_count"] + 1)
223 223
224 224
225 225 def handle_iopub(self, msg_id):
226 226 """ Method to process subscribe channel's messages
227 227
228 228 This method consumes and processes messages on the IOPub channel,
229 229 such as stdout, stderr, pyout and status.
230 230
231 231 It only displays output that is caused by the given msg_id
232 232 """
233 233 while self.client.iopub_channel.msg_ready():
234 234 sub_msg = self.client.iopub_channel.get_msg()
235 235 msg_type = sub_msg['header']['msg_type']
236 236 parent = sub_msg["parent_header"]
237 237 if (not parent) or msg_id == parent['msg_id']:
238 238 if msg_type == 'status':
239 239 state = self._execution_state = sub_msg["content"]["execution_state"]
240 240 # idle messages mean an individual sequence is complete,
241 241 # so break out of consumption to allow other things to take over.
242 242 if state == 'idle':
243 243 break
244 244
245 245 elif msg_type == 'stream':
246 246 if sub_msg["content"]["name"] == "stdout":
247 247 print(sub_msg["content"]["data"], file=io.stdout, end="")
248 248 io.stdout.flush()
249 249 elif sub_msg["content"]["name"] == "stderr" :
250 250 print(sub_msg["content"]["data"], file=io.stderr, end="")
251 251 io.stderr.flush()
252 252
253 253 elif msg_type == 'pyout':
254 254 self.execution_count = int(sub_msg["content"]["execution_count"])
255 255 format_dict = sub_msg["content"]["data"]
256 256 self.handle_rich_data(format_dict)
257 257 # taken from DisplayHook.__call__:
258 258 hook = self.displayhook
259 259 hook.start_displayhook()
260 260 hook.write_output_prompt()
261 261 hook.write_format_data(format_dict)
262 262 hook.log_output(format_dict)
263 263 hook.finish_displayhook()
264 264
265 265 elif msg_type == 'display_data':
266 self.handle_rich_data(sub_msg["content"]["data"])
267
266 data = sub_msg["content"]["data"]
267 handled = self.handle_rich_data(data)
268 if not handled:
269 # if it was an image, we handled it by now
270 if 'text/plain' in data:
271 print(data['text/plain'])
268 272
269 273 _imagemime = {
270 274 'image/png': 'png',
271 275 'image/jpeg': 'jpeg',
272 276 'image/svg+xml': 'svg',
273 277 }
274 278
275 279 def handle_rich_data(self, data):
276 280 for mime in self.mime_preference:
277 281 if mime in data and mime in self._imagemime:
278 282 self.handle_image(data, mime)
279 return
283 return True
280 284
281 285 def handle_image(self, data, mime):
282 286 handler = getattr(
283 287 self, 'handle_image_{0}'.format(self.image_handler), None)
284 288 if handler:
285 289 handler(data, mime)
286 290
287 291 def handle_image_PIL(self, data, mime):
288 292 if mime not in ('image/png', 'image/jpeg'):
289 293 return
290 294 import PIL.Image
291 295 raw = base64.decodestring(data[mime].encode('ascii'))
292 296 img = PIL.Image.open(BytesIO(raw))
293 297 img.show()
294 298
295 299 def handle_image_stream(self, data, mime):
296 300 raw = base64.decodestring(data[mime].encode('ascii'))
297 301 imageformat = self._imagemime[mime]
298 302 fmt = dict(format=imageformat)
299 303 args = [s.format(**fmt) for s in self.stream_image_handler]
300 304 with open(os.devnull, 'w') as devnull:
301 305 proc = subprocess.Popen(
302 306 args, stdin=subprocess.PIPE,
303 307 stdout=devnull, stderr=devnull)
304 308 proc.communicate(raw)
305 309
306 310 def handle_image_tempfile(self, data, mime):
307 311 raw = base64.decodestring(data[mime].encode('ascii'))
308 312 imageformat = self._imagemime[mime]
309 313 filename = 'tmp.{0}'.format(imageformat)
310 314 with NamedFileInTemporaryDirectory(filename) as f, \
311 315 open(os.devnull, 'w') as devnull:
312 316 f.write(raw)
313 317 f.flush()
314 318 fmt = dict(file=f.name, format=imageformat)
315 319 args = [s.format(**fmt) for s in self.tempfile_image_handler]
316 320 subprocess.call(args, stdout=devnull, stderr=devnull)
317 321
318 322 def handle_image_callable(self, data, mime):
319 323 self.callable_image_handler(data)
320 324
321 325 def handle_stdin_request(self, msg_id, timeout=0.1):
322 326 """ Method to capture raw_input
323 327 """
324 328 msg_rep = self.client.stdin_channel.get_msg(timeout=timeout)
325 329 # in case any iopub came while we were waiting:
326 330 self.handle_iopub(msg_id)
327 331 if msg_id == msg_rep["parent_header"].get("msg_id"):
328 332 # wrap SIGINT handler
329 333 real_handler = signal.getsignal(signal.SIGINT)
330 334 def double_int(sig,frame):
331 335 # call real handler (forwards sigint to kernel),
332 336 # then raise local interrupt, stopping local raw_input
333 337 real_handler(sig,frame)
334 338 raise KeyboardInterrupt
335 339 signal.signal(signal.SIGINT, double_int)
336 340
337 341 try:
338 342 raw_data = input(msg_rep["content"]["prompt"])
339 343 except EOFError:
340 344 # turn EOFError into EOF character
341 345 raw_data = '\x04'
342 346 except KeyboardInterrupt:
343 347 sys.stdout.write('\n')
344 348 return
345 349 finally:
346 350 # restore SIGINT handler
347 351 signal.signal(signal.SIGINT, real_handler)
348 352
349 353 # only send stdin reply if there *was not* another request
350 354 # or execution finished while we were reading.
351 355 if not (self.client.stdin_channel.msg_ready() or self.client.shell_channel.msg_ready()):
352 356 self.client.stdin_channel.input(raw_data)
353 357
354 358 def mainloop(self, display_banner=False):
355 359 while True:
356 360 try:
357 361 self.interact(display_banner=display_banner)
358 362 #self.interact_with_readline()
359 363 # XXX for testing of a readline-decoupled repl loop, call
360 364 # interact_with_readline above
361 365 break
362 366 except KeyboardInterrupt:
363 367 # this should not be necessary, but KeyboardInterrupt
364 368 # handling seems rather unpredictable...
365 369 self.write("\nKeyboardInterrupt in interact()\n")
366 370
367 371 def wait_for_kernel(self, timeout=None):
368 372 """method to wait for a kernel to be ready"""
369 373 tic = time.time()
370 374 self.client.hb_channel.unpause()
371 375 while True:
372 376 msg_id = self.client.kernel_info()
373 377 reply = None
374 378 while True:
375 379 try:
376 380 reply = self.client.get_shell_msg(timeout=1)
377 381 except Empty:
378 382 break
379 383 else:
380 384 if reply['parent_header'].get('msg_id') == msg_id:
381 385 return True
382 386 if timeout is not None \
383 387 and (time.time() - tic) > timeout \
384 388 and not self.client.hb_channel.is_beating():
385 389 # heart failed
386 390 return False
387 391 return True
388 392
389 393 def interact(self, display_banner=None):
390 394 """Closely emulate the interactive Python console."""
391 395
392 396 # batch run -> do not interact
393 397 if self.exit_now:
394 398 return
395 399
396 400 if display_banner is None:
397 401 display_banner = self.display_banner
398 402
399 403 if isinstance(display_banner, string_types):
400 404 self.show_banner(display_banner)
401 405 elif display_banner:
402 406 self.show_banner()
403 407
404 408 more = False
405 409
406 410 # run a non-empty no-op, so that we don't get a prompt until
407 411 # we know the kernel is ready. This keeps the connection
408 412 # message above the first prompt.
409 413 if not self.wait_for_kernel(self.kernel_timeout):
410 414 error("Kernel did not respond\n")
411 415 return
412 416
413 417 if self.has_readline:
414 418 self.readline_startup_hook(self.pre_readline)
415 419 hlen_b4_cell = self.readline.get_current_history_length()
416 420 else:
417 421 hlen_b4_cell = 0
418 422 # exit_now is set by a call to %Exit or %Quit, through the
419 423 # ask_exit callback.
420 424
421 425 while not self.exit_now:
422 426 if not self.client.is_alive():
423 427 # kernel died, prompt for action or exit
424 428
425 429 action = "restart" if self.manager else "wait for restart"
426 430 ans = self.ask_yes_no("kernel died, %s ([y]/n)?" % action, default='y')
427 431 if ans:
428 432 if self.manager:
429 433 self.manager.restart_kernel(True)
430 434 self.wait_for_kernel(self.kernel_timeout)
431 435 else:
432 436 self.exit_now = True
433 437 continue
434 438 try:
435 439 # protect prompt block from KeyboardInterrupt
436 440 # when sitting on ctrl-C
437 441 self.hooks.pre_prompt_hook()
438 442 if more:
439 443 try:
440 444 prompt = self.prompt_manager.render('in2')
441 445 except Exception:
442 446 self.showtraceback()
443 447 if self.autoindent:
444 448 self.rl_do_indent = True
445 449
446 450 else:
447 451 try:
448 452 prompt = self.separate_in + self.prompt_manager.render('in')
449 453 except Exception:
450 454 self.showtraceback()
451 455
452 456 line = self.raw_input(prompt)
453 457 if self.exit_now:
454 458 # quick exit on sys.std[in|out] close
455 459 break
456 460 if self.autoindent:
457 461 self.rl_do_indent = False
458 462
459 463 except KeyboardInterrupt:
460 464 #double-guard against keyboardinterrupts during kbdint handling
461 465 try:
462 466 self.write('\nKeyboardInterrupt\n')
463 467 source_raw = self.input_splitter.source_raw_reset()[1]
464 468 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
465 469 more = False
466 470 except KeyboardInterrupt:
467 471 pass
468 472 except EOFError:
469 473 if self.autoindent:
470 474 self.rl_do_indent = False
471 475 if self.has_readline:
472 476 self.readline_startup_hook(None)
473 477 self.write('\n')
474 478 self.exit()
475 479 except bdb.BdbQuit:
476 480 warn('The Python debugger has exited with a BdbQuit exception.\n'
477 481 'Because of how pdb handles the stack, it is impossible\n'
478 482 'for IPython to properly format this particular exception.\n'
479 483 'IPython will resume normal operation.')
480 484 except:
481 485 # exceptions here are VERY RARE, but they can be triggered
482 486 # asynchronously by signal handlers, for example.
483 487 self.showtraceback()
484 488 else:
485 489 self.input_splitter.push(line)
486 490 more = self.input_splitter.push_accepts_more()
487 491 if (self.SyntaxTB.last_syntax_error and
488 492 self.autoedit_syntax):
489 493 self.edit_syntax_error()
490 494 if not more:
491 495 source_raw = self.input_splitter.source_raw_reset()[1]
492 496 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
493 497 self.run_cell(source_raw)
494 498
495 499
496 500 # Turn off the exit flag, so the mainloop can be restarted if desired
497 501 self.exit_now = False
@@ -1,63 +1,83 b''
1 1 """Tests for two-process terminal frontend
2 2
3 3 Currently only has the most simple test possible, starting a console and running
4 4 a single command.
5 5
6 6 Authors:
7 7
8 8 * Min RK
9 9 """
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Imports
13 13 #-----------------------------------------------------------------------------
14 14
15 15 import sys
16 import time
17 16
18 import nose.tools as nt
19 17 from nose import SkipTest
20 18
21 19 import IPython.testing.tools as tt
22 20 from IPython.testing import decorators as dec
23 from IPython.utils import py3compat
24 21
25 22 #-----------------------------------------------------------------------------
26 23 # Tests
27 24 #-----------------------------------------------------------------------------
28 25
29 26 @dec.skip_win32
30 27 def test_console_starts():
31 28 """test that `ipython console` starts a terminal"""
29 p, pexpect, t = start_console()
30 p.sendline('5')
31 idx = p.expect([r'Out\[\d+\]: 5', pexpect.EOF], timeout=t)
32 idx = p.expect([r'In \[\d+\]', pexpect.EOF], timeout=t)
33 stop_console(p, pexpect, t)
34
35 def test_help_output():
36 """ipython console --help-all works"""
37 tt.help_all_output_test('console')
38
39
40 def test_display_text():
41 "Ensure display protocol plain/text key is supported"
42 # equivalent of:
43 #
44 # x = %lsmagic
45 # from IPython.display import display; display(x);
46 p, pexpect, t = start_console()
47 p.sendline('x = %lsmagic')
48 idx = p.expect([r'In \[\d+\]', pexpect.EOF], timeout=t)
49 p.sendline('from IPython.display import display; display(x);')
50 p.expect([r'Available line magics:', pexpect.EOF], timeout=t)
51 stop_console(p, pexpect, t)
52
53 def stop_console(p, pexpect, t):
54 "Stop a running `ipython console` running via pexpect"
55 # send ctrl-D;ctrl-D to exit
56 p.sendeof()
57 p.sendeof()
58 p.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=t)
59 if p.isalive():
60 p.terminate()
61
62
63 def start_console():
64 "Start `ipython console` using pexpect"
32 65 from IPython.external import pexpect
33 66
34 67 args = ['console', '--colors=NoColor']
35 68 # FIXME: remove workaround for 2.6 support
36 69 if sys.version_info[:2] > (2,6):
37 70 args = ['-m', 'IPython'] + args
38 71 cmd = sys.executable
39 72 else:
40 73 cmd = 'ipython'
41 74
42 75 try:
43 76 p = pexpect.spawn(cmd, args=args)
44 77 except IOError:
45 78 raise SkipTest("Couldn't find command %s" % cmd)
46 79
47 80 # timeout after one minute
48 81 t = 60
49 82 idx = p.expect([r'In \[\d+\]', pexpect.EOF], timeout=t)
50 p.sendline('5')
51 idx = p.expect([r'Out\[\d+\]: 5', pexpect.EOF], timeout=t)
52 idx = p.expect([r'In \[\d+\]', pexpect.EOF], timeout=t)
53 # send ctrl-D;ctrl-D to exit
54 p.sendeof()
55 p.sendeof()
56 p.expect([pexpect.EOF, pexpect.TIMEOUT], timeout=t)
57 if p.isalive():
58 p.terminate()
59
60 def test_help_output():
61 """ipython console --help-all works"""
62 tt.help_all_output_test('console')
63
83 return p, pexpect, t
General Comments 0
You need to be logged in to leave comments. Login now