##// END OF EJS Templates
Merge pull request #1946 from tkf/terminal-image-handler...
Bussonnier Matthias -
r8505:ee83d92c merge
parent child Browse files
Show More
@@ -0,0 +1,95
1 #-----------------------------------------------------------------------------
2 # Copyright (C) 2012 The IPython Development Team
3 #
4 # Distributed under the terms of the BSD License. The full license is in
5 # the file COPYING, distributed as part of this software.
6 #-----------------------------------------------------------------------------
7
8 import os
9 import sys
10 import unittest
11 import base64
12
13 from IPython.zmq.kernelmanager import KernelManager
14 from IPython.frontend.terminal.console.interactiveshell \
15 import ZMQTerminalInteractiveShell
16 from IPython.utils.tempdir import TemporaryDirectory
17 from IPython.testing.tools import monkeypatch
18 from IPython.testing.decorators import skip_without
19 from IPython.utils.ipstruct import Struct
20
21
22 SCRIPT_PATH = os.path.join(
23 os.path.abspath(os.path.dirname(__file__)), 'writetofile.py')
24
25
26 class ZMQTerminalInteractiveShellTestCase(unittest.TestCase):
27
28 def setUp(self):
29 km = KernelManager()
30 self.shell = ZMQTerminalInteractiveShell(kernel_manager=km)
31 self.raw = b'dummy data'
32 self.mime = 'image/png'
33 self.data = {self.mime: base64.encodestring(self.raw).decode('ascii')}
34
35 def test_no_call_by_default(self):
36 def raise_if_called(*args, **kwds):
37 assert False
38
39 shell = self.shell
40 shell.handle_image_PIL = raise_if_called
41 shell.handle_image_stream = raise_if_called
42 shell.handle_image_tempfile = raise_if_called
43 shell.handle_image_callable = raise_if_called
44
45 shell.handle_image(None, None) # arguments are dummy
46
47 @skip_without('PIL')
48 def test_handle_image_PIL(self):
49 import PIL.Image
50
51 open_called_with = []
52 show_called_with = []
53
54 def fake_open(arg):
55 open_called_with.append(arg)
56 return Struct(show=lambda: show_called_with.append(None))
57
58 with monkeypatch(PIL.Image, 'open', fake_open):
59 self.shell.handle_image_PIL(self.data, self.mime)
60
61 self.assertEqual(len(open_called_with), 1)
62 self.assertEqual(len(show_called_with), 1)
63 self.assertEqual(open_called_with[0].getvalue(), self.raw)
64
65 def check_handler_with_file(self, inpath, handler):
66 shell = self.shell
67 configname = '{0}_image_handler'.format(handler)
68 funcname = 'handle_image_{0}'.format(handler)
69
70 assert hasattr(shell, configname)
71 assert hasattr(shell, funcname)
72
73 with TemporaryDirectory() as tmpdir:
74 outpath = os.path.join(tmpdir, 'data')
75 cmd = [sys.executable, SCRIPT_PATH, inpath, outpath]
76 setattr(shell, configname, cmd)
77 getattr(shell, funcname)(self.data, self.mime)
78 # cmd is called and file is closed. So it's safe to open now.
79 with open(outpath, 'rb') as file:
80 transferred = file.read()
81
82 self.assertEqual(transferred, self.raw)
83
84 def test_handle_image_stream(self):
85 self.check_handler_with_file('-', 'stream')
86
87 def test_handle_image_tempfile(self):
88 self.check_handler_with_file('{file}', 'tempfile')
89
90 def test_handle_image_callable(self):
91 called_with = []
92 self.shell.callable_image_handler = called_with.append
93 self.shell.handle_image_callable(self.data, self.mime)
94 self.assertEqual(len(called_with), 1)
95 assert called_with[0] is self.data
@@ -0,0 +1,33
1 #-----------------------------------------------------------------------------
2 # Copyright (C) 2012 The IPython Development Team
3 #
4 # Distributed under the terms of the BSD License. The full license is in
5 # the file COPYING, distributed as part of this software.
6 #-----------------------------------------------------------------------------
7
8 """
9 Copy data from input file to output file for testing.
10
11 Command line usage:
12
13 python writetofile.py INPUT OUTPUT
14
15 Binary data from INPUT file is copied to OUTPUT file.
16 If INPUT is '-', stdin is used.
17
18 """
19
20 if __name__ == '__main__':
21 import sys
22 from IPython.utils.py3compat import PY3
23 (inpath, outpath) = sys.argv[1:]
24
25 if inpath == '-':
26 if PY3:
27 infile = sys.stdin.buffer
28 else:
29 infile = sys.stdin
30 else:
31 infile = open(inpath, 'rb')
32
33 open(outpath, 'w+b').write(infile.read())
@@ -0,0 +1,20
1 #-----------------------------------------------------------------------------
2 # Copyright (C) 2012- The IPython Development Team
3 #
4 # Distributed under the terms of the BSD License. The full license is in
5 # the file COPYING, distributed as part of this software.
6 #-----------------------------------------------------------------------------
7
8 import os
9
10 from IPython.utils.tempdir import NamedFileInTemporaryDirectory
11
12
13 def test_named_file_in_temporary_directory():
14 with NamedFileInTemporaryDirectory('filename') as file:
15 name = file.name
16 assert not file.closed
17 assert os.path.exists(name)
18 file.write(b'test')
19 assert file.closed
20 assert not os.path.exists(name)
@@ -19,15 +19,26 from __future__ import print_function
19 19
20 20 import bdb
21 21 import signal
22 import os
22 23 import sys
23 24 import time
25 import subprocess
26 from io import BytesIO
27 import base64
24 28
25 29 from Queue import Empty
26 30
31 try:
32 from contextlib import nested
33 except:
34 from IPython.utils.nested_context import nested
35
27 36 from IPython.core.alias import AliasManager, AliasError
28 37 from IPython.core import page
29 38 from IPython.utils.warn import warn, error, fatal
30 39 from IPython.utils import io
40 from IPython.utils.traitlets import List, Enum, Any
41 from IPython.utils.tempdir import NamedFileInTemporaryDirectory
31 42
32 43 from IPython.frontend.terminal.interactiveshell import TerminalInteractiveShell
33 44 from IPython.frontend.terminal.console.completer import ZMQCompleter
@@ -36,7 +47,64 from IPython.frontend.terminal.console.completer import ZMQCompleter
36 47 class ZMQTerminalInteractiveShell(TerminalInteractiveShell):
37 48 """A subclass of TerminalInteractiveShell that uses the 0MQ kernel"""
38 49 _executing = False
39
50
51 image_handler = Enum(('PIL', 'stream', 'tempfile', 'callable'),
52 config=True, help=
53 """
54 Handler for image type output. This is useful, for example,
55 when connecting to the kernel in which pylab inline backend is
56 activated. There are four handlers defined. 'PIL': Use
57 Python Imaging Library to popup image; 'stream': Use an
58 external program to show the image. Image will be fed into
59 the STDIN of the program. You will need to configure
60 `stream_image_handler`; 'tempfile': Use an external program to
61 show the image. Image will be saved in a temporally file and
62 the program is called with the temporally file. You will need
63 to configure `tempfile_image_handler`; 'callable': You can set
64 any Python callable which is called with the image data. You
65 will need to configure `callable_image_handler`.
66 """
67 )
68
69 stream_image_handler = List(config=True, help=
70 """
71 Command to invoke an image viewer program when you are using
72 'stream' image handler. This option is a list of string where
73 the first element is the command itself and reminders are the
74 options for the command. Raw image data is given as STDIN to
75 the program.
76 """
77 )
78
79 tempfile_image_handler = List(config=True, help=
80 """
81 Command to invoke an image viewer program when you are using
82 'tempfile' image handler. This option is a list of string
83 where the first element is the command itself and reminders
84 are the options for the command. You can use {file} and
85 {format} in the string to represent the location of the
86 generated image file and image format.
87 """
88 )
89
90 callable_image_handler = Any(config=True, help=
91 """
92 Callable object called via 'callable' image handler with one
93 argument, `data`, which is `msg["content"]["data"]` where
94 `msg` is the message from iopub channel. For exmaple, you can
95 find base64 encoded PNG data as `data['image/png']`.
96 """
97 )
98
99 mime_preference = List(
100 default_value=['image/png', 'image/jpeg', 'image/svg+xml'],
101 config=True, allow_none=False, help=
102 """
103 Preferred object representation MIME type in order. First
104 matched MIME type will be used.
105 """
106 )
107
40 108 def __init__(self, *args, **kwargs):
41 109 self.km = kwargs.pop('kernel_manager')
42 110 self.session_id = self.km.session.session
@@ -163,6 +231,7 class ZMQTerminalInteractiveShell(TerminalInteractiveShell):
163 231 elif msg_type == 'pyout':
164 232 self.execution_count = int(sub_msg["content"]["execution_count"])
165 233 format_dict = sub_msg["content"]["data"]
234 self.handle_rich_data(format_dict)
166 235 # taken from DisplayHook.__call__:
167 236 hook = self.displayhook
168 237 hook.start_displayhook()
@@ -171,6 +240,61 class ZMQTerminalInteractiveShell(TerminalInteractiveShell):
171 240 hook.log_output(format_dict)
172 241 hook.finish_displayhook()
173 242
243 elif msg_type == 'display_data':
244 self.handle_rich_data(sub_msg["content"]["data"])
245
246 _imagemime = {
247 'image/png': 'png',
248 'image/jpeg': 'jpeg',
249 'image/svg+xml': 'svg',
250 }
251
252 def handle_rich_data(self, data):
253 for mime in self.mime_preference:
254 if mime in data and mime in self._imagemime:
255 self.handle_image(data, mime)
256 return
257
258 def handle_image(self, data, mime):
259 handler = getattr(
260 self, 'handle_image_{0}'.format(self.image_handler), None)
261 if handler:
262 handler(data, mime)
263
264 def handle_image_PIL(self, data, mime):
265 if mime not in ('image/png', 'image/jpeg'):
266 return
267 import PIL.Image
268 raw = base64.decodestring(data[mime].encode('ascii'))
269 img = PIL.Image.open(BytesIO(raw))
270 img.show()
271
272 def handle_image_stream(self, data, mime):
273 raw = base64.decodestring(data[mime].encode('ascii'))
274 imageformat = self._imagemime[mime]
275 fmt = dict(format=imageformat)
276 args = [s.format(**fmt) for s in self.stream_image_handler]
277 with open(os.devnull, 'w') as devnull:
278 proc = subprocess.Popen(
279 args, stdin=subprocess.PIPE,
280 stdout=devnull, stderr=devnull)
281 proc.communicate(raw)
282
283 def handle_image_tempfile(self, data, mime):
284 raw = base64.decodestring(data[mime].encode('ascii'))
285 imageformat = self._imagemime[mime]
286 filename = 'tmp.{0}'.format(imageformat)
287 with nested(NamedFileInTemporaryDirectory(filename),
288 open(os.devnull, 'w')) as (f, devnull):
289 f.write(raw)
290 f.flush()
291 fmt = dict(file=f.name, format=imageformat)
292 args = [s.format(**fmt) for s in self.tempfile_image_handler]
293 subprocess.call(args, stdout=devnull, stderr=devnull)
294
295 def handle_image_callable(self, data, mime):
296 self.callable_image_handler(data)
297
174 298 def handle_stdin_request(self, timeout=0.1):
175 299 """ Method to capture raw_input
176 300 """
@@ -3,14 +3,14
3 3 This is copied from the stdlib and will be standard in Python 3.2 and onwards.
4 4 """
5 5
6 import os as _os
7
6 8 # This code should only be used in Python versions < 3.2, since after that we
7 9 # can rely on the stdlib itself.
8 10 try:
9 11 from tempfile import TemporaryDirectory
10 12
11 13 except ImportError:
12
13 import os as _os
14 14 from tempfile import mkdtemp, template
15 15
16 16 class TemporaryDirectory(object):
@@ -74,3 +74,33 except ImportError:
74 74 self._rmdir(path)
75 75 except self._os_error:
76 76 pass
77
78
79 class NamedFileInTemporaryDirectory(object):
80
81 def __init__(self, filename, mode='w+b', bufsize=-1, **kwds):
82 """
83 Open a file named `filename` in a temporary directory.
84
85 This context manager is preferred over `NamedTemporaryFile` in
86 stdlib `tempfile` when one needs to reopen the file.
87
88 Arguments `mode` and `bufsize` are passed to `open`.
89 Rest of the arguments are passed to `TemporaryDirectory`.
90
91 """
92 self._tmpdir = TemporaryDirectory(**kwds)
93 path = _os.path.join(self._tmpdir.name, filename)
94 self.file = open(path, mode, bufsize)
95
96 def cleanup(self):
97 self.file.close()
98 self._tmpdir.cleanup()
99
100 __del__ = cleanup
101
102 def __enter__(self):
103 return self.file
104
105 def __exit__(self, type, value, traceback):
106 self.cleanup()
General Comments 0
You need to be logged in to leave comments. Login now